Quick intro to React hooks

React 16.8 was released in February 2019. One of the major additions in this version is a feature called Hooks. Hooks allow us to use state in function components and provide a way to access lifecycle methods such as componentDidMount() and componentDidUpdate().

Before hooks in order to make use of the state and lifecycle methods, you had to create a class component. An additional benefit that class components had is React.PureComponent. Instead of extending React.Component, you could extend React.PureComponent. This class implemented shouldComponentUpdate() lifecycle method with a shallow prop and state comparison, nice performance optimisation! However, a similar technique for function components was introduced in React 16.6 – React.memo (a HOC that works by memoizing/caching the result).

import React from 'react';

const Button = () => React.memo((props) => {
  // re-renders only if props change
});

The addition of hooks is a non-breaking change therefore we can carry on using classes whenever we need to. Also React team does not recommend re-writing class components into function components. Instead, they suggest that it is a good idea to try out hooks when building new components.

Yes, hooks is a new feature, a new way to work with state but it doesn’t mean that you’ll have to change the way you write React applications. You should definitely first try out hooks in the non-critical parts of your application and see how it works for you.

State in function components

Here’s a simple function component. It renders a button and simulates an API call (that fetches some data) when the button is pressed.

import React from 'react';

// simulate an API call
const fetchData = () => {
  return setTimeout(() => {
    console.log('fetching finished');
    return [];
  }, 1000);
};

export const App = () => {
  return (
    <div>
      <button onClick={fetchData}>Get data</button>
    </div>
  );
};

Let’s suppose we want to show some text (for example, ‘Loading’) when the data is being fetched to provide a better user experience. We have to use state. For now, we will convert this function into a class component.

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isDataFetching: false
    };

    this.renderLoader = this.renderLoader.bind(this);
    this.fetchData = this.fetchData.bind(this);
  }

  fetchData() {
    this.setState({
      ...this.state,
      isDataFetching: true
    });

    return setTimeout(() => {
      this.setState({
        ...this.state,
        isDataFetching: false
      });

      return [];
    }, 1000);
  }

  renderLoader() {
    if (this.state.isDataFetching) {
      return <p>Loading</p>;
    }

    return null;
  }

  render() {
    return (
      <div>
        { this.renderLoader() }
        <button onClick={this.fetchData}>Get data</button>
      </div>
    );
  }
}

That’s quite a change! And all of this just to add state to the component? There must be a better way… And sure enough, there is now!

useState hook

useState is the first hook that we’ll look at. It lets us add state to function components. Let’s see it in action.

// [1] imports useState hook
import React, { useState } from 'react';

const App = () => {
  /*  [2] useState is the same as this.state in a class component. 
   *  It preserves state between function calls.
   *
   *  We pass the initial state (false) into useState as an argument.
   *
   *  useState function returns the current state (isDataFetched) and the function to update it 
   *  (isDataFetching).
   */
  const [isDataFetched, isDataFetching] = useState(false);

  const fetchData = () => {
    // [3] update the state
    isDataFetching(true);

    return setTimeout(() => {
      console.log('fetching finished');
      // [4] update the state again
      isDataFetching(false);
      return [];
    }, 1000);
  };
  
  const renderLoader = () => {
    // [5] check the state
    if (isDataFetched) {
      return <p>Loading</p>;
    }
    
    return null;
  }
  
  return (
    <div>
      { renderLoader() }
      <button onClick={fetchData}>Get data</button>
    </div>
  );
};

You can see how elegant this solution is with the minimal code change.

We can call useState() multiple times for each state variable or put all of the state in a single object.

import React, { useState } from 'react';

const App = () => {
  // [1] we use multiple state variables here, they are easy to update and manage
  const [isDataFetched, isDataFetching] = useState(false);
  const [clickCount, incrementClickCount] = useState(0);
  const [items, addItem] = useState([]);
  
  const fetchData = () => {
    // [2] update the state
    isDataFetching(true);
    incrementClickCount(clickCount + 1);

    return setTimeout(() => {
      console.log('fetching finished');
      // [3] update the state again
      isDataFetching(false);
      return [];
    }, 1000);
  };

  return (
    <div>
      { renderLoader() }
      <button onClick={fetchData}>Get data</button>
    </div>
  );
}

The downside of using a single object is that we will have to manually merge the old and the new state. It works differently from this.setState that does shallow merge.

import React, { useState } from 'react';

const App = () => {
  // [1] we use a single object to manage state
  const [state, setState] = useState({
    isDataFetched: false,
    clickCount: 0,
    items: []
  });

  const fetchData = () => {
    // [2] we have to remember to update the state manually
    setState((state) => {
      return { 
         ...state, 
         isDataFetching: true,
         clickCount: state.clickCount + 1
      };
    });

    return setTimeout(() => {
      console.log('fetching finished');
      // [3] update the state again
      setState((state) => {
        return { 
          ...state, 
          isDataFetching: false
        };
      });
      return [];
    }, 1000);
  };

  return (
    <div>
      { renderLoader() }
      <button onClick={fetchData}>Get data</button>
    </div>
  );
}

There are a number of other hooks that React provides. The most basic and commonly used ones are useEffect(func) and useContext(context). useEffect(func) allows us to perform side effects (data fetching, DOM manipulation, etc.). The function that we pass in as an argument will run after the render phase. useContext(context) allows us to read context and subscribe to changes.

There are also a number of other React hooks.

So why use hooks?

Hooks make it possible for us to test stateful logic independently. We can extract the logic from a component and share between other components or projects. This also makes it easier to test it independently.

Have you ever had your lifecycle methods do multiple unrelated things in complex components? If you have then I bet you found it to be quite messy, tricky to test and perhaps buggy. Hooks solve this by making it possible to split a component into functions, that contain related logic instead of splitting based on the lifecycle methods.

Classes in JavaScript are just syntactic sugar, they simply do not exist (they are a thin layer over prototypal inheritance). The way this (reference variable) works in JS classes is different from many other languages and can be a bit of a pain to understand. So React developers who need to use classes have to learn and understand them first. And of course, there is also an age-old debate about class vs function components. But now hooks allow us to use state and lifecycle methods without ever worrying about creating classes.