Search code examples
reactjsreact-hooksuse-effect

Can different cleanups be done conditionally in useEffect?


So, I have an useEffect like this:

useEffect(()=>{
    if(foo) {
        // do something
        return () => { // cleanup function }
    }
}, [foo])

Here, the cleanup function is never called, even if if block is executed. But if I modify the effect to be:

useEffect(()=>{
    if(foo) {
        // do something
    }
    return () => { // cleanup function }
}, [foo])

it works. So, is the cleanup done only if return is the last statement of useEffect or is there something I am missing?


Solution

  • The cleanup function is recreated every time the useEffect updater function is called. The current cleanup function would be called when the dependency changes (or on each render if no dependency), or before the component is unmounted. After the cleanup is called, the updater would be called, and a new cleanup function can be returned.

    You can return a different function, or none at all, whenever the updater function is called.

    For example, click the Inc button multiple times, and you can see in the console that the cleanup function exists only for even counter numbers, because it's returned conditionally.

    const { useState, useEffect } = React;
    
    const Demo = () => {
      const [counter, setCount] = useState(0);
      
      useEffect(() => {
        console.log(`Effect ${counter}`);
        
        if(counter % 2 === 0) {
          return () => console.log(`Cleanup ${counter}`);
        }
      }, [counter]);
      
      return (
        <div>
          <p>Counter: {counter}</p>
          
          <button onClick={() => setCount(counter + 1)}>Inc</button>
        </div>
      );
    };
    
    ReactDOM.render(
      <Demo />,
      root
    );
    .as-console-wrapper { top: 0; left: 50% !important; max-height: unset !important; }
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    Sometimes, nesting the cleanup function inside a condition might be confusing. Another option is to always return a cleanup function, and put the logic inside it:

    useEffect(()=>{
        if(foo) {
            // do something
        }
        
        return () => { 
          if(foo) {
            // cleanup something
          }
        }
    }, [foo])
    

    And you can see in this example, the result is the same:

    const { useState, useEffect } = React;
    
    const Demo = () => {
      const [counter, setCount] = useState(0);
      
      useEffect(() => {
        console.log(`Effect ${counter}`);
    
        return () => {
          if(counter % 2 === 0) {
            console.log(`Cleanup ${counter}`);
          }    
        };
      }, [counter]);
      
      return (
        <div>
          <p>Counter: {counter}</p>
          
          <button onClick={() => setCount(counter + 1)}>Inc</button>
        </div>
      );
    };
    
    ReactDOM.render(
      <Demo />,
      root
    );
    .as-console-wrapper { top: 0; left: 50% !important; max-height: unset !important; }
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    
    <div id="root"></div>