State and Lifecycle

Previously we made a clock that was re-render every second by a setInterval loop. Now we're going to see how to make a component that updates itself automatically using state.

function tick() {
  const clock = (
    <div id='dingo'>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  ) 
  render(
    clock,
    'body'
  )
}

setInterval(tick, 1000)

In the above example, the tick function is passing the clock element to the render function inside a setInterval. This works, but we can do better by converting it into a case-based component.

Convert a Function Component into a Class-based One

We can easily convert a function component into one the uses the Component class by following these steps:

  1. Create an ES6 class with the same name as the functional component that extends Component.
  2. Add a single empty method render() to the new class.
  3. Move the body of the functional component into the render() method.
  4. Replace props with this.props in the render() body.
  5. Delete the old, functional component.

Following these steps with our clock from above, we get this:

class Clock extends Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    )
  }
}

Now we have the basis for a Clock component, but we need to add local state and lifecycle hooks.

Adding Local State to a Class

In order to add local state to the class, we'll first need to give it a constructor. The constructor always needs to come first in a class. Because this is an extension of the Component class, we'll also need to invoke super in the constructor so we can pass the class's props to the Component class. Right after the super call we can add in state. Remember that state in the constructor needs to be attached to the this keyword. And finally, since the class now has state, we don't need to use this.props.date to access the date. Instead we switch that to use the class's state.

One more thing about component classes. When we make a functional component and pass it to render, we also pass in a selector for where we want the component to be rendered. For component classes we indicate where we want it render by giving the class a container property. In this case we'll be adding that to the constructor, right after the state:

class Clock extends Component {
  constructor(props) {
    super(props)
    this.state = {date: new Date()}
    this.container = 'body'
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    )
  }
}

Since we now have a class with state, we no longer need to pass it to the render function. Instead we simply create an instance with the new keyword:

const clock = new Clock()

Codepen Example:

See the Pen Composi Tuts -State and Lifecycle-1 by Robert Biggs (@rbiggs) on CodePen.

When we instantiate our clock as above, it will render in the browser. However, as it is the clock will not update every second. We need a way to know that the component was created and inserted into the document before starting a loop to do so.

Adding Lifecycle Methods to a Class

Class components have five different lifecycle methods that we can use. These hooks let your do something right after your component is created, before or after it is updated, and before it is destroyed. In our case, we want to start an interval loop as soon as the component was create. At the same time, if the component gets unmounted and destroyed, we want a way to end the loop. For this we will need to add componentDidMount and componentWillUnmount to our class:

See the Pen Composi Tuts -State and Lifecycle-2 by Robert Biggs (@rbiggs) on CodePen.

With the lifecycle hooks setup, instantiating a new instance of the clock will cause the loop to kick in, resulting in a clock that updates itself automatically. Using class components with state is a more efficient way of creating component with encapsulated behaviors.

Using State Correctly

Although you can in some cases change state directly, it's always better to use setState to do so. When state is a primitive type, such as string, number or boolean, it is possible to set the state directly through assignment:

See the Pen Composi Tuts -State and Lifecycle-3 by Robert Biggs (@rbiggs) on CodePen.

For objects, we pass in an object with the property and value we want to udpate:

See the Pen Composi Tuts -State and Lifecycle-4 by Robert Biggs (@rbiggs) on CodePen.

Notice how to update the employer property, we passed it in as its own small object literal. When you use setState on state that is an object, it performs a mixin where the new property/value replaces the previous one. In the case where the new property/value don't exist on the state object, they get added.

Updating an Array

Updating the state when it is an array is easy, just use a callback setState to update the array:

See the Pen Composi Tuts -State and Lifecycle-5 by Robert Biggs (@rbiggs) on CodePen.

Updating Array of Objects

Updating component state when it is an array of objects, it is more not complicate either. Again just use a callback inside setState to update the array's the object values. Suppose we have an array of persons and we assign it to a component instance called personsList:

const people = [
  {
    name: 'Joe Bodoni',
    job: 'mechanic'
  },
  {
    name: 'Ellen Vanderbilt',
    job: 'lab technician'
  },
  {
    name: 'Sam Anderson',
    job: 'developer'
  }
]

Passing a Callback to setState

The first approach is of course doable, whoever it can be a bit messy. Using the second approach results in more container result. When passing a callback to setState, the component's state gets passed as the argument of the callback. So, using this approach, let's solve the problem of updating Sam's job:

peopleList.setState(prevState => {
  // Update Sam's job:
  prevState[2].job = 'cook'
  return prevState
})

When using a callback, you must take care to always return the modified state. Not doing so will result in the component state not being updated. Returning the modified state will cause the component state to be updated properly.

Updates Use requestAnimationFrame

If you have a situation where state can get updated in rapid succession, you might be worried about unnecessary layout thrashing. However, internally Composi performs update with requestAnimationFrame. If even this is too much for you, there are other options to limit how many times state gets set. You could use _.debounce or _.throttle from lodash. You could use these on the event causing state to be set, or use them direction on the code setting state.

The Data Flows Down

We alread saw that a component can have child component, and that these are always function components. By their very nature, functional components do not know about their parent, whether it is stateless or stateful. Whatever data a child consumes it gets as props passed down from its parent.

To see how this happens, we can take another look at our class component clock example, modified to use a child component:

See the Pen Composi Tuts -State and Lifecycle-6 by Robert Biggs (@rbiggs) on CodePen.

Notice how we've create a functional component, FormattedDate. And in the render function we pass it the component state as a prop--data. FormattedDate does not know what the source of its data is. It doesn't matter. It just expects to receive and input.

Data always flows down from parent to child. This is called a unidirectional data flow. There is no two-way data binding like some libraries offer. One-way data flow makes it easy to reason about what is happening with a component's data.

State in Class Instatiation

Instead of assigning state in the class constructor, you can provide it as a property in the class instantiation. In that case you would not need to have a state assignment in the constructor. Taking our above clock example, we we get rid of its constructor and define its state and container in the class instantiation:

See the Pen Composi Tuts -State and Lifecycle-7 by Robert Biggs (@rbiggs) on CodePen.

Our class is now a bit clearner without the constuctor and we can provide the container and state when we create a new component instance. Providing the container and state during instantiation allows you to create multiple instances of a component in different places in the DOM and with different state.

Summary

State is a powerful way to make your components own their data and react when it changes. But you may prefer stateless components and use Redux, Mobx or other state management libraries instead. Check out the docs about how to use Redux and Mobx with Composi.