Prevent useEffect’s callback firing during initial render

React’s useEffect hook has indeed made life a lot easier. But there are times when you would most certainly miss the fine-grained control class-based components give you over a component’s lifecycle. One such time is when you want to perform a side effect when a component’s state changes.   

Of course, you can pass the state variable as a dependency into the useEffect hook but the problem is the callback function gets called during the initial render as well.   

Take this little example. Let’s say I have an app that has a button. Once the button is clicked, I need to display a text that says “The button has been pressed!”. A simple way of doing this is to have a Boolean state value that will be set to true once the button is clicked. The text can be displayed only if the Boolean value is true.  

useEffect hook’s callback gets called during the initial render

However, for the sake of using the useEffect hook, let’s have two state values. Let me name the first state “press” and the second state “pressed”. Now, once the button is clicked, the press state is set to true. Then let’s call the useEffect hook and pass a callback function that would set pressed to true when press changes. The text should be shown only if pressed is set to true.  

export default function App() {
  const [press, setPress] = useState(false);
  const [pressed, setPressed] = useState(false);

  useEffect(() => {
    setPressed(true);
  }, [press]);
  
  return (
    <div className="App">
      {pressed && <div>The button has been pressed!</div>}
      <button
        onClick={() => {
          setPress(true);
        }}
      >
        Click!
      </button>
    </div>
  );

But on running the app, you will realize that the text is shown even before the button is clicked. The reason? Well, useEffect’s callback function gets called not only when one of the dependencies changes but also during the initial render.  

To prevent this from happening, we need a variable that can be set to false after the initial render. We can then use this variable to prevent the side effect from taking place during the initial render.  

export default function App() {
  const [press, setPress] = useState(false);
  const [pressed, setPressed] = useState(false);

  let initialRender=true;
  useEffect(() => {
    if(initialRender){
      initialRender=false;
    } else{
      setPressed(true);
    }
  }, [press]);
  
  return (
    <div className="App">
      {pressed && <div>The button has been pressed!</div>}
      <button
        onClick={() => {
          setPress(true);
        }}
      >
        Click!
      </button>
    </div>
  );
}

Preventing useEffect callback during the initial render

However, as you may have already guessed, using a variable will not solve our problem. This is because the function gets called every time the component is to be re-rendered and this will reset the variable to its initial value during every re-render.   

To resolve this issue, we can use a state variable. The state values are persisted across renders as we all know. But using the state to detect the initial render will itself result in additional re-renders since a component is re-rendered every time the state is updated. There should be a neater solution.  

Leveraging the useRef hook

Enter useRef. Even though useRef is often seen as a way of obtaining a reference to a DOM element, it can actually be used to store data that should be persisted across re-renders. In fact, React’s doc itself describes this very ability of useRef.  

However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.  

To get started, we need to call the useRef hook and pass the Boolean value true as an argument. This will be its initial value. Let’s store the returned object in a constant called initialRender. We would be able to access and modify this object’s value by using its current property.  

Then, inside our callback function, we can check if the current property of the useRef object is set to true. If it is true, then that means that the function is being called during the initial render. If it’s the initial render, then we can set the current property to false so that the next time it is called, we will know that it is not the initial render.   

Next, if the current property is false, then that means it is not the initial render and the side effect should be executed.   

export default function App() {
  const [press, setPress] = useState(false);
  const [pressed, setPressed] = useState(false);

  const initialRender = useRef(true);

  useEffect(() => {
    if (initialRender.current) {
      initialRender.current = false;
    } else {
      setPressed(true);
    }
  }, [press]);

  return (
    <div className="App">
      {pressed && <div>The button has been pressed!</div>}
      <button
        onClick={() => {
          setPress(true);
        }}
      >
        Click!
      </button>
    </div>
  );
}

Let’s turn this into a custom a hook

This way we can make sure the callback function is called only when the dependencies change. Now, what if we want to reuse this? Let’s turn this into a custom hook.  

export const useNonInitialEffect = (effect: EffectCallback, deps?: DependencyList) => {
	const initialRender = useRef(true);

	useEffect(() => {
		let effectReturns: void | (() => void | undefined) = () => {};

        if (initialRender.current) {
			initialRender.current = false;
		} else {
			effectReturns = effect();
		}

		if (effectReturns && typeof effectReturns === "function") {
			return effectReturns;
		}
	}, deps);
};

This hook is syntactically similar to the useEffect hook as it accepts a callback function and an array of dependencies as arguments. But unlike the useEffect hook, the callback function doesn’t get called during the initial render. 

You can find the complete code of this custom hook in this GitHub repo.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.