useState place in lifecycle in react

358 views Asked by At

I really want to understand the lifecycle of react functional component. In many websites you see these three steps:
1-mounthing 2-render 3-unmounthing.

But what about other code that is written before useeffect() function for example assume this:


const Countdown = () => {
  let x = 0;
  const [countDown, setCountDown] = useState(10)
   
   x = x + 1
   
   if(x > 100) { 
     x = 0
   }

  useEffect(() => {
    const interval = setInterval(() => {
        setCountDown(countDown - 1);
        console.log(x)
      }, 1000);
    }, [countDown]);
};

I want to know :

  1. when countDown state and x variable is declared before useEffect or after it (or inside it)?

  2. when if or for phrases is declared(in this example if phrase), Are they inside useEffect indeed?

what is loading page order? what is start point of executing?

2

There are 2 answers

0
T.J. Crowder On BEST ANSWER

The lifecycle

1-mounthing 2-render 4-unmounthing.

It's more like (docs):

  • Mount
    • Render (React calls your component function)
    • Commit to DOM (React reconciles elements with the DOM)
    • Layout effects (React calls relevant layout effect callbacks scheduled via useLayoutEffect)
    • Effects (React calls relevant non-layout effect callbacks scheduled via useEffect))
  • Update (repeats)
    • Render (React calls your component function)
    • Commit to DOM (React reconciles elements with the DOM)
    • Layout effects (React calls relevant layout effect callbacks scheduled via useLayoutEffect)
    • Effects (React calls relevant non-layout effect callbacks scheduled via useEffect))
    • Cleanup if any (React React calls any effect cleanup callbacks)
  • Unmount
    • Removal (React removes elements from the DOM)
    • Cleanup if any (React React calls any effect cleanup callbacks)

Your Questions

when countDown state and x variable is declared before useEffect or after it (or inside it)?

Before. The React library can't change how JavaScript code execution occurs. The useState call and associated declarations are before the useEffect call, so they happen before it.

when if or for phrases is declared(in this example if phrase), Are they inside useEffect indeed?

No, only the code within the useEffect callback is called as an effect.

How your code runs

The cycle here is:

  1. First render
    1. React creates behind-the-scenes instance storage for the element.
    2. React calls your component function.
      1. Your code declares a local x variable and sets it to 0.
      2. Your code declares countDown and setCountDown and calls useState, which allocates a state slot in the instance storage; your code stores what useState returns (the initial state value and the setter) in those constants.
      3. Your x = x + 1 statement runs, updating x to 1.
      4. Your if statement runs, but the condition will never be true — x is a local variable, not a state member, so its value will always be 1 at this point.
      5. Your call to useEffect schedules an effect callback for when countDown changes.
      6. At this point, your code should be returning elements from Countdown.
  2. First commit / "mount"
    1. React takes the elements Countdown should return and commits them to the DOM (making the DOM show what they describe).
  3. React calls your useEffect callback (useEffect callbacks are always called just after mount)
    1. Your callback creates an interval timer that, when run, will call setCountDown.
    2. Your callback logs x, which will be 1.
  4. The timer calls setCountDown, changing the value.
  5. Second render
    1. React calls your function to re-render.
      1. Your code declares a new local x variable and sets it to 0.
      2. Your code declares countDown and setCountDown and calls useState, which retrieves the updated state from the instance storage; your code stores what useState returns (the current state value and the setter) in those constants.
      3. Your x = x + 1 statement runs, updating x to 1.
      4. Your if statement runs, but the condition will never be true.
      5. Your call to useEffect schedules an effect callback for when countDown changes.
      6. At this point, your code should be returning elements from Countdown.
  6. Because countDown changed, React calls your useEffect callback
    1. Your callback creates a new interval timer that, when run, will call setCountDown.
    2. Your callback logs x, which will be 1.
  7. And so on until/unless the component is umounted by its parent.

Issues with the code

There are a couple of bugs in the code you've shown

  1. You never cancel the interval timer, but you're creating a new one every time countDown changes. This will quickly lead to hundreds and then thousands of timers all triggering update calls. You should:
    1. At minimum, remember the timer handle and cancel the timer in an effect cleanup.
    2. Consider not using countDown as a dependency, so the effect only runs on mount. Then use the callback form of setCountDown.
  2. (As mentioned) Your component never returns any elements
  3. Your code seemed to expect the value of x to be maintained between calls to the function, but it's a local variable, so it's re-created each time.
  4. Nothing special happens when countDown reaches 0, so it will just keep going to -1, -2, etc.

Updated version

Here's an updated version with some notes. I was going to remove x because it wasn't really used for anything, but then thought it might be better to leave it with comments. And I didn't do anything about #4 above, because I wasn't sure what you wanted to do.

const Countdown = () => {
    let x = 0;
    const [countDown, setCountDown] = useState(10);

    x = x + 1;
    if (x > 100) {  // `x` will always be `1` here, remember that
        x = 0;      // `x` is a *local variable*
    }

    useEffect(() => {
        const interval = setInterval(() => {
            // Use the callback form of the setter so you can update the
            // up-to-date value
            setCountDown((c) => c - 1);
            // Will always show 1
            console.log(x);
        }, 1000);
        // Return a cleanup callback that removes the interval timer
        return () => {
            clearInterval(interval);
        };
    }, []);
    // ^^ don't use `countDown` as a dependency (in this particular case),
    // since we don't use it (anymore, now we use the callback setter)

    // Return some elements
    return <div>{countDown}</div>;
};

const { useState, useEffect } = React;

const Countdown = () => {
    let x = 0;
    const [countDown, setCountDown] = useState(10);

    x = x + 1;
    if (x > 100) {  // `x` will always be `1` here, remember that
        x = 0;      // `x` is a *local variable*
    }

    useEffect(() => {
        const interval = setInterval(() => {
            // Use the callback form of the setter so you can update the
            // up-to-date value
            setCountDown((c) => c - 1);
            // Will always show 1
            console.log(x);
        }, 1000);
        // Return a cleanup callback that removes the interval timer
        return () => {
            clearInterval(interval);
        };
    }, []);
    // ^^ don't use `countDown` as a dependency (in this particular case),
    // since we don't use it (anymore, now we use the callback setter)

    // Return some elements
    return <div>{countDown}</div>;
};

const Example = () => {
    return <Countdown />;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

If we wanted the countdown to stop when it reached 0 and turn off the timer, there are a couple of ways we might do that, see comments in the two live examples showing different ways:

const { useState, useEffect } = React;

const Countdown = () => {
    const [countDown, setCountDown] = useState(10);

    useEffect(() => {
        const interval = setInterval(() => {
            // We could cancel the interval from within the setter
            // callback. It's a bit dodgy, though, to have side-
            // effects in setter callbacks.
            setCountDown((c) => {
                const updated = c - 1;
                if (updated === 0) {
                    clearInterval(interval);
                }
                return updated;
            });
        }, 1000);
        return () => {
            clearInterval(interval);
        };
    }, []);

    return <div>{countDown === 0 ? "Done" : countDown}</div>;
};

const Example = () => {
    return <Countdown />;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

const { useState, useEffect, useRef } = React;

const Countdown = () => {
    // We could store the timer handle in a ref (which is maintained
    // across renders) and use a second `useEffect` to cancel it when
    // `countDown` reaches zero.
    const intervalRef = useRef(0);
    const [countDown, setCountDown] = useState(10);

    useEffect(() => {
        intervalRef.current = setInterval(() => {
            setCountDown((c) => c - 1);
        }, 1000);
        return () => {
            // (It's okay if this tries to clear an interval that
            // isn't running anymore.)
            clearInterval(intervalRef.current);
        };
    }, []);

    useEffect(() => {
        if (countDown === 0) {
            clearInterval(intervalRef.current);
        }
    }, [countDown]);

    return <div>{countDown === 0 ? "Done" : countDown}</div>;
};

const Example = () => {
    return <Countdown />;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

0
Avi On

useEffec's place in the lifecycle

It doesn't matter where you put a useEffect in your component code, all effects will always run after every render. The only time the useEffect place in the component's code might be important is if you have multiple useEffects, as they will execute by the order you've written them.

x variable/state

As for your x variable, this is not something that would work in react because that variable will be redeclared on every render. The way you keep track of state is by using the useState. I would argue you could track both values from the same state but if you want to have ab additional explicit state, you need to use another useState hook. The general convention is that state hooks are declared at the top of the component (but they don't have to).

if statements

One of the rules of react hooks is that all hooks must run on every render. You can still use id statements in your components and custom hooks but they either have to run after all the hooks in your component, or you can declare then within your hook - that way the hook executes on every render but the code block inside it will run only when the if statement's condition was true.

useEffect(() => {
  if (condition) {
    code to run...
  }
}, [countDown]);

additional notes

Another important thing to remember about useEffects is that sometimes they need to be cleaned up with a return statement. In your case you are declaring an interval, without a returning a clearInterval your useEffect will create an additional interval on every render which will cause your counter to get out of sync