Stateful Components
Components can only carry properties given, but can also maintain their own state.
For example, consider the case of a spinner widget, where the user has two buttons : one to increment the value and one to decrement, like so:
But what should happen when the user clicks the “+” or the “-” button? To answer this question we will introduce you to the Component State
Component State
Every component is created with an empty state object, initialized to { }
. This object persists throughout the life-cycle of the component and is disposed when unmounted.
The component can update it’s state by calling the setState
method, giving the new values for the state properties. This will cause the element to re-render in order to apply the new values.
The state
and setState
variables are passed as arguments to the component function, right after the props
object:
function Welcome(props, state, setState) {
// ...
}
This means that we can now re-write our spinner widget using the state object like so:
State Propagation
When designing stateful components, it’s important to find out where the state of your application will be. It is a good practice to keep the state to the top-most component and pass down portions of it, to stateless components.
As an exception of this rule, it is also acceptable to use stateful components that maintain a temporary state of a user-interfacing component.
Example
Consider the more complicated example of a ToDo List App. In this example, we are using three kinds of components:
- The
TodoList
component that renders the items and the input field - The
TodoItem
component that renders a single item and allows the user to mark it as completed. - The
InputField
where the user enters the new item text
In such cases it’s important to find out where the state is going to be maintained. It is typically a good practice to keep the state to the root component and pass down portions of it to stateless components.
Let’s start designing our leaf components. First the TodoItem component:
function TodoItem({ text, completed, oncomplete }) {
return div.rowflex(
button(
{ onclick: oncomplete},
"Ok"
),
div[completed ? "strike" : ""](text)
);
}
This stateless component accepts the following properties:
text
: The text of the eleemtncompleted
: A boolean flag that indicates if the item is completedoncomplete
: A callback function to call when the user completes the item
Then let’s design the InputField component:
function InputField({ oncreate }, {text=""}, setState) {
return div.rowflex(
input({
type: 'text',
value: text,
onchange(e) {
setState({ text: e.target.value })
}
}),
button({
onclick() {
setState({ text: "" });
oncreate(text);
}
}, "Create")
)
}
Important! When in the same handler you callsetState
and also call a handler function that you received as property, make sure to callsetState
first and then call-out to the handler.
This is a stateful component, that uses it’s local state only for keeping track of the user input. It still forwards the important events to the parent.
It accepts the following properties:
oncreate
: A callback function that will be called when the user clicks the “Create” button, passing down the value the user has entered.
And finally, let’s design the TodoList component:
function TodoList(props, {items=[]}, setState) {
const addItem = text => {
setState({
items: [].concat({
text,
completed: false
}, (state.items || []))
})
};
const checkItem = index => {
items[index].completed =
!items[index].completed;
setState({ items });
};
return div(
items.map((item, index) => H(TodoItem, {
text: item.text,
completed: item.completed,
oncomplete: checkItem.bind(index)
})),
H(InputField, {
oncreate: addItem
})
);
}
And here is what the full example looks like: