This is the third part of a series on using Preact and HTM without transpilation.
- Part 1: Conceptual introduction & simple, single component, static site.
- Part 2: Nested components, injecting children components, html & CSS best practices
- Part 3: State and events. <<< You are here.
;^)
- Not yet published: Routing
- ???
State & Events Up
If we're truly using the functional programming paradigm, I think we can argue that there should be no state in our components. There is state somewhere, but it lives outside of our UI "tree".
There are some exceptions to this that we'll cover in a bit, but for the most part, we want to store state upsteam from our Preact UI code.
Look, this is one of the more controversial takes (if you ignore that our goal is to avoid transpilation!) you're going to get from me here. Most folks are going to use some sort of library for state management, but I'd counter by saying that if you need to manage state in a way much more complicated than what you have here, you've got a code smell.
Try to keep interactions small enough that the paradigm offered here works easily, and if you can't, see if you can't simplify. You'll thank me later. And if you can't, welcome to the world of Redux or MobX or whatever you choose.
Let's show what a simple state management setup might look like.
WARNING: I'm posting this in reverse chronological order so they appear correctly in the blog, which lists the most recent first.
Start by creating a file, state.js, at the root of your project and import it into index.html.
var state = {
messages: [
"This is the first message",
"This is the second message",
"This is the third message",
"This message refuses to declare its ordinal",
],
};
Then in index.html, add:
<script src="./state.js"></script>
Now let's refactor ParentComponent
so that it can loop through these messages. Just for fun, we'll have ParentComponent
send children to MyComponent
iff the messages
count is even.
(function () {
window.ParentComponent = function (props) {
var helloChild = html`<b>hello, world</b>`;
var toRender = props.messages.map(
(x, i) =>
html`<${MyComponent} displayText="${x}">
${i % 2 ? helloChild : null}
</MyComponent>`
);
return html`<div>${toRender}</div>`;
};
})();
So far we have a read-only state system with no events, but we do have a really easy way to produce more MyComponent
components!
Add an event handler
Now let's rig up a way to capture events from MyComponent
to alter that state.
First, we'll create a link in MyComponent
and link it to an event handler, if it exists, in props
. We really don't need to add an a
tag to add an onclick
handler, but it is conventional.
MyComponent.js
(function () {
window.MyComponent = function (props) {
var clickHandler = function () {
if (props.hasOwnProperty("ordinal") && utils.isFunction(props.clickHandler)) {
props.clickHandler(props.ordinal, `This is a new message from ${props.ordinal}`);
} else {
console.log("no handler");
}
};
var toDisplay = props.children || html`<p>This is some normally-sized text.</p>`;
return html`<div class=${props.class}>
<h3>#${props.displayText}#</h3>
<a href="#" onclick=${clickHandler}>${toDisplay}</a>
</div>`;
};
})();
You'll notice this code calls
utils.isFunction
. I've defined that in a file called utils.js, but you could put this function inline in index.html or whatever you want.utils.js
(function () { window.utils = { isFunction: function (x) { return Object.prototype.toString.call(x) == "[object Function]"; }, }; })();
Let's create an event handler!
First, I'm going to have the event handler live in a global object that I put into state.js so that the datastore/state will live in the same scope. (Points for noticing that since state
is global, that's not necessary with this simple setup.)
window.eventManager = {
updateMessageByOrdinal: function (ordinal, newMessage) {
var cleanedOrdinal = parseInt(ordinal, 10);
if (cleanedOrdinal < state.messages.length) {
state.messages[cleanedOrdinal] = newMessage;
}
},
};
Now let's pass this down to the component tree.
We have ParentComponent
between index.html and MyComponent
, so we know ParentComponent
needs to pass the event handler object to MyComponent
.
Let's first add some code to ParentComponent
so that it expects an event handler.
The two changes are to add the myComponentClickHandler
variable which will hold the handler if it's defined in props
(setting it to a NOOP default if it's not), and then passing that to each MyComponent
we spin up by passing a refence to the MyComponent
s' props
' clickHandler
(clickHandler=${myComponentClickHandler})
.
(function () {
window.ParentComponent = function (props) {
// NEW!!!!!!!!!!!!!!!!
var myComponentClickHandler =
utils.isFunction(props.clickHandler)
? props.clickHandler
: () => console.log("default/nonhandler");
// end of new (though see clickHandler prop, below)
var helloChild = html`<b>hello, world</b>`;
var toRender = props.messages.map(
(x, i) =>
html`<${MyComponent}
displayText=${x}
ordinal=${i}
clickHandler=${myComponentClickHandler}
>
${i % 2 ? helloChild : null}
</MyComponent>`
);
return html`<div>${toRender}</div>`;
};
})();
Okay, look, I know,
eventManager
is global. We could just access it inMyComponent
aswindow.eventManager
(which, if you've all state management kewl, you might say is very MobX-like):(function () { window.MyComponent = function (props) { var clickHandler = window.eventMaanager; // <<<<<<<<<<<<<< var toDisplay = props.children || html`<p>This is some normally-sized text.</p>`; return html`<div class=${props.class}> <h3>#${props.displayText}#</h3> <a href="#" onclick=${clickHandler}>${toDisplay}</a> </div>`; }; })();
And that would work! It's ugly and it's not especially self-commenting, but it would work! As I mentioned by comparing this setup to MobX, importing your state management outside of
props
isn't even considered a broken pattern.But doing things this way does create an external dependency for
MyComponent
that the previous version didn't. That is, now if you don't set up window.eventManager, you can't useMyComponent
. I prefer self-contained code -- it's a good way to insure you have a proper separation of concerns.So I'm going to bucket-brigade it from my root context in index.html all the way down.