INDIVIRTUAL - TECHNISCH PARTNER IN DIGITALE DIENSTVERLENING

React Hooks explained

January 1, 2020

React Hooks explained

React Hooks has been introduced with the release of React 16.8. This addition made it possible to use state and lifecycle methods in functional components. This blogpost is a 5 minute read and covers some benefits of working with React Hooks, using examples you may recognize as a React developer. Finally, I will discuss some tricks to optimize your application with Hooks.

Local state

Many of us have done it: changing functional components into class components, because we changed our mind and decided we could use some local state. After rewriting our component, calling super(props) and adding a constructor to set the initial state, we were finally able to access the state with this.state. Thankfully, with the useState Hook this struggle is over. All we need to do to add some local state to our functional component, is importing this Hook from the react library and declare a new state variable like this:

import { useState } from ‘react’;

function Example(){
    const [count, setCount] = useState(0);	
};

Now, we can easily read the state {count} and update the state with setCount(). No need for using the this keyword, as the useState Hook outputs local variables. Notice that the only argument passed into the useState Hook is the initial state. Unlike before Hooks, the state doesn’t have to be an object, it can be whatever you like. If we need to store multiple variables in our state, we just call useState several times. If you are dealing with a more complex state containing multiple variables relying on each other, you could use useReducer as an alternative to useState.

Global state

It never seemed idealistic, passing props down the component tree to get access to some data in a lower level component. Even if the intermediate components itself didn’t need to have any knowledge of this data, it still needed to receive and forward those props, because its children need them.
Not the most brilliant solution, right? Fortunately, React has provided us with the useContext Hook, which allows us to access data globally without having to pass props down manually at every level. This is how to do it. First, we create some context:

import React from "react";
    
const initialCount = {
    count: 0
};
    
const CounterContext = React.createContext(initialCount);

Next, we need a provider and add a value to it:

import React from "react";       
    
const initialCount = {
    count: 0
};
    
const CounterContext = React.createContext(initialCount);

function CounterProvider(props) {              
  return (
    <CounterContext.Provider value={initialCount}>
      {props.children}
    </CounterContext.Provider>
  );
};

export { CounterContext, CounterProvider };  

Now, we’re able to access {initialCount} from every component that is inside the CounterProvider component.
Hooks comes in when we want to consume this context. In order to do this, we need to import the useContext Hook from the react library and import our context { CounterContext }. Then, we create a reference to the context with the useContext Hook:

import React, { useContext } from "react";
import { CounterContext } from "./Context";
    
function Counter() {
    
    const context = useContext(CounterContext);
    
    return (
        <h5>Count: {context.count}</h5>
    );
};
    
export default Counter;

Now, we’re able to read the value from our context with {context.count}. It get’s more interesting when we combine useContext with useState or useReducer. This does the magic; it allows us not only to read the data from our context, but also to update it from anywhere in our application. For those of you who used any third-party dependencies in the past to manage global state, like Redux, this combination of Hooks will come in very useful. Also the pattern may look familiar. Here, I created a reducer and added the returned dispatch method and state as values to the provider:

const reducer = (state, action) => {
    switch (action.type) {
        case "increment":
            return { ...state, count: state.count + 1 };
        case "decrement":
            return { ...state, count: state.count - 1 };
        case "reset":
            return { count: 0};    
        default:
            return;
    };
};
    
function CounterProvider(props) {
    const [state, dispatch] = useReducer(reducer, initialCount);
    
    return (
        <CounterContext.Provider value={{ state, dispatch }}>
            {props.children}
        </CounterContext.Provider>
    );
};
    
export { CounterContext, CounterProvider };

Now, we can use the dispatch method from anywhere in our application to update the state:

function Counter() {
    
const { state, dispatch } = useContext(CounterContext);
    
return (
    <>
        <h5>Count: {state.count}</h5>
        <button onClick={ () => dispatch({ type: "increment" })}> + </button>
        <button onClick={ () => dispatch({ type: "decrement" })}> - </button>
        <button onClick={ () => dispatch({ type: "reset" })}> reset </button>
    </>
  );
};

Mind you, as we talking global state now, the data get’s updated in every component that consumes this context.

Lifecycle methods

React’s lifecycle methods are used to cause side effects. For example to fetch data when our component mounts, clean up after it unmounts or update when the props change. The useEffect Hook combines three commonly used lifecycle methods; componentDidMount, componentDidUpdate and componentWillUnmount. Here’s how it works. The first parameter you pass into the useEffect Hook must be a function (the effect). Optionally, we could return from this function another function. This specifies what needs to run after the component unmounts (the cleanup). By default useEffect runs after every render. To optimize this we could add a second parameter, which is an array of dependencies. If this array is empty, then useEffect is only called after the first render. When you add values to this array, then changing these values causes a re-render of the component and useEffect is called again:

import { useEffect } from ‘react’;
    
useEffect(() => {
    console.log('apply the effect')
    return console.log('clean up')
}, [dependencies]);

Optimization with Hooks

To speed up or optimize applications, a commonly used technique is called memoization. With this technique results from a function call are stored and returned from the cache when the same input occurs again. React provides us with two Hooks we can use for memoization; useCallback to cache a function and useMemo to cache a value.
As known, changing the props or state automatically causes a re-render of a React component, which is usually a good thing. However, some re-renders are unnecessary. For example, when we pass a new function to a child component each time the component changes:

function Counter(){
    const [count, setCount] = useState(0)
    
    const increment = () => {
        setCount(count + 1)
    };
    
    const decrement = () => {
        setCount(count - 1)
    };
    
    return (
        <>
            <h5>Count: {count}</h5>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </>
        )
    };

In this case, both two functions are created again, every time the counter is updated. This causes a re-render, even when the data hasn’t changed. To avoid this, we can wrap these functions in the useCallback Hook. This Hook memorizes the function and only rebuilds it if one of the specified dependencies has changed:

const increment = useCallback(() => {
    setCount(count + 1)
}, [count]);

const decrement = useCallback(() => {
    setCount(count - 1)
}, [count]);

useMemo is similar to useCallback except it allows you to apply memoization to any type of value. This optimization is especially useful with complex calculations made within components. useMemo accepts a function as its first parameter and second an array of dependencies. It will only recalculate the memorized value when one of the dependencies has changed, which avoids recalculations on every render. If no array is provided, a new value will be computed on every render:

const memoizedValue = useMemo(() => computeExpensiveValue(), [dependencies]);

On a final note, there is a set of rules that needs to be followed when working with React Hooks. It might be helpful to know the React team has provided us with a linter plugin to enforce these rules automatically.

That’s it. I hope you enjoyed the read.

Daniëlle Repko

Daniëlle Repko

Front-end Developer