Stateful Components

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:

Y29uc3QgeyBkaXYsIGJ1dHRvbiB9ID0gSDsKCmZ1bmN0aW9uIFNwaW5uZXIocHJvcHMpIHsKICByZXR1cm4gZGl2LnJvd2ZsZXgoCiAgICBidXR0b24oJiMzNDstJiMzNDspLAogICAgZGl2KGAke3Byb3BzLnZhbHVlfWApLAogICAgYnV0dG9uKCYjMzQ7KyYjMzQ7KQogICk7Cn0KClIoSChTcGlubmVyLCB7dmFsdWU6IDB9KSwgZG9jdW1lbnQuYm9keSk7
Loading interactive example...

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:

Y29uc3QgeyBkaXYsIGJ1dHRvbiB9ID0gSDsKCmZ1bmN0aW9uIFNwaW5uZXIoCiAgcHJvcHMsIAogIHt2YWx1ZT1wcm9wcy52YWx1ZX0sIAogIHNldFN0YXRlCikgewogIHJldHVybiBkaXYucm93ZmxleCgKICAgIC8vIERlY3JlbWVudCBidXR0b24KICAgIGJ1dHRvbih7CiAgICAgIG9uY2xpY2soKSB7CiAgICAgICAgc2V0U3RhdGUoeyB2YWx1ZTogdmFsdWUgLSAxIH0pCiAgICAgIH0KICAgIH0sICYjMzQ7LSYjMzQ7KSwKICAgIC8vIFZhbHVlIGxhYmVsCiAgICBkaXYoYCR7dmFsdWV9YCksCiAgICAvLyBJbmNyZW1lbnQgYnV0dG9uCiAgICBidXR0b24oewogICAgICBvbmNsaWNrKCkgewogICAgICAgIHNldFN0YXRlKHsgdmFsdWU6IHZhbHVlICsgMSB9KQogICAgICB9CiAgICB9LCAmIzM0OysmIzM0OykKICApOwp9CgpSKEgoU3Bpbm5lciwge3ZhbHVlOiAwfSksIGRvY3VtZW50LmJvZHkpOw==
Loading interactive example...

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 eleemtn
  • completed : A boolean flag that indicates if the item is completed
  • oncomplete : 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 call setState and also call a handler function that you received as property, make sure to call setState 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:

Y29uc3QgeyBkaXYsIGJ1dHRvbiwgaW5wdXQgfSA9IEg7CgpmdW5jdGlvbiBUb2RvSXRlbSh7IHRleHQsIGNvbXBsZXRlZCwgb25jb21wbGV0ZSB9KSB7CiAgcmV0dXJuIGRpdi5yb3dmbGV4KAogICAgYnV0dG9uKAogICAgICB7IG9uY2xpY2s6IG9uY29tcGxldGV9LCAKICAgICAgJiMzNDtPayYjMzQ7CiAgICApLAogICAgZGl2W2NvbXBsZXRlZCA/ICYjMzQ7c3RyaWtlJiMzNDsgOiAmIzM0OyYjMzQ7XSh0ZXh0KQogICk7Cn0KCmZ1bmN0aW9uIElucHV0RmllbGQoeyBvbmNyZWF0ZSB9LCB7dGV4dD0mIzM0OyYjMzQ7fSwgc2V0U3RhdGUpIHsKICByZXR1cm4gZGl2LnJvd2ZsZXgoCiAgICBpbnB1dCh7CiAgICAgIHR5cGU6ICYjMzk7dGV4dCYjMzk7LAogICAgICB2YWx1ZTogdGV4dCwKICAgICAgb25jaGFuZ2UoZSkgewogICAgICAgIHNldFN0YXRlKHsgdGV4dDogZS50YXJnZXQudmFsdWUgfSkKICAgICAgfQogICAgfSksCiAgICBidXR0b24oewogICAgICBvbmNsaWNrKCkgewogICAgICAgIHNldFN0YXRlKHsgdGV4dDogJiMzNDsmIzM0OyB9KTsKICAgICAgICBvbmNyZWF0ZSh0ZXh0KTsKICAgICAgfQogICAgfSwgJiMzNDtDcmVhdGUmIzM0OykKICApCn0KCmZ1bmN0aW9uIFRvZG9MaXN0KHByb3BzLCBzdGF0ZSwgc2V0U3RhdGUpIHsKICBjb25zdCBhZGRJdGVtID0gdGV4dCA9Jmd0OyB7CiAgICBzZXRTdGF0ZSh7CiAgICAgIGl0ZW1zOiBbXS5jb25jYXQoewogICAgICAgIHRleHQsCiAgICAgICAgY29tcGxldGVkOiBmYWxzZQogICAgICB9LCAoc3RhdGUuaXRlbXMgfHwgW10pKQogICAgfSkKICB9OwogIGNvbnN0IGNoZWNrSXRlbSA9IGluZGV4ID0mZ3Q7IHsKICAgIHN0YXRlLml0ZW1zW2luZGV4XS5jb21wbGV0ZWQgPSAKICAgICAgIXN0YXRlLml0ZW1zW2luZGV4XS5jb21wbGV0ZWQ7CiAgICBzZXRTdGF0ZSh7IH0pOwogIH07CgogIHJldHVybiBkaXYoCiAgICBIKElucHV0RmllbGQsIHsgb25jcmVhdGU6IGFkZEl0ZW0gfSksCiAgICAoc3RhdGUuaXRlbXMgfHwgW10pLm1hcCgoaXRlbSwgaW5kZXgpID0mZ3Q7IEgoVG9kb0l0ZW0sIHsKICAgICAgazogaXRlbS50ZXh0LAogICAgICB0ZXh0OiBpdGVtLnRleHQsCiAgICAgIGNvbXBsZXRlZDogaXRlbS5jb21wbGV0ZWQsCiAgICAgIG9uY29tcGxldGU6IGNoZWNrSXRlbS5iaW5kKHRoaXMsIGluZGV4KQogICAgfSkpLAogICk7Cn0KClIoSChUb2RvTGlzdCksIGRvY3VtZW50LmJvZHkpOw==
Loading interactive example...