An introduction to hooks in React

Function components in React are awesome. They let you create a component without the overhead and extra code of a class. However, up until recently, class components did have an edge over function components. Specifically: class components could have state, whereas function components were doomed to be stateless. Now, with the addition of hooks in React 16.8, function components can have state as well! This opens up a lot of possibilities and has a neat little side-effect that I am going to show you in this post.

We use plenty of React here at Yoast. Not only in our plugins, but also in MyYoast, our customer environment. With the Gutenberg editor, React even made its entrance into WordPress itself. In fact, in June 2019, Gutenberg introduced its own hooks'useSelect'and'useDispatch'

What are hooks, exactly?

Hooks let you use state and lifecycle features in function components. In the past, you could only use these other features in-class components. The easiest way to explain what hooks do, and what their advantages are, is with an example.

Let us create a simple counter component. The counter should do two things:

  1. Keep track of a count.
  2. Update the count by one every second.

We are going to create the counter in two ways: as a class, and as a function with hooks. Afterwards, we are going to compare the results.

The counter as a class

import React from 'react';

class Counter extends React.Component {

  constructor( props ) {
     super( props );
     this.state = {
        count: this.props.initialCount,
        intervalID: null,
     };
     this.updateCount = this.updateCount.bind( this );
  }

  updateCount() {
     this.setState( { count: this.state.count + 1 } );
  }

  componentDidMount() {
     const intervalID = window.setInterval( this.updateCount, 1000 );
     this.setState( { intervalID } );
  }

  componentWillUnmount() {
     window.clearInterval( this.state.intervalID );
  }

  render() {
     return <p>
        { this.state.count } seconds and counting!
     </p>
  }

}

export default Counter;

This is a classic React class component. It uses the React state to keep track of the count. Updating the count is done by this function call:

const intervalID = window.setInterval( this.updateCount, 1000 );

This tells the internet browser to update the count every 1000 milliseconds (or every one second). It is wrapped in a method called 'componentDidMount'. Adding the code to this method tells React to run it once: when the component is added to the screen. If the component is removed, we tell the browser to stop updating the count by calling 'window.clearInterval'.

The counter as a function with hooks

import React, { useEffect, useState } from 'react';

function Counter( { initialCount } ) {

  // Count state.
  const [ count, setCount ] = useState( initialCount );

  // Update count every second.
  useEffect( () => {
     const updateCount = () => setCount( count => count + 1 );

     const intervalID = window.setInterval( updateCount, 1000 );

     return () => {
        window.clearInterval( intervalID );
     }
  }, [] );

  return <p>
     { count } seconds, and counting!
  </p>

}

export default Counter;

This is a function-component. The function does the same as the class, but uses hooks instead. Specifically, the 'useState' and the 'useEffect' hooks.

What does the useState hook do?

The 'useState' hook lets you use state in your function component. It takes the form of a function call:

const [ count, setCount ] = useState( initialCount );

useState gets one argument: the initial value of a state variable. In turn, it returns two things: the current value of the state, and a function to update the state with a new value.

In our case, we supply it with an initial count and get back the current count and a function to set the count to a different value.

The current value stays the same between renders, so you do not need to worry about keeping track of it yourself!

What does the useEffect hook do? 

The 'useEffect' hook provides a way to hook into the lifecycle of a component. Class components implement this functionality as methods. Methods like `componentDidMount`, for when the component is first shown, and `componentWillUnmount`, when the component is removed again.

'useEffect', like other React hooks, takes the form of a function call:

useEffect( () => {
     const updateCount = () => setCount( count => count + 1 );

     const intervalID = window.setInterval( updateCount, 1000 );

     return () => {
        window.clearInterval( intervalID );
     }
  }, [] );

It takes two arguments. The first argument is a callback function. The second argument is a list of variables. The callback function is called when:

  1. The component is first shown on screen.
  2. One of the variables in the second argument changes.

Since we only want to callback to fire in the first case, we provide an empty array as the second argument.

What to do when components unmount?

You can let the callback function return another function. React calls this other function when the component is removed from the screen. We use it to tell the browser to stop updating by calling 'window.clearInterval', just like in the class component.

What are the advantages of hooks?

The obvious advantage is that we can use state and lifecycle in function components. Features we could only use in classes before. However, there is one other major advantage. Hooks group together code that naturally belongs together. This makes the code a lot more readable, and maintainable. Furthermore, we can combine the hooks into a separate function. This way, we can reuse it in another component!

We can see this in action if we highlight the code examples. The lines that handle the state are colored in yellow. The lines that handle the setup and tear down of the component are colored in blue:

Class component


import React from 'react';
class Counter extends React.Component {
  constructor( props ) {
     super( props );
    
     this.state = {
        count: this.props.initialCount,
        intervalID: null,
     };
     this.updateCount = this.updateCount.bind( this );
  }
  updateCount() {
     this.setState( { count: this.state.count + 1 } );
  }
  componentDidMount() {
     const intervalID = window.setInterval( this.updateCount, 1000 );
     this.setState( { intervalID } );
  }
  componentWillUnmount() {
     window.clearInterval( this.state.intervalID );
  }
  render() {
     <p>
        { this.state.count } seconds and counting!
     </p>
  }
}

Function component


import React, { useEffect, useState } from 'react';
function useCount( initialCount ) {
  // Count state.
  const [ count, setCount ] = useState( initialCount );
  // Update count every second.
  useEffect( () => {
     const updateCount = () => setCount( count => count + 1 );
     const intervalID = window.setInterval( updateCount, 1000 );
     return () => {
        window.clearInterval( intervalID );
     }
  }, [] );

  return count;
}
function Counter( { initialCount } ) {
  const count = useCount( initialCount );
  return <p>
     { count } seconds, and counting!
  </p>;
}
export default Counter;

In the class component, the code is all over the place. The lines that handle the state are divided between three methods. The lines that handle the setup and teardown are divided between two methods. This is different in the function component.

In the function component, the setup and teardown of the component are handled by the `useEffect` hook. The `useState` hook manages most of the state. Did you notice that I combined them into a new hook called `useCount`? This way we can easily reuse it in other components.

Conclusion

Hooks make a great difference in React. With hooks, you can add state and other functionality to function components. Moreover, hooks make it easier to group code that belongs together. This, in turn, makes your React components more readable. It also allows you to create new hooks, which can be easily reused in other components.