Timeline of a React Component With Hooks

An interactive timeline showing how a React component with hooks runs including a quiz of React Riddles to test your knowledge.

Published · Last updated

Understanding the order in which function components run hooks can be helpful in writing React correctly and effectively. I made this diagram that shows just that. Take some time to click through it first, there’s a description and a quiz afterward. Let’s dive in!

function Component() {
const [inputValue, setInputValue] = useState(() => getInitialInput());

const ref = useRef(null);

useEffect(() => {
...

return () => {
...
};
}, [inputValue]);

useLayoutEffect(() => {
...

return () => {
...
};
});

const id = "textInput";

const heavy = useMemo(() => {
return doSomethingHeavy(inputValue)
}, [inputValue])

return (
<div>
<label htmlFor={id}>Text</label>
<input
id={id}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Type something to update"
ref={ref}
autoFocus
onFocus={(e) => {
console.log("onFocus");
}}
/>
<Child heavy={heavy} />
</div>
);
}
Render
Render

“Rendering” is React calling your components to figure out what to display on screen. React’s rendering process must always be pure, components should only return their JSX, and not change any objects or variables that existed before rendering.

A React component is ‘just’ a function that returns markup. You don’t call the component yourself, you give it to React in a tree-shaped chain of components and React will call it for you when it needs to update the UI.

React Hooks augment a component function, allowing you to hook into React to give it special abilities. Hooks run in a certain order. The React docs doesn’t recommend you thinking in “timelines” for components and urge you to think more in terms of “data” and “synchronization” instead. While you shouldn’t rely on it, timing still matters. Having an incomplete idea of the flow may trip you up and can lead you to write buggy code, so it’s good to ‘know the flow’.

Previously [with class components], you were thinking from the component’s perspective. When you looked from the component’s perspective, it was tempting to think of Effects as “callbacks” or “lifecycle events” that fire at a specific time like “after a render” or “before unmount”. This way of thinking gets complicated very fast, so it’s best to avoid it.

You describe how the UI should look at any given moment, and React makes it happen. Take advantage of that model!
Don’t try to introduce unnecessary timing assumptions into your component behavior. Your component should be ready to re-render at any time.

Test your knowledge with these React Riddles! You should be able to get the answer by clicking through the diagram. Please try to answer questions yourself before revealing the answer.

Q: Will the <Child /> component also re-render in an update of <Parent />? Does the type of update (self or by parent) trigger of <Parent /> matter in this case?

Q: Is it safe to perform side-effects in a ref callback?

Q: How many times will an inline ref callback be called in an update?

Q: How many times will a stable ref callback be called in an update?

Q: Can you read a DOM ref in render?

Q: What will happen if you pass a state-setter function to a ref attribute?: <input ref={setSomeState} />

Q: Is it safe to read a DOM ref in a useLayoutEffect or useEffect setup function?

Q: Is it safe to read a DOM ref in a useEffect or useLayoutEffect cleanup function?

Q: Why is initializing state in useEffect inefficient?

Q: Just by looking at this code block, determine in what order will the console.logs appear in the console when <Parent/> is rendered.

Bonus question: will ref.current in the onFocus be true or false?


function Child() {
console.log("Called Child");
useEffect(() => {
console.log("Child useEffect");
}, []);
useLayoutEffect(() => {
console.log("Child useLayoutEffect");
}, []);
return <div>Child</div>;
}
function Parent() {
console.log("Called Parent");
const ref = useRef(false);
useEffect(() => {
console.log("Parent useEffect");
}, []);
useLayoutEffect(() => {
ref.current = true;
console.log("Parent useLayoutEffect");
}, []);
return (
<main>
<button
autoFocus
onFocus={() => {
console.log("Focusing button. At this time, `ref.current` is", ref.current);
}}
>
Button
</button>
{console.log("Before <Child/>")}
<Child />
{console.log("After <Child/>")}
</main>
);
}

  • Setting state in a components render will make React throw away the returned JSX and immediately retry rendering . In this diagram, it would go from "Return React elements" right back to the start of “Render”. This is more efficient than setting state in a useEffect, which would completely go through the usual flow again.
  • I’ve purposefully left out useInsertionEffect because you’ll very likely never need it, unless you’re writing a CSS-in-JS library. In case you’re wondering, it runs before “Insert/Update DOM elements” and it has no cleanup. It also does not have access to refs and cannot schedule updates .
  • useEffect fires before paint when state is set in a useLayoutEffect. I’ll just refer you to useEffect sometimes fires before paint
  • Wrapping a state update in ReactDOM.flushSync opts-out state update batching and makes React apply the update to the DOM immediately. It flushes the update syncchronously.
  • Starting in React 18, the function passed to useEffect will fire synchronously before layout and paint when it’s the result of a discrete user input such as a click, or when it’s the result of an update wrapped in ReactDOM.flushSync.
  • React Context; a component consuming a context will update when the context value is changed.
  • Concurrent features, just because I haven’t looked in to that yet.

Maybe there are more things I’m not aware of? Let me know on Twitter.

Thanks to all the people who wrote something that was included in here.

Special thanks to Mark Erikson for his feedback and suggestion to include a quiz!

Diego Haz for this quiz tweet on timing of autoFocus & onFocus and Jamon Holmgren for another quiz tweet.

I couldn’t include footnotes in the text in the diagram component so instead they’re piled up here ¯_(ツ)_/¯.

Ref
ref Callbacks
Browserpaint
Browser paint
useEffect
useEffect cleanup
useLayoutEffect
Update phases
Set state in render
State initializer function
useCallback
useMemo

  1. React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!
    A (Mostly) Complete Guide to React Rendering Behavior - Standard Render Behavior
  2. Another important principle specifies the order in which refs are set and unset. The part we rely on the most is that the ref is always set before useLayoutEffect [...] for the corresponding DOM update is called. This, in turn, means that useEffect and parent useLayoutEffect are also called after the ref is set. In a single render, all the ref unsets happen before any set — otherwise, you’d get a chance to unset a ref that’s already been set during this render. Next, useLayoutEffect cleanup during re-rendering runs right between ref unset and set, meaning that ref.current is always null there. To be honest, I’m not sure why it works this way, as it’s a prime way to shoot yourself in the foot, but this seems to be the case for all React versions with hooks.
    Thoughtspile: So you think you know everything about React refs
    2 3 4 5
  3. If the ref callback is defined as an inline function, it will get called twice during updates, first with null and then again with the DOM element. This is because a new instance of the function is created with each render, so React needs to clear the old ref and set up the new one. You can avoid this by defining the ref callback as a bound method on the class, but note that it shouldn’t matter in most cases.
    React Docs: Refs and the DOM — Caveats with callbacks refs
    2
  4. In React, every update is split in two phases: During render, React calls your components to figure out what should be on the screen. During commit, React applies changes to the DOM. In general, you don’t want to access refs during rendering. That goes for refs holding DOM nodes as well. During the first render, the DOM nodes have not yet been created, so ref.current will be null. And during the rendering of updates, the DOM nodes haven’t been updated yet. So it’s too early to read them. React sets ref.current during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes. Usually, you will access refs from event handlers. If you want to do something with a ref, but there is no particular event to do it in, you might need an Effect. We will discuss effects on the next pages.
    React Docs: Manipulating the DOM with Refs – When React attaches the refs
    2
  5. React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency array.
    React Docs: Hooks Reference useState
  6. The ref value ref.current will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy ref.current to a variable inside the effect, and use that variable in the cleanup function.
    eslint-plugin-react-hooks Warning on reading ref.current in effect cleanup
  7. What will be logged on the console when this React component loads?
    [code]
    With [client side rendering], the correct answer is false. Layout effects run after DOM mutations. This happens after the browser has rendered and autofocused the element. The onFocus handler will be called but before the [useLayoutEffect] callback. But here’s a curious thing: the new useInsertionEffect hook runs before DOM mutations and before the autoFocus event. If you replaced useLayoutEffect with useInsertionEffect, the answer would be true (with CSR).
    Tweet by Diego Haz
  8. When you update a component during rendering, React throws away the returned JSX and immediately retries rendering. To avoid very slow cascading retries, React only lets you update the same component’s state during a render. If you update another component’s state during a render, you’ll see an error. A condition like items !== prevItems is necessary to avoid loops. You may adjust state like this, but any other side effects (like changing the DOM or setting a timeout) should remain in event handlers or Effects to keep your components predictable. Although this pattern is more efficient than [resetting state in] an Effect, most components shouldn’t need it either.
    React Docs: You Might Not Need an Effect — Adjusting some state when a prop changes
    2
  9. In the rare case that none of these apply, there is a pattern you can use to update state based on the values that have been rendered so far, by calling a set function while your component is rendering. [..] Note that if you call a set function while rendering, it must be inside a condition like prevCount !== count, and there must be a call like setPrevCount(count) inside of the condition. Otherwise, your component would re-render in a loop until it crashes. Also, you can only update the state of the currently rendering component like this. Calling the set function of another component during rendering is an error. Finally, your set call should still update state without mutation — this special case doesn’t mean you can break other rules of pure functions.
    React Docs: useState - Storing information from previous renders
  10. The signature [of useInsertionEffect] is identical to useEffect, but it fires synchronously before all DOM mutations. Use this to inject styles into the DOM before reading layout in useLayoutEffect. Since this hook is limited in scope, this hook does not have access to refs and cannot schedule updates. Note: useInsertionEffect should be limited to css-in-js library authors. Prefer useEffect or useLayoutEffect instead.
    React Docs: Hooks Reference — useInsertionEffect
  11. [...] not all effects can be deferred. For example, a DOM mutation that is visible to the user must fire synchronously before the next paint so that the user does not perceive a visual inconsistency. (The distinction is conceptually similar to passive versus active event listeners.) For these types of effects, React provides one additional Hook called useLayoutEffect. It has the same signature as useEffect, and only differs in when it is fired. Additionally, starting in React 18, the function passed to useEffect will fire synchronously before layout and paint when it’s the result of a discrete user input such as a click, or when it’s the result of an update wrapped in flushSync. This behavior allows the result of the effect to be observed by the event system, or by the caller of flushSync.
    React Docs: Hooks Reference — Timing of effects
    2
  12. In React 18, useEffect fires synchronously when it's the result of a discrete input.For example, if useEffect attaches an event listener, the listener is guaranteed to be added before the next input.
    New in 18: useEffect fires synchronously when it's the result of a discrete input
  13. To get the basics out of the way, ref is set to the DOM node when it’s mounted, and set to null before removing the DOM node. No surprises this far. One thing to note here is that a ref is, strictly speaking, never updated. If a DOM node is replaced by some other node (say, its DOM tag or key changes), the ref is unset, and then set to a new node. (You may think I’m being picky here, but it’s goint to prove useful in a minute.) [...] The part I was not aware of is that the identity of ref prop also forces it to update. When a ref prop is added, it’s set to DOM node. When a ref prop is removed, the old ref is set to null. Here, again, the ref is unset, than set again. This means that if you pass an inline arrow as a ref, it’ll go through unset / set cycle on every render [...]: So, why does it work that way? In short, it allows you to attach refs conditionally and even swap them between components [...]. So far we’ve learnt that refs are set node when the DOM mounts or when the ref prop is added, and unset when the DOM unmounts or the ref prop is removed. As far as I’m concerned, nothing else causes a ref to update. A changing ref always goes through null.
    Thoughtspile: So you think you know everything about React refs
  14. Every React component goes through the same lifecycle: 1) A component mounts when it’s added to the screen. 2) A component updates when it receives new props or state. This usually happens in response to an interaction. 3) A component unmounts when it’s removed from the screen. It’s a good way to think about components, but not about Effects. Instead, try to think about each Effect independently from your component’s lifecycle. An Effect describes how to synchronize an external system to the current props and state. As your code changes, this synchronization will need to happen more or less often. [...] Intuitively, you might think that React would start synchronizing when your component mounts and stop synchronizing when your component unmounts. However, this is not the end of the story! Sometimes, it may also be necessary to start and stop synchronizing multiple times while the component remains mounted.
    React Docs: Lifecycle of Reactive Effects — The lifecycle of an Effect
  15. React also supports another way to set refs called “callback refs”, which gives more fine-grain control over when refs are set and unset. Instead of passing a ref [object] created by [useRef], you pass a function. The function receives the [...] HTML DOM element as its argument, which can be stored and accessed elsewhere.
    React Docs: Refs and the DOM — Callbacks refs
  16. React will also call your ref callback whenever you pass a different ref callback. In the above example, (node) => { ... } is a different function on every render. This is why, when your component re-renders, the previous function will be called with null as the argument, and the next function will be called with the DOM node.
    When the <div> DOM node is added to the screen, React will call your ref callback with the DOM node as the argument. When that <div> DOM node is removed, React will call your ref callback with null. React will also call your ref callback whenever you pass a different ref callback. In the above example, (node) => { ... } is a different function on every render. This is why, when your component re-renders, the previous function will be called with null as the argument, and the next function will be called with the DOM node.
    ReactDOM Docs: Components ref callback function
  17. There’s a subtle difference between having an [inline functions (a callback)] as your ref prop and a ref object or a stable callback — the [inline function] has a new identity on every render, forcing the ref to go through an update cycle [where it will be] null. This is normally not too bad, but good to know.
    Thoughtspile.tech: So you think you know everything about React refs
  18. After rendering is done and React updated the DOM, the browser will repaint the screen. Although this process is known as “browser rendering”, we’ll refer to it as “painting” to avoid confusion in the rest of these docs.
    React Docs: Render and Commit — Epilogue: Browser paint
  19. Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. [...] Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
    React Docs: Using the Effect Hook
  20. Every time we re-render, we schedule a different effect, replacing the previous one. In a way, this makes the effects behave more like a part of the render result — each effect “belongs” to a particular render.
    React Docs: useEffect — Effects Without Cleanup — Detailed Explanation
  21. Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen.
    React Docs: useEffect — Timing of effects
  22. If you’re used to classes, you might be wondering why the effect cleanup phase happens after every re-render, and not just once during unmounting. There is no special code for handling updates because useEffect handles them by default. It cleans up the previous effects before applying the next effects. [...] This behavior ensures consistency by default and prevents bugs that are common in class components due to missing update logic.
    React Legacy Docs: useEffect — Explanation: Why Effects Run on Each Update
  23. Often, effects create resources that need to be cleaned up before the component leaves the screen, such as a subscription or timer ID. To do this, the function passed to useEffect may return a clean-up function. For example, to create a subscription:
    Hooks API Reference - useEffect Cleaning up an effect
  24. Effects are reactive blocks of code. They re-synchronize when the values you read inside of them change. Unlike event handlers, which only run once per interaction, Effects run whenever synchronization is necessary.
    You can’t “choose” your dependencies. Your dependencies must include every reactive value you read in the Effect. The linter enforces this. Sometimes this may lead to problems like infinite loops and to your Effect re-synchronizing too often. Don’t fix these problems by suppressing the linter!
    React Docs: Lifecycle of Reactive Effects - What to do when you don’t want to re-synchronize
  25. useEffect should run after paint to prevent blocking the update. But did you know it’s not really guaranteed to fire after paint? Updating state in useLayoutEffect makes every useEffect from the same render run before paint, effectively turning them into layout effects. Confusing? Let me explain. [...] There is, however, a more interesting passage in the docs: ”Although useEffect is deferred until after the browser has painted, it’s guaranteed to fire before any new renders. React will always flush a previous render’s effects before starting a new update.” This is a good guarantee — you can be sure no updates are missed. But it also implies that sometimes the effect fires before paint. If a) effects are flushed before a new update starts, and b) an update can start before paint, e.g. when triggered from useLayoutEffect, then the effect must be flushed before that update, which is before paint.
    Thoughtspile: useEffect sometimes fires before paint
  26. Conceptually, React does work in two phases:1) The render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls [components] and then compares the result to the previous render. 2) The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.)
    Introducing the React Profiler — Browsing commits
  27. How do I implement getDerivedStateFromProps?
    While you probably don’t need it, in rare cases that you do [...], you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive. Here, we store the previous value of the row prop in a state variable so that we can compare.
    Hooks FAQ – How do I implement getDerivedStateFromProps?
  28. React saves the initial state once and ignores it on the next renders. Although the result of [the function call passed to useState] is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating large arrays or performing expensive calculations. To solve this, you may pass it as an initializer function to useState instead. [...] If you pass a function to useState, React will only call it during initialization.
    React Docs: useState — Avoiding recreating the initial state
  29. React saves the initial state once and ignores it on the next renders. Although the result of [a function call passed to the initial value of useReducer] is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating large arrays or performing expensive calculations. To solve this, you may pass it as an initializer function to useReducer as the third argument instead. This way, the initial state does not get re-created after initialization.
    React Docs: useReducer — Avoiding recreating the initial state
  30. fn: [parameter] The function value that you want to memoize. It can take any arguments and return any values. React will return (not call!) your function back to you during the initial render. On subsequent renders, React will return the same function again if the dependencies have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later. React will not call the function. The function is returned to you so you can decide when and whether to call it.
    React Docs: useCallback — Parameters
    2