BROG

Replace componentDidMount with useEffect<!-- --> | <!-- -->BROG

Replace componentDidMount with useEffect

Posted on Mar 21, 2019

With the introduction of hooks into React, you may find yourself wanting to convert existing class components into function components to make use of hooks. If you have existing redux thunk actions or API calls in componentDidMount you may wonder how you can convert them to a hook-friendly implementation that does not use React’s component class lifecycle methods.

Implementation with a Class Component

Let’s look at how we’d have written a class component that makes use of redux thunk and dispatch asynchronous actions:

import React from 'react';
import { connect } from 'react-redux';

// Imagine this is an asynchronous redux thunk action.
import { fetchTweetsForUser } from './actions';

class TwitterExample extends React.Component {
  componentDidMount() {
    this.props.onFetchTweetsForUser(this.props.userID);
  }

  render() {
    return (
      <div>
        {tweets.map(tweet => (
          <div>{tweet.text}</div>
        ))}
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    userID: state.userID,
    tweets: state.tweets,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onFetchTweetsForUser: userID => dispatch(fetchTweetsForUser(userID)),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(TwitterExample);

This was a fairly common pattern. In React’s documentation the recommended method for doing API calls was to do them in componentDidMount. If your reducer action was asynchronous, you’d likely do it in componentDidMount, and it would be called only once, on the initial render of the component. You could fetch all your initially needed data in componentDidMount.

So, how can this be achieved using the useEffect hook? useEffect’s documentation includes this note:

If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.

useEffect contains the functionality of componentDidMount, which is the logic we’re trying to replace, so we’re looking in the right place.

useEffect can be considered a superset of componentDidMount. As noted in the excerpt from the React documentation above, it can be thought of as all three of the lifecycle methods: componentDidMount, componentDidUpdate, and componentWillUnmount. To understand how it can give us our old componentDidMount behaviour, we need to know a little more about the useEffect hook.

useEffect takes two parameters:

  1. The first parameters is a function, the “effect” code you want to run. This parameter is required.
  2. The second parameter is an array, the values the first parameter depends on. This parameter is optional.

If you specify no second parameter, the first parameter “effect” will run on every component render. If you specify the optional second parameter, the hook will only rerun your effect if at least one of the dependencies has changed between renders. It’s important to note that if you specify any dependencies as part of the second parameter, you almost certainly need to specify all of the values your “effect” makes use of. For more information about why, see the official React documentation. The React team has also created an eslint plugin to help follow the rules of hooks: eslint-plugin-react-hooks.

Implementation with a Function Component

Now that we know a little more about the useEffect hooks, lets translate our example to a function component making use of it:

import React, { useEffect } from 'react';
import { connect } from 'react-redux';

function TwitterExample(props) {
  useEffect(
    // The first argument to useEffect is the code to run.
    () => {
      this.props.onFetchTweetsForUser(this.props.userID);
    },
    // The second argument is the list of values the first
    // argument depends on. In this case, our effect won't
    // rerun unless onFetchTweetsForUser or userID changes.
    [this.props.onFetchTweetsForUser, this.props.userID]
  );

  return (
    <div>
      {tweets.map(tweet => (
        <div>{tweet.text}</div>
      ))}
    </div>
  );
}

// Has not changed.
function mapStateToProps(state) {
  /* ... */
}

// Has not changed.
function mapDispatchToProps(dispatch) {
  /* ... */
}

// Has not changed.
export default connect(mapStateToProps, mapDispatchToProps)(TwitterExample);

This will work! This is not functionally the same as our original implementation. One big difference is the hooks implementation will fetch tweets again if this.props.userID or this.props.onFetchTweetsForUser changes. One thing that initially worried me is that this.props.onFetchTweetsForUser would change on every render causing our hook to erroneously rerun on every render of the component. Conveniently this is not the case! mapDispatchToProps only runs one time, unless you use the optional second parameter ownProps. If you use ownProps, you’ll have to memoize the functions returned by mapDispatchToProps to ensure they don’t cause unnecessary runs of your “effect”. Additionally, because our “effect” will rerun when this.props.userID changes, our code is now more correct than it was before we had hooks! With a class component we would have needed to implement both componentDidMount and componentDidUpdate to achieve the same effect!

This is a simple example of converting a React class component that does an asynchronous task in componentDidMount to a functional component that instead uses the useEffect hook. We have to do a little more work supplying a dependency array to the “effect”, but in return our code is more correct than it started.

To learn more about the useEffect hook, see the official React documentation on hooks.