skip to Main Content

I have the following useEffects in a React functional component below. I want to be sure that when showComponentProp is updated, that the first useEffect updates the state before the second useEffect is run.

I understand that useEffect hooks are run in the order they appear in the component body, however, as these contain setState functions, I’m not 100% sure of the behaviour. I don’t want there to be a race condition between the two setState functions in the useEffect hooks.

Could someone please clarify the exact order of the useEffects being called and when the component is rerendered in this case. For instance, is the first effect called, and when the isVisible state is updated, does the component rerender before the second effect is called? In that case will both effects be run again after the rerender, or just the second effect as the first effect was called previously.

Any references would be appreciated as I could not find a resource that explained this.

const [isVisible, setIsVisible] = useState(showComponentProp);

useEffect(() => {
    setIsVisible(showComponentProp);
}, [showComponentProp]);

useEffect(() => {
    if (showComponentProp && timerProp) {
      const timeout = setTimeout(() => setIsVisible(false), timerProp);
      return () => clearTimeout(timeout);
    }
}, [showComponentProp, timerProp]);

2

Answers


  1. I want to be sure that when showComponentProp is updated, that the
    first useEffect updates the state before the second useEffect is
    run.

    The first useEffect hook’s callback will certainly enqueue the isVisible state update before the second useEffect hook is called.

    I understand that useEffect hooks are run in the order they appear
    in the component body, however, as these contain setState functions,
    I’m not 100% sure of the behaviour. I don’t want there to be a race
    condition between the two setState functions in the useEffect hooks.

    All React hooks are called each render cycle, in the order they are defined. If showComponentProp changes, then because it is included in the dependency array of both useEffect hooks it will trigger the both useEffect hooks’ callbacks, in the order they are defined.

    Could someone please clarify the exact order of the useEffects being
    called and when the component is rerendered in this case. For
    instance, is the first effect called, and when the isVisible state
    is updated, does the component rerender before the second effect is
    called? In that case will both effects be run again after the
    rerender, or just the second effect as the first effect was called
    previously.

    1. The component will render and call all React hooks, in order.
    2. At the end of the render cycle the enqueued useEffect hook callbacks will be called, in order.
      1. Effect #1 callback will enqueue an isVisible state update to update to the current showComponentProp value.
      2. Effect #2 callback will check the current showComponentProp and timerProp values, and if both are truthy enter the if-block and instantiate the timeout to enqueue another isVisible state update to false, and return a useEffect hook cleanup function.
    3. The enqueued state updates will be processed and trigger a component rerender:
      1. Back to step 1 above to repeat the process
      2. The running timeout then either:
        • expires after timerProp ms and calls the callback to enqueue another isVisible state update to false, and trigger another component rerender, e.g. back to step 1 above.
        • cancelled by the cleanup function before expiration

    In any case, both useEffect hooks will be called each and every render cycle, and the callbacks called only when the dependencies change.

    However, I suspect you could accomplish this all in a single useEffect hook, which may make understanding the logic and flow a bit easier.

    const [isVisible, setIsVisible] = useState(showComponentProp);
    
    // Run effect when either showComponentProp or timerProp update
    useEffect(() => {
      // Only instantiate timeout when we've both truthy
      // showComponentProp and timerProp values
      if (showComponentProp && timerProp) {
        // Enqueue isVisible state update "now"
        setIsVisible(true);
    
        // Enqueue timeout to enqueue isVisible state update "later"
        const timeout = setTimeout(() => setIsVisible(false), timerProp);
    
        return () => clearTimeout(timeout);
      }
    }, [showComponentProp, timerProp]);
    
    Login or Signup to reply.
  2. For instance, is the first effect called, and when the isVisible state
    is updated ?

    No, the reason is that the dependency array dictates the scope of the code inside the hook. At present neither hook has set setIsVisible as its dependency.

    More clearly, at present the first hook will run only on the following events:

    1. When the component mounts
    2. On every change of the prop showComponentProp
    3. When the component will unmount

    Similarly the second hook will run only on the following events:

    1. When the component mounts
    2. On every change on the props showComponentProp or timerProp
    3. When the component will unmount

    Does the component rerender before the second effect is called?

    No, each render will be followed by exactly only one useEffect event, however, there may be any number of useEffect handlers. All will execute in the order of the code. Each state setter call across all useEffect handlers will be queued up, and will process only on the next single render.

    In that case will both effects be run again after the rerender,

    yes, It is so, both effects will run after every single render,
    however the scope is determined by the dependency array, as mentioned earlier.

    or just the second effect as the first effect was called previously.

    No, this is not the case, which you may understand by now.

    Please see below, a sample code is given along with its test run results.

    App.js

    import { useState, useEffect } from 'react';
    
    export default function App() {
      const [showComponentProp, setShowComponentProp] = useState(true);
      const [timerProp, setTimerProp] = useState(1000);
    
      return (
        <>
          <Component showComponentProp={showComponentProp} timerProp={timerProp} />
          <br />
          <button onClick={() => setShowComponentProp(!showComponentProp)}>
            Toggle Show componentProp
          </button>
          <br />
          <button onClick={() => setTimerProp(timerProp + 10)}>
            Increase timerProp by 10 milliseconds
          </button>
        </>
      );
    }
    
    function Component({ showComponentProp, timerProp }) {
      const [isVisible, setIsVisible] = useState(showComponentProp);
    
      console.log(
        `rendered with showComponentProp : ${showComponentProp} isVisible : ${isVisible}`
      );
    
      useEffect(() => {
        console.log('first useEffect');
        setIsVisible(showComponentProp);
      }, [showComponentProp]);
    
      useEffect(() => {
        console.log('second useEffect');
        if (showComponentProp && timerProp) {
          const timeout = setTimeout(() => setIsVisible(false), timerProp);
          return () => clearTimeout(timeout);
        }
      }, [showComponentProp, timerProp]);
    
      return (
        <>
          I am the component
          <br />
          <button onClick={() => setIsVisible(!isVisible)}>
            Toggle visibility
          </button>
        </>
      );
    }
    

    Test runs

    Application instance:

    enter image description here

    Test 1 : When the App has been loaded initially, the following log entries have been created.

    Please note the additional logs created on StrictMode
    have been removed for brevity.

    // rendered with showComponentProp : true isVisible : true
    // first useEffect
    // second useEffect
    // rendered with showComponentProp : true isVisible : false
    

    Notes :

    1. The component rendered.
    2. Upon render, the two useEffects have been run in the given order.
    3. The state setter call in the first useEffect did not have any effect since the value to update and the present value of the state isVisible are the same – it is true. It means there is no change in value.
    4. Therefore the next state setter in the second
      useEffect only has taken the effect. Since its
      value is false. Therefore the latest render has got the same false value as shown in the log.

    The summary is that there are two renders, two useEffect calls and only one state setter call on loading the app.

    Test 2 : Now clicking on the Toggle Visibility button

    Logs generated:

    // rendered with showComponentProp : true isVisible : true
    

    Notes :

    The component rendered since there is a state change within the component. However, there is no useEffect invoked this time. The logs
    do not show anything from it. Since the dependency does not include the state isVisible, no useEffect fired. This is the point with respect to your first question.

    The summary is that there is only one render, no useEffects calls and no state setter calls in this test.

    Test 3 : Now clicking on Toggle Show componentPro

    Logs generated:

    // rendered with showComponentProp : false isVisible : true
    // first useEffect
    // second useEffect
    // rendered with showComponentProp : false isVisible : false
    

    Notes:

    1. The component rendered since there is a change in the parent component.

    2. As a result, upon rendering, the two useEffects have been invoked since its dependency include showComponentProp, and there is a change in
      that prop.

    3. The state setter call in the first useEffect have an effect since the value to update and the present value of the state isVisible are different. It means there is a change in the state.

    4. However, the next state setter in the second
      useEffect has no effect. Since its
      value is false and the state isVisible is also false. So there is no change in value.

    Your second and third questions should have been answered here.

    The summary is that there are two renders, two useEffects invoked and there is only one state setter call.

    Test 4 – Clicking on button Increase TimerProp by 10 milliseconds

    The Logs:

    // rendered with showComponentProp : false isVisible : false
    // second useEffect
    

    Notes:

    1. The component rendered since there is a change in the parent component.

    2. As a result, upon rendering, the first useEffect did not fire since its dependency does not match.

    3. The second useEffect have been invoked since its dependency includes timerProp, and there is a change in that prop. However there is no state update in isVisible since there is no change in the value, its has false already, and the state setter in the second useEffect is also trying to update it with the same false value.

    The summary is that there is one render, one useEffect invoked and there is no state setter call.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search