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.
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!
“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.log
s 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 8 9. 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 10. useEffect
fires before paint when state is set in auseLayoutEffect
. 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 inReactDOM.flushSync
. 11 12 - 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.
- React Docs: Render and Commit
- React Docs: Lifecycle of Reactive Effects
- Why React Re-Renders - Josh W Comeau
- Thoughtspile: So you think you know everything about React refs
- Thoughtspile: useLayoutEffect is a bad place for deriving state
- Thoughtspile: useEffect sometimes fires before paint
- Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior · Mark’s Dev Blog
- Or Zhenghao’s summary if you’re short on time or focus: When Does React Render Your Component?
- A Visual Guide to useEffect | Alex Sidorenko
- A Visual Guide to useEffect - Cleanups | Alex Sidorenko
- A Complete Guide to useEffect — Overreacted
- 📺 Understand the React Hook Flow | egghead.io
- donavon: A flowchart that explains the new lifecycle of a Hooks component
I couldn’t include footnotes in the text in the diagram component so instead they’re piled up here ¯_(ツ)_/¯.
- Ref
- 13
- 2
- 14
- 4
- ref Callbacks
- 15
- 3
- 16
- Browserpaint
- 17
- Browser paint
- 18
- useEffect
- 19
- 11
- 20
- 21
- useEffect cleanup
- 22
- 23
- 24
- useLayoutEffect
- 25
- Update phases
- 26
- Set state in render
- 8
- 27
- State initializer function
- 28
- 29
- useCallback
- 30
- useMemo
- 30
-
↩
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 ↩3 ↩4 ↩5
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
Thoughtspile: So you think you know everything about React refsuseLayoutEffect
[...] for the corresponding DOM update is called. This, in turn, means thatuseEffect
and parentuseLayoutEffect
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 thatref.current
is alwaysnull
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. -
↩ ↩2
If the ref callback is defined as an inline function, it will get called twice during updates, first with
React Docs: Refs and the DOM — Caveats with callbacks refsnull
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. -
↩ ↩2
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
React Docs: Manipulating the DOM with Refs – When React attaches the refsref.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 setsref.current
during the commit. Before updating the DOM, React sets the affectedref.current
values tonull
. 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 guarantees that
React Docs: Hooks Reference useStatesetState
function identity is stable and won’t change on re-renders. This is why it’s safe to omit from theuseEffect
oruseCallback
dependency array. -
↩
The ref value
eslint-plugin-react-hooks Warning on reading ref.current in effect cleanupref.current
will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copyref.current
to a variable inside the effect, and use that variable in the cleanup function. -
↩
What will be logged on the console when this React component loads?
Tweet by Diego Haz
[code]
With [client side rendering], the correct answer isfalse
. Layout effects run after DOM mutations. This happens after the browser has rendered and autofocused the element. TheonFocus
handler will be called but before the [useLayoutEffect
] callback. But here’s a curious thing: the newuseInsertionEffect
hook runs before DOM mutations and before theautoFocus
event. If you replaced useLayoutEffect withuseInsertionEffect
, the answer would betrue
(with CSR). -
↩ ↩2
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
React Docs: You Might Not Need an Effect — Adjusting some state when a prop changesitems !== 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. -
↩
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
React Docs: useState - Storing information from previous rendersset
function while your component is rendering. [..] Note that if you call aset
function while rendering, it must be inside a condition likeprevCount !== count
, and there must be a call likesetPrevCount(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. -
↩
The signature [of
React Docs: Hooks Reference — useInsertionEffectuseInsertionEffect
] is identical touseEffect
, but it fires synchronously before all DOM mutations. Use this to inject styles into the DOM before reading layout inuseLayoutEffect
. 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. PreferuseEffect
oruseLayoutEffect
instead. -
↩ ↩2
[...] 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
React Docs: Hooks Reference — Timing of effectsuseLayoutEffect
. It has the same signature as useEffect, and only differs in when it is fired. Additionally, starting in React 18, the function passed touseEffect
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 influshSync
. This behavior allows the result of the effect to be observed by the event system, or by the caller offlushSync
. -
↩
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 -
↩
To get the basics out of the way, ref is set to the DOM node when it’s mounted, and set to
Thoughtspile: So you think you know everything about React refsnull
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 throughnull
. -
↩
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 -
↩
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 [
React Docs: Refs and the DOM — Callbacks refsuseRef
], you pass a function. The function receives the [...] HTML DOM element as its argument, which can be stored and accessed elsewhere. -
↩
React will also call your ref callback whenever you pass a different ref callback. In the above example,
ReactDOM Docs: Components ref callback function(node) => { ... }
is a different function on every render. This is why, when your component re-renders, the previous function will be called withnull
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 withnull
. 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 withnull
as the argument, and the next function will be called with the DOM node. -
↩
There’s a subtle difference between having an [inline functions (a callback)] as your
Thoughtspile.tech: So you think you know everything about React refsref
prop and aref
object or a stable callback — the [inline function] has a new identity on every render, forcing theref
to go through an update cycle [where it will be]null
. This is normally not too bad, but good to know. -
↩
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 -
↩
Does
React Docs: Using the Effect HookuseEffect
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. -
↩
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 -
↩
Unlike
React Docs: useEffect — Timing of effectscomponentDidMount
andcomponentDidUpdate
, the function passed touseEffect
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. -
↩
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
React Legacy Docs: useEffect — Explanation: Why Effects Run on Each UpdateuseEffect
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. -
↩
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 -
↩
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.
React Docs: Lifecycle of Reactive Effects - What to do when you don’t want to re-synchronize
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! -
↩
Thoughtspile: useEffect sometimes fires before paintuseEffect
should run after paint to prevent blocking the update. But did you know it’s not really guaranteed to fire after paint? Updating state inuseLayoutEffect
makes everyuseEffect
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: ”AlthoughuseEffect
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 fromuseLayoutEffect
, then the effect must be flushed before that update, which is before paint. -
↩
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 -
↩
How do I implement getDerivedStateFromProps?
Hooks FAQ – 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. -
↩
React saves the initial state once and ignores it on the next renders. Although the result of [the function call passed to
React Docs: useState — Avoiding recreating the initial stateuseState
] 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 touseState
instead. [...] If you pass a function to useState, React will only call it during initialization. -
↩
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
React Docs: useReducer — Avoiding recreating the initial stateuseReducer
] 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. -
↩ ↩2
React Docs: useCallback — Parametersfn:
[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.