4

I'd like to clarify my understanding of what's happening here. Any detail to improve my current understanding'd be appreciated.

function Timer() {

    let [time, setTime] = useState(5);

    useEffect(() => {
        let timer = setInterval(() => {
          setTime(time - 1);
        }, 1000)
        return () => clearInterval(timer);
    }, );

    return <div>{time}</div>
}

export default Timer

https://codesandbox.io/s/cranky-chaplygin-g1r0p

  1. time is being initialised to 5.
  2. useEffect is read. Its callback must be made ready to fire later.
  3. The div is rendered.
  4. useEffect's callback is executed. setInterval's callback gets ready to fire. Surely useEffect's return statement doesn't fire here, because if it did it would cancel the timer (and the timer does work).
  5. After, roughly, 1 second, setInterval's callback fires changing the state of time (to 4).
  6. Now that a piece of state has changed, the function is re-executed. time, a new variable, is initialised to the new time state.
  7. A new useEffect is read, it's callback made ready to fire later. (This happens because there is no 2nd argument of useEffect()).
  8. The component function's return statement is executed. This effectively re-renders the div.
  9. At some point, the previous useEffect's return statement executes (which disables the timer in that previous useEffect). I'm not sure when this occurs.
  10. The 'new' useEffect's callback is executed.
tonitone120
  • 1,332
  • 1
  • 1
  • 16
  • 2
    `useEffect` has a dependancy array, if you don't use it, useEffect will get called every state change.. For a timer like this passing `[]` to your dependency will stop the Timer from constantly been created / destroyed. PS: blindly passing `[]` to all useEffects is not always a good idea, make sure any dependency's that should effect your render are also included, – Keith Dec 09 '20 at 21:24
  • Thanks @Keith but this isn't the question I'm asking. I've stripped down the original code and want to make sure my understanding on what's happening in the 'lifecycle's correct. If you have any comments on my understanding of that, that'd be helpful – tonitone120 Dec 09 '20 at 21:27
  • 2
    No.8, What do you mean does a re-render, a re-render is only done if Props or State change. Your getting a re-render, because you change the state, nothing more. – Keith Dec 09 '20 at 21:33
  • I think 8-9 are incorrect, but I'm not sure what answer you expect, the answer to the title is: No, function component does not dismount after state change, it's clarified in the docs when the component dismounted. – Dennis Vash Dec 09 '20 at 21:37
  • @Keith I understood re-render to mean the html/visuals of the website change - in this case the `div`. I then thought it logical to say the `return` statement re-renders the `div`. – tonitone120 Dec 09 '20 at 21:40
  • 1
    And just to clarify, in this example, you **RECREATE an interval on every render**, just be sure you understand why this counter works, although it's a pretty bad example, that's not how you make a counter in React. – Dennis Vash Dec 09 '20 at 21:41
  • @DennisVash An answer that corrects what you think ought to be corrected would be great. If it's the case a function component does not dismount after state change, then I'm confused. I thought the `return` statement of a `useEffect` executes when the component is 'dismounted'. Could you help me understand, in general or in the example in my question, when the `return` statement of `useEffect` executes? – tonitone120 Dec 09 '20 at 21:43
  • @DennisVash Perhaps at the end you could point me towards a better example of a timer? – tonitone120 Dec 09 '20 at 21:49
  • Ill add an answer to not obvious parts, all other things you can look up in stackoverflow and google – Dennis Vash Dec 09 '20 at 21:51
  • 2
    @tonitone120 Using `SetTimeout` instead of `SetInterval` would be better in your example, or if you do use `SetInterval` use the dependency `[]`, Also to make this a bit more full-proof, async stuff like timers etc should do a mounted check, otherwise you could end up calling `setState` on an unmounted compoenent. – Keith Dec 09 '20 at 21:52

3 Answers3

6

Does a functional component 'dismount' after a change in its state and does this result in previous useEffect callback return statements executing?

No, component dismounts only once at end of its life time, React allows to execute a callback with useEffect hook by providing an empty dep array with a return statement:

useEffect(() => {
  return () => {
    console.log("unmounts");
  };
}, []);

When component dismounts?

When its parent stops rendering it. See conditional rendering.


Could you help me understand, in general or in the example in my question, when the return statement of useEffect executes?

Depends on the dep array:

  • if it's empty [], on unmount.
  • if it has dependencies [value1,value2], on dependencies change (shallow comparison).
  • if it has no dependencies (no 2nd argument for useEffect) it runs on every render.

See follow up question useEffect in depth / use of useEffect?

Dennis Vash
  • 31,365
  • 5
  • 46
  • 76
  • Thanks @DennisVash. I've updated my question to show my understanding of what's happening in the code now that I understand `useEffect` a little better. I'm not confident of when my `useEffect`'s return statement executes. You say it runs 'on' every render. Is initial execution/render included in this? Surely the problem with that idea is, `useEffect`'s `return` would immediately cancel the timer before the timer's callback had a chance to execute (and the timer does work). – tonitone120 Dec 10 '20 at 02:08
3

Your understanding of the sequence of events is correct. The only thing missing is the precise timing of the effect callbacks and cleanup.

When the component re-renders, any useEffects will have their dependency arrays analyzed for changes. If there has been a change, then that effect callback will run. These callbacks are guaranteed to run in the order that they're declared in the component. For example, below, a will always be logged just before b.

const App = () => {
    const [num, setNum] = React.useState(0);
    React.useEffect(() => {
      setInterval(() => {
        setNum(num => num + 1);
      }, 1000);
    }, []);
    React.useEffect(() => {
      console.log('a', num);
    }, [num]);
    React.useEffect(() => {
      console.log('b', num);
    }, [num]);
    return num;
}

ReactDOM.render(<App />, document.querySelector('.react'));
<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 class='react'></div>

These effect callbacks will run shortly after the browser re-paints.

Now add the effect cleanup callback into the mix. These will always run synchronously just before the effect callback for a render runs. For example, let's say the component starts at Render A, and in Render A, an effect hook has returned a cleanup callback. Then, some state changes, and a transition to Render B occurs, and there exists a useEffect with a dependency array that includes the state change. What will happen is:

  • The functional component will be called with the new props/state, for Render B
  • The component returns the new markup at the end of the function
  • The browser repaints the screen if necessary
  • The cleanup function from render A will run
  • The effect callback from render B will run

You can see the source code for those last two actions here:

commitHookEffectListUnmount(Passive$1 | HasEffect, finishedWork);
commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);

That first call invokes all cleanup callbacks from a prior render. That second call invokes all effect callbacks for the current render. Current render effect callbacks run synchronously after the execution of prior render cleanup callbacks.

CertainPerformance
  • 260,466
  • 31
  • 181
  • 209
  • So the clean-up callbacks of RenderA always execute before the regular callbacks of RenderB's `useEffect`s? Even if `useEffect1`, containing a regular callback that is seen on renderB, is higher up in source-order than `useEffect2` (which, on renderB, has only the cleanup callback to execute), `useEffect2`'s cleanup callback will execute before `useEffect1`'s regular callback? – tonitone120 Jan 08 '21 at 00:11
  • Moreover, amongst clean-up callbacks, they execute in source-order? – tonitone120 Jan 08 '21 at 00:13
  • 1
    Yes, that's how it works. All cleanups from the prior render run (in effect declaration order), then all effect callbacks for the current render run (in effect declaration order). – CertainPerformance Jan 08 '21 at 00:17
  • I also understand (although admittedly, only generally) that, when dealing with a (functional) component and its child component(s), the `useEffect` (regular) callbacks of the child component will execute before the parent's. Can I expect similar behaviour when it comes to 'clean-up' callbacks? E.g. child component's clean-up callbacks will execute before its parent's clean-up callbacks. This might not be useful information (feel free to prove me wrong) but I thought I might as well understand. – tonitone120 Jan 13 '21 at 00:33
1

You are almost on point, let me try to provide some more clarity on this

Before we dive into this we need to understand that if useEffect has nothing as the second argument ( as it is in the question ) the function passed to useEffect will be executed in every render.

  1. The argument passed to useState() is used as the initial value. Hence time is initialised to 5
  2. useEffect is executed => setTimeout() will now execute after 1 sec => return value of useEffect lets call it func1 is stored to be executed later
  3. The value of time, which is 5 right now, is rendered
  4. After 1 second setTimeout() executes and changes the value of time to 4 and sets it
  5. hence a re-render occurs and useEffect is executed again. At this point useEffect executes func1 to clean up the previous effect and then function passed to useEffect will execute so a new setTimeout() is initialised and return statement, lets call this func2 is stored to be executed later
  6. The value of time, which is 4 right now, is rendered
  7. After 1 second setTimeout() executes and changes the value of time to 3 and sets it
  8. This now goes back to point 4 and the process keeps happening infinitely
Yash Kalwani
  • 297
  • 3
  • 10