How React hooks saved my life

Okay, the title is a bit too dramatic. My life would probably have been absolutely fine if React hooks did not come into existence, but if you are reading this then the click bait wording may have got you ... hooked in.

February 2019 saw the release of React v16.8.0 which introduced Hooks to the world. That is all you need to know if you are not a React developer. But if you are, then read on.

I don't care about shiny new things, what was the problem in the first place?

React components can be written as a functional or a class component.

A functional component, also known as a stateless component, would look something like this in ES6:

const Hello = (props) => <div>Hello {props.name}</div>;

This is the same component but rewritten as a class:

class Hello extends React.Component {
    render = () => <div>Hello {this.props.name}</div>
}

Even though the behaviour of Hello is very simple we can see that the functional component is more concise than the class component.

So you are probably thinking, what is the point in having class components?

Well classes provide some additional features such as local state manipulation and lifecycle methods.

Let's say that our Hello component has a Germanize button which switches the language from English to German:

class Hello extends React.Component {

    constructor(props) {
        super(props);
        this.state = {};
    }
    
    render = () =>
        <div>{this.state.inGerman ? 'Hallo' : 'Hello'} {this.props.name}
            <button onClick={() =>this.setState({inGerman: true})}>Germanise!</button>
    </div>
}

This would not be possible to do with a functional component so if a developer wanted to work with component state they would need to create a clas...hold the phone, the hooks have arrived.

Time to get hooked in

With hooks, we can do state updates in a functional component. The hook that is used for updating state is useState and here is an example of how it can be used with our Hello functional component from earlier:

const Hello = (props) => {
    const [inGerman, setInGerman ] = useState(false);
    return (<div>{inGerman ? 'Hallo' : 'Hello'} {props.name}
        <button onClick={() => setInGerman(true)}>Germanise!</button>
    </div>);
};

The argument passed to the useState function is the initial value. In this case, the initial value of inGerman is false. useState returns a pair of values: the state value inGerman and a value setter setInGerman.

With useState we don't need to create a class component with a constructor to initialise state; we can just initialise it in our render method which in turn keeps our code more concise.

It is worth bearing in mind that, unlike this.setState() which merges old state with new state values, setting state using the hook will discard the previous state and put the new values in place. Look at the code example below:

const Hello = () => {
    const[helloState, setHelloState ] = useState({
        inGerman: false,
        currentUser: 'Spock'
    });

    return (<div>{helloState.inGerman ? 'Hallo' : 'Hello'} {helloState.currentUser}
        <button onClick={() => {
            setHelloState({inGerman: true});
        }}>Germanise!</button>
    </div>);
};

After the Germanize! button has been clicked what do you think will be displayed? a) Hallo Spock or b) Hallo

If you answered b, then congratulations, you were paying attention. The setHelloState method will replace the current state object { inGerman: false, currentUser: 'Spock'} with {inGerman: true} meaning the currentUser property is no longer present in the component state. Therefore only Hallo is displayed after the Germanize! button is clicked since helloState.currentUser won't return a value.

To resolve this issue we can change the method call to:

setHelloState({ ...helloState, inGerman: true});

However this tightly couples the currentUser and inGerman state logic together which doesn't make sense to do because they won't typically change at the same time i.e. setting the language to German should not alter the current user and vice versa.

Therefore a cleaner solution would be to use multiple state variables like below:

const Hello = () => {
 const [inGerman, setInGerman] = useState(false);
 const [currentUser, setCurrentUser] = useState('Spock');

 return (<div>{inGerman ? 'Hallo' : 'Hello'} {currentUser}
 <button onClick={() => {
 setInGerman(true);
 }}>Germanise!</button>
 </div>);
};

The fun does not stop with state hooks though...

Effect hooks

Lifecycle methods such as componentDidMount and componentDidUpdate can be replaced with the hook, useEffect().

Our Hello component now uses a UserService to get the name of the current user. The service might be getting the current user from an API, local storage or anywhere your imagination takes you to. We could retrieve the current user as soon as the component mounts. We could write this functionality using a class component like below:

export default class Hello extends React.Component {

    constructor(props) {
        super(props);
        this.state = {};
    }

    componentDidMount() {
        const currentUser = UserService.getCurrentUser();
        this.setState({currentUser})
    }

    render = () =>
        <div>{this.state.inGerman ? 'Hallo' : 'Hello'} {this.state.currentUser}
            <button onClick={() =>this.setState({inGerman: true})}>Germanise!</button>
    </div>
}

Code inside the componentDidMount body will execute when the component mounts so the current user is set then.

This behaviour can be replaced by using an useEffect instead like below:

import React, {useEffect, useState} from "react";
import {UserService} from "./UserService";

export const Hello = () => {
    const [inGerman, setInGerman] = useState(false);
    const [currentUser, setCurrentUser] = useState('');

    useEffect(() => {
        const currentUser = UserService.getCurrentUser();
        setCurrentUser(currentUser);
    });

    return (<div>{inGerman ? 'Hallo' : 'Hello'} {currentUser}
        <button onClick={() => {
            setInGerman(true);
        }}>Germanise!</button>
    </div>);
};

The callback argument in useEffect is called, by default, after a component renders. This means currentUser is set when the component renders the first time and each time the component updates.

At the moment useEffect is mimicking behaviour for componentDidMount and componentDidUpdate. However, now the current user is set everytime the component re-renders. This is not ideal especially if we are fetching a huge amount of data. If we only wanted useEffect to replicate componentDidMount i.e. the useEffect callback is only invoked when the component mounts and not when it updates, then we can pass [] as a secondary parameter:

useEffect(() => {
 const currentUser = UserService.getCurrentUser();
 setCurrentUser(currentUser);
}, []);

Passing in an empty array is essentially telling useEffect to not watch any variables, meaning it will only call when the component mounts.

You can see that useEffect simplifies the traditional component lifecycle functionality since you do not need to think about the different component lifecycle events. Instead, simply think of these events as rendering side-effects, hence the name useEffect.

Reuse state logic

So far we have seen that using hooks cuts out a lot of cruft which is introduced by classes and lifecycle methods. Another benefit is that they allow state logic to be shared between components.

Let's look at our Hello component as it stands at the moment

export const Hello = () => {
    const [inGerman, setInGerman] = useState(false);
    const [currentUser, setCurrentUser] = useState('');

    useEffect(() => {
        const currentUser = UserService.getCurrentUser();
        setCurrentUser(currentUser);
    }, []);

    return (<div>{inGerman ? 'Hallo' : 'Hello'} {currentUser}
        <button onClick={() => {
            setInGerman(true);
        }}>Germanise!</button>
    </div>);
};

We can see the stateful logic is as follows:

1) Get the current user from the User Service when the component mounts.

2) Set the current user value to the state.

There maybe other components in our app which also need to load the current user and set it to the state. Rather than duplicating code, we can reuse this logic by extracting it into a custom hook called something imaginative like useCurrentUser:

export const useCurrentUser = () => {

    const [currentUser, setCurrentUser] = useState('');
    useEffect(() => {
        const currentUser = UserService.getCurrentUser();
        setCurrentUser(currentUser);
    }, []);

    return currentUser;
};

Now that the custom hook useCurrentUser encapsulates the process of getting and setting the current user, our Hello component has been simplified to:

export const Hello = () => {
    const [inGerman, setInGerman] = useState(false);
    const currentUser = useCurrentUser();

    return (<div>{inGerman ? 'Hallo' : 'Hello'} {currentUser}
        <button onClick={() => {
            setInGerman(true);
        }}>Germanise!</button>
    </div>);
};

Additionally, any other component which needs to retrieve the current user simply has to use the useCurrentUser hook.

Higher order components?

Before hooks, we could use higher order components to share component logic.

However, using higher order components can be difficult to set up, increases complexity of the render tree and can be especially difficult to use with typescript since we have to careful with prop types.

Here is a Menu component which uses the withRouter higher order component to get access to the history prop type:

import * as React from "react";
import {MenuList} from "@material-ui/core";
import MenuItem from "@material-ui/core/MenuItem";
import {RouteComponentProps, withRouter} from "react-router";

const Menu = (props: RouteComponentProps) => {
    return (<MenuList>
        <MenuItem onClick={() => props.history.push('/shop')}>Shop</MenuItem>
        <MenuItem onClick={() => props.history.push('/orders')}>Orders</MenuItem>
        <MenuItem onClick={() => props.history.push('/myProfile')}>My profile</MenuItem>
    </MenuList>)
};

export default withRouter(Menu);

The behaviour of withRouter higher order function can be rewritten as a hook :

import * as React from "react";
import {MenuList} from "@material-ui/core";
import MenuItem from "@material-ui/core/MenuItem";
import useRouter from "use-react-router";

const Menu = () => {

    const {history} = useRouter();
    return (<MenuList>
        <MenuItem onClick={() => history.push('/shop')}>Shop</MenuItem>
        <MenuItem onClick={() => history.push('/orders')}>Orders</MenuItem>
        <MenuItem onClick={() => history.push('/myProfile')}>My profile</MenuItem>
    </MenuList>)

};

export default Menu;

I won't go into detail how useRouter() is implemented but if you are interested, then this blog is worth reading.

With useRouter we don't need to wrap our component with another component thus removing that complexity. We also don't need to pass in a RouteComponentProps object.

Summing up

We can see that using React hooks can help us write more concise code meaning that we will end with components that are arguably easier to understand and maintain. Hooks also give us the ability to share state logic between components without going through the arduous set up of higher order components and injecting props into child components. Furthermore we can level up our separation of concerns since our state logic can be encapsulated inside a hook, rather than interweaving with the component rendering logic.

It is worth noting that you can still use class components and you don't need to re-write/ hook-anise your code so it is compatible with the current release of React. However, now that you have been exposed to the benefits, you may just be tempted to replace everything with hooks...

This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

X