Move your React component lifecycle methods to React effect hooks

Move your React component lifecycle methods to React effect hooks

ยท

7 min read

So, you used to use recompose / class components in your React app to add lifecycle methods to your stateless functional components, before the Hooks API was launched.

The time has come to upgrade to React 16 and make use of the new way of managing state within your function components (note that they're now no longer stateless).

This guide will walk through implementing side effects when your component mounts (componentDidMount), un-mounts (componentWillUnmount) and re-renders (componentDidUpdate). We will also touch on the React 16 approach to shouldComponentUpdate.

Running side effects when your component mounts/updates/unmounts

If you need to run the same code in your componentDidMount and componentDidUpdate and componentWillUnmount lifecycle methods then you can use useEffect like below.

Tip: When working with effects, try to forget about the concept of mounting/unmounting and instead replace this with rendering.

The useEffect hook tells React that your component needs to do something after a render. You can pass a single argument to useEffect this is what we refer to as your effect - essentially the function to run when the render happens. In the example below we pass callback function down called onUpdate and use this to pass our counter value higher up on every render.

 const Counter = ({ onUpdate }) => {
    const [counter, setCounter] = useState(0);

    useEffect(() => {
        onUpdate(counter);
    });

    const incrementCounter = setCounter(counter + 1);

    return (
        <div>
            <button onClick={incrementCounter}>Increment Counter</button>
            {counter}
        </div>
    )
}

Conditional effects - componentDidUpdate

Sometimes you only want your side effects to run when certain props change, maybe to prevent infinite re-renders. In a standard lifecycle method in a recompose or class based approach you would include a conditional if statement within your componentDidUpdate function.

compose(
    lifecycle({
        componentDidUpdate(prevProps) {
            const { count, update } = this.props;
            if (prevProps.count !== count) {
                update(count)
            }
        }
    })
)(Counter)

Using hooks, you can utilise the useEffect hook we made above and make use of the second argument that you can pass. The second argument is essentially a whitelist of values that should be compared on each render and if the value is found to be different only then should your effect run.

In the below example you can see that we pass an array with our counter value in. We are telling React to only call our effect on a render if the value of count has changed.

 const Counter = ({ onUpdate }) => {
    const [counter, setCounter] = useState(0);

    useEffect(() => {
        onUpdate(counter);
    }, [counter]);

    const incrementCounter = setCounter(counter + 1);

    return (
        <div>
            <button onClick={incrementCounter}>Increment Counter</button>
            {counter}
        </div>
    )
}

componentDidMount / componentWillUpdate

It's common to have to perform an action that will need cleaning up when your component is unmounted. One example of such a scenario could be setting and clearing an interval/timer.

If you were implementing a counter than incremented every second, you could have set it up like in the example below. using componentDidMount and componentDidUpdate.

compose(
    withState("counter", "setCounter", 0),
    withState("intervalRef", "setIntervalRef"),
    withHandlers({
        incrementCounter: ({ counter, setCounter }) => () => {
            setCounter(counter + 1);
        }
    }),
    lifecycle({
        componentDidMount() {
            const { setIntervalRef, incrementCounter } = this.props;
            const interval = setInterval(incrementCounter, 1000)
            setIntervalRef(interval)
        },
        componentWillUnmount() {
            clearInterval(this.props.intervalRef);
        }
    })
)(Counter)

You could achieve the same result with hooks by utilising our useEffect with a couple of modifications.

  • First of all we need to pass [] as the second argument. By not whitelisting props, the effect will only be called on mount. This is the syntax for componentDidMount.
  • Secondly we need to return a function within our effect. Returning a function is the syntax for the equivalent to a componentWillUnmount.
  • Finally we need to make use of of the functional update form of setState. You'll notice that we aren't referencing counter directly inside our effect. If we did, when our component mounts, the initial value of counter would get locked inside our effect. This would mean that every time the function we passed to our setInterval call runs it would always evaluate to setCounter(0 + 1) as counter would always be frozen at 0. Instead of passing in a new value to our state setter setCounter you can also pass a function that gets called with the current state value and returns the new value.
 const Counter = ({ onUpdate }) => {
    const [counter, setCounter] = useState(0);

    useEffect(() => {
        const interval = setInterval(setCounter(count => count + 1), 1000);

            // The function you return acts like your componentWillUnmount
            return () => clearInterval(interval);
    }, []);

    return (
        <div>
            {counter}
        </div>
    )
}

Should component update

In recompose the shouldUpdate hoc would allow you to specify a custom function to determine whether your component should update or not.

compose(
    shouldUpdate((props, nextProps) => props.id !== nextProps.id)
)(Person)

Instead of using shouldUpdate you can wrap your component in React.memo().

Note: React.memo() is not a hook.

By default your component will only update if props change. You can also pass a second argument to React.memo() which is a custom equality check to determine if your component should update.

Watch out: We're checking for quality here rather than prop difference so we use ===.

const Person = React.memo(
    ({ firstName, surname }) => {

        return (
            <div>
                <span>{firstName}</span> <span>{surname}</span>
            </div>
        )
    },
    (prevProps, nextProps) => prevProps.id === nextProps.id
)