Stateful Components
UPDATE 2017-04-07: The "nestable" pattern described below, is no longer quite the same as what I use in my hyperapp-nestable. The basic idea is the same, but I've changed how I handle props and children passed to the component from its parent.
A frequently asked question
Just about every new user of hyperapp at some point asks the question: "How can I make components with local state?"
In React, for example, there are object-components with
render methods, and .setState()
. In Vue.js, every component has its .props
. Such components encapsulate state and behavior locally, keeping it out of the rest of the app (because it's only relevant internally to the component).
But Hyperapp's design is clear: there is one single state store, and components are purely functional. So components with local state are off the table ... right?
The trick is to realize that a stateful component is essentially a simple app, exposing an interface for interacting with it from the outside. The interface is such that it allows for composing with other components in a larger app.
Reusing an app ... in an app!
Let's begin with this, small simple gif-search app:
See the Pen gif search hyperapp by Zacharias Enochsson (@zaceno) on CodePen.
Imagine now you're working on some kind of larger social-app, where one feature is for a user to use a gif to diplay their mood. You'd like to use the gif-search app you already built above, as the mechanism for finding and selecting the appropriate mood-gif.
An simplified version of the app you'd like to write, might then look like this:
app(
//STATE
{
gif: null,
selecting: false,
},
//ACTIONS
{
select: _ => ({selecting: true}),
onselect: url => ({
gif: url,
selecting: false,
})
},
//VIEW
(state, actions) => (
<main>
<h1>I feel like:</h1>
<img src={state.gif} />
<br />
{
state.selecting
? <GifSearch onselect={actions.onselect} />
: <button onclick={actions.select}>Select GIF</button>
}
</main>
),
//CONTAINER
document.body
)
Even though the <GifSearch />
component doesn't exist yet, it's clear what we want to
achieve:
- When the user clicks the "Select GIF"-button, we should render the gif search app.
- When the user clicks (or somehow "selects") one of the gifs, we expect a callback, with the url to the selected gif.
- The newly selected gif is displayed. Simultaneously, the gif search dissapears.
It is also clear that we don't expect to have to handle any of the search component's state or actions in the main app.
The first step is to make sure that when we render <GifSearch />
, we start the search app and render it at that place in the DOM:
const GifSearch = props => h('gif-search', {
oncreate: element => {
const actions = app(
gifSearchState,
gifSearchActions,
gifSearchView,
element,
)
}
})
With this, the <GifSearch />
component produces an UnkonwnHTMLElement
in the DOM with the tag name gif-search
. When the element is created, we initiate the search app with the element as it's container.
Now, the question is what to do with the onselect
property we pass to the <GifSearch />
component? We need to somehow make the search app is aware of the callback, and make use it to call back when the user selects a gif.
Did you notice the generic update
action in the gif-search app above?
const actionDefinitions = {
update: x => x,
...
... all it does is update the state with whatever partial-state you pass it as the argument. The search app uses it to set the state before and after performing the search. But since an app's actions are returned from the app(...)
call, we have access to it, and can use it to inject the component's state with whatever props are passed from the main app -- in particular the onselect
callback.
const GifSearch = props => h('gif-search', {
oncreate: element => {
const actions = app(
gifSearchState,
gifSearchActions,
gifSearchView,
element,
)
actions.update(props) //<------ THIS
}
})
This means the onselect
callback is available in the state of the search app. So let's add a click-handler to the images, so that onselect
is called when a gif-thumbnail is clicked:
<img key={gif} src={gif} width="80" height="80" onclick={_ => state.onselect(gif)} />
This is enough to turn our gif-search app into a reusable component for the example app above:
See the Pen gif search hyperapp - 2 by Zacharias Enochsson (@zaceno) on CodePen.
Generalizing the pattern
So that works, but of course it would be even nicer to have a function that could make our stateful component for us.
const GifSearch = appComponent(
gifSearchState,
gifSearchActions,
gifSearchView,
'gif-search'
)
...using the pattern we had before:
const appComponent = (initialState, actionDefinitions, view, tagName) =>
props =>
h(tagName, {
oncreate: element => {
const actions = app(
initialState,
actionDefinitions,
view,
element,
)
actions.update(props)
}
})
While that worked for the example above, there are some significant deficiencies if we want to
use appComponent
more generally.
First: It is not enough to call the update function just when the element is created -- we must call it everytime the main app rerenders, to make the sub-app aware of prop changes.
Since the same element will be provided to both the oncreate
and onupdate
lifecycle methods, that's an opportuninty to store the update
action, so that we can call it in onupdate
as well as in oncreate
.
const appComponent = (initialState, actionDefinitions, view, tagName) =>
props =>
h(tagName, {
oncreate: element => {
const actions = app(
initialState,
actionDefinitions,
view,
element,
)
actions.update(props)
element._update = actions.update // <---- THIS
},
onupdate: element => element._update(props) // <--- THIS
})
Of course, we can't rely on the actionDefinitions
to have an update
action. To be safe, we need to add such an action ourselves. We'll use a name that is unlikely to be used, to avoid naming collisions:
const appComponent = (initialState, actionDefinitions, view, tagName) =>
props =>
h(tagName, {
oncreate: element => {
actionDefinitions._$update = x => x // <--- THIS
const actions = app(
initialState,
actionDefinitions,
view,
element,
)
actions._$update(props)
element._update = actions._$update
},
onupdate: element => element._$update(props)
})
Finally, since it's important that the element doesn't change between oncreate
and subsequent onupdate
-calls, the user will likely want to key their components. So let's make sure to pass along them to the vnode:
const appComponent = (initialState, actionDefinitions, view, tagName) =>
props =>
h(tagName, {
key: props.key // <--- THIS
oncreate: element => {
actionDefinitions._$update = x => x
const actions = app(
initialState,
actionDefinitions,
view,
element,
)
actions._$update(props)
element._update = actions._$update
},
onupdate: element => element._$update(props)
})
Finally
Now that you made it through this article, and you know how to write a function to turn app definitions into stateful components, the prize is: you'll never have to!
See hyperapp-nestable which implements almost exactly this pattern. (It also adds the feature of having an initialization action and an ondestroy action)
The final example of our mood-gif display, with reusable gif-search component, using hyperapp-nestable is here.
See the Pen gif search hyperapp - 3 by Zacharias Enochsson (@zaceno) on CodePen.