componentDidUpdate() with React Hooks

In this post, we will see how to replicate the same behavior we have in componentDidUpdate() using react hooks.

What is componentDidUpdate()?

componentDidUpdate() is a class component lifecycle method that is invoked immediately after an update occurs. This method is very useful when we want to perform some action on a state change.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  increment = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  };

  doSomething = () => {
    console.log(`You clicked ${count} times`);
  };

  componentDidUpdate(prevProps) {
    if (this.props.count !== prevProps.count) {
      // invoced only when count updates
      this.doSomething();
    }
  }

  render() {
    return (
      <button type="button" onClick={this.increment}>Click</button>
    );
  }
}

export default App;

As you can see the doSomething() method is invoked only when the count prop is updated.

How can we perform an action on state change in function components?

We cannot use lifecycle methods in function components. One way to perform some action in function components after a prop state change is by using the useEffect hook.

What does the useEffect do?

The useEffect hook lets us perform side effects in function components.

By default, useEffect runs after every render.

useEffect(() => {
  // runs after every render
  console.log('render!');
});

To avoid running effects in every render we can pass an array of dependencies as a second argument.

useEffect(() => {
  console.log(`You clicked ${count} times`);
}, [count]); // only re-run the effect if count changes

Does this mean we can replicate the same behavior we have in the componentDidUpdate() by just passing the array of dependencies as a second argument?

There is a problem with this approach. Unlike componentDidUpdate() the useEffect runs not only when the dependency count updates, but it runs as well on the initial render when the component mounts.

There is a way to fix that. We can combine the useEffect with the useRef hook.

import { useState, useEffect, useRef } from 'react';

const App = () => {
  const [count, setCount] = useState(0);
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      console.log(`You clicked ${count} times`);
    } else {
      didMount.current = true;
    }
  }, [count]);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <button type="button" onClick={increment}>
      Click
    </button>
  );
};

export default App;

As you can see, first we check if the component did mount, and then we run the effect when the dependency count updates.

To make it better, we can create a custom hook and reuse it anytime we want.

import { useState, useEffect, useRef } from 'react';

const useDidUpdate = (callback, dependencies) => {
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      callback();
    } else {
      didMount.current = true;
    }
  }, [callback, dependencies]);
};

const App = () => {
  const [count, setCount] = useState(0);

  useDidUpdate(() => {
    console.log(`You clicked ${count} times`);
  }, [count]);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <button type="button" onClick={increment}>
      Click
    </button>
  );
};

export default App;