https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/ Mark's Dev Blog * About Random musings on React, Redux, and more, by Redux maintainer Mark "acemarke" Erikson Sponsor @markerikson Home Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior Posted on May 17, 2020 #react #context #redux #greatest-hits This is a post in the Blogged Answers series. --------------------------------------------------------------------- Details on how React rendering behaves, and how use of Context and React-Redux affect rendering I've seen a lot of ongoing confusion over when, why, and how React will re-render components, and how use of Context and React-Redux will affect the timing and scope of those re-renders. After having typed up variations of this explanation dozens of times, it seems it's worth trying to write up a consolidated explanation that I can refer people to. Note that all this information is available online already, and has been explained in numerous other excellent blog posts and articles, several of which I'm linking at the end in the "Further Information" section for reference. But, people seem to be struggling to put the pieces together for a full understanding, so hopefully this will help clarify things for someone. Note: Updated October 2022 to cover React 18 and future React updates I also did a talk based on this post for React Advanced 2022: React Advanced 2022 - A (Brief) Guide to React Rendering Behavior Table of Contents [?] * What is "Rendering"? + Rendering Process Overview + Render and Commit Phases * How Does React Handle Renders? + Queuing Renders + Standard Render Behavior + Rules of React Rendering + Component Metadata and Fibers + Component Types and Reconciliation + Keys and Reconciliation + Render Batching and Timing + Async Rendering, Closures, and State Snapshots + Render Behavior Edge Cases * Improving Rendering Performance + Component Render Optimization Techniques + How New Props References Affect Render Optimizations + Optimizing Props References + Memoize Everything? + Immutability and Rerendering + Measuring React Component Rendering Performance * Context and Rendering Behavior + Context Basics + Updating Context Values + State Updates, Context, and Re-Renders + Context Updates and Render Optimizations * React-Redux and Rendering Behavior + React-Redux Subscriptions + Differences between connect and useSelector * Future React Improvements + "React Forget" Memoizing Compiler + Context Selectors * Summary * Final Thoughts * Further Information What is "Rendering"? [?] Rendering is the process of React asking your components to describe what they want their section of the UI to look like, now, based on the current combination of props and state. Rendering Process Overview [?] During the rendering process, React will start at the root of the component tree and loop downwards to find all components that have been flagged as needing updates. For each flagged component, React will call either FunctionComponent(props) (for function components), or classComponentInstance.render() (for class components) , and save the render output for the next steps of the render pass. A component's render output is normally written in JSX syntax, which is then converted to React.createElement() calls as the JS is compiled and prepared for deployment. createElement returns React elements, which are plain JS objects that describe the intended structure of the UI. Example: // This JSX syntax: return Text here // is converted to this call: return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here") // and that becomes this element object: {type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]} // And internally, React calls the actual function to render it: let elements = MyComponent({...props, children}) // For "host components" like HTML: return // becomes React.createElement("button", {onClick}, "Click Me") // and finally: {type: "button", props: {onClick}, children: ["Click me"]} After it has collected the render output from the entire component tree, React will diff the new tree of objects (frequently referred to as the "virtual DOM"), and collects a list of all the changes that need to be applied to make the real DOM look like the current desired output. The diffing and calculation process is known as "reconciliation". React then applies all the calculated changes to the DOM in one synchronous sequence. Note: The React team has downplayed the term "virtual DOM" in recent years. Dan Abramov said: I wish we could retire the term "virtual DOM". It made sense in 2013 because otherwise people assumed React creates DOM nodes on every render. But people rarely assume this today. "Virtual DOM" sounds like a workaround for some DOM issue. But that's not what React is. React is "value UI". Its core principle is that UI is a value, just like a string or an array. You can keep it in a variable, pass it around, use JavaScript control flow with it, and so on. That expressiveness is the point -- not some diffing to avoid applying changes to the DOM. It doesn't even always represent the DOM, for example is not DOM. Conceptually it represents lazy function calls: Message.bind(null, { recipientId: 10 }). Render and Commit Phases [?] The React team divides this work into two phases, conceptually: * The "Render phase" contains all the work of rendering components and calculating changes * The "Commit phase" is the process of applying those changes to the DOM After React has updated the DOM in the commit phase, it updates all refs accordingly to point to the requested DOM nodes and component instances. It then synchronously runs the componentDidMount and componentDidUpdate class lifecycle methods, and the useLayoutEffect hooks. React then sets a short timeout, and when it expires, runs all the useEffect hooks. This step is also known as the "Passive Effects" phase. React 18 added "Concurrent Rendering" features like useTransition. This gives React the ability to pause the work in the rendering phase to allow the browser to process events. React will either resume, throw away, or recalculate that work later as appropriate. Once the render pass has been completed, React will still run the commit phase synchronously in one step. A key part of this to understand is that "rendering" is not the same thing as "updating the DOM", and a component may be rendered without any visible changes happening as a result. When React renders a component: * The component might return the same render output as last time, so no changes are needed * In Concurrent Rendering, React might end up rendering a component multiple times, but throw away the render output each time if other updates invalidate the current work being done This excellent interactive React hooks timeline diagram helps illustrate the sequencing of rendering, committing, and executing hooks: React hooks timeline diagram For additional visualizations, see: * React hooks flow diagram * React hooks render/commit phase diagram * React class lifecycle methods diagram How Does React Handle Renders? [?] Queuing Renders [?] After the initial render has completed, there are a few different ways to tell React to queue a re-render: * Function components: + useState setters + useReducer dispatches * Class components: + this.setState() + this.forceUpdate() * Other: + Calling the ReactDOM top-level render() method again (which is equivalent to calling forceUpdate() on the root component) + Updates triggered from the new useSyncExternalStore hook Note that function components don't have a forceUpdate method, but you can get the same behavior by using a useReducer hook that always increments a counter: const [, forceRender] = useReducer((c) => c + 1, 0); Standard Render Behavior [?] It's very important to remember that: React's default behavior is that when a parent component renders, React will recursively render all child components inside of it! As an example, say we have a component tree of A > B > C > D, and we've already shown them on the page. The user clicks a button in B that increments a counter: * We call setState() in B, which queues a re-render of B. * React starts the render pass from the top of the tree * React sees that A is not marked as needing an update, and moves past it * React sees that B is marked as needing an update, and renders it. B returns as it did last time. * C was not originally marked as needing an update. However, because its parent B rendered, React now moves downwards and renders C as well. C returns again. * D was also not marked for rendering, but since its parent C rendered, React moves downward and renders D too. To repeat this another way: Rendering a component will, by default, cause all components inside of it to be rendered too! Also, another key point: In normal rendering, React does not care whether "props changed" - it will render child components unconditionally just because the parent rendered! This means that calling setState() in your root component, with no other changes altering the behavior, will cause React to re-render every single component in the component tree. After all, one of the original sales pitches for React was "act like we're redrawing the entire app on every update". Now, it's very likely that most of the components in the tree will return the exact same render output as last time, and therefore React won't need to make any changes to the DOM. But, React will still have to do the work of asking components to render themselves and diffing the render output. Both of those take time and effort. Remember, rendering is not a bad thing - it's how React knows whether it needs to actually make any changes to the DOM! Rules of React Rendering [?] One of the primary rules of React rendering is that rendering must be "pure" and not have any side effects! This can be tricky and confusing, because many side effects are not obvious, and don't result in anything breaking. For example, strictly speaking a console.log() statement is a side effect, but it won't actually break anything. Mutating a prop is definitely a side effect, and it might not break anything. Making an AJAX call in the middle of rendering is also definitely a side effect, and can definitely cause unexpected app behavior depending on the type of request. Sebastian Markbage wrote an excellent document entitled The Rules of React. In it, he defines the expected behaviors for different React lifecycle methods, including render, and what kinds of operations would be considered safely "pure" and which would be unsafe. It's worth reading that in its entirety, but I'll summarize the key points: * Render logic must not: + Can't mutate existing variables and objects + Can't create random values like Math.random() or Date.now() + Can't make network requests + Can't queue state updates * Render logic may: + Mutate objects that were newly created while rendering + Throw errors + "Lazy initialize" data that hasn't been created yet, such as a cached value Component Metadata and Fibers [?] React stores an internal data structure that tracks all the current component instances that exist in the application. The core piece of this data structure is an object called a "fiber", which contains metadata fields that describe: * What component type is supposed to be rendered at this point in the component tree * The current props and state associated with this component * Pointers to parent, sibling, and child components * Other internal metadata that React uses to track the rendering process If you've ever heard the phrase "React Fiber" used to describe a React version or feature, that's really referring to the rewrite of React's internals that switched the rendering logic to rely on these "Fiber" objects as the key data structure. That was released as React 16.0, so every version of React since then has used this approach. The shortened version of the Fiber type looks like: export type Fiber = { // Tag identifying the type of fiber. tag: WorkTag; // Unique identifier of this child. key: null | string; // The resolved function/class/ associated with this fiber. type: any; // Singly Linked List Tree Structure. child: Fiber | null; sibling: Fiber | null; index: number; // Input is the data coming into this fiber (arguments/props) pendingProps: any; memoizedProps: any; // The props used to create the output. // A queue of state updates and callbacks. updateQueue: Array; // The state used to create the output memoizedState: any; // Dependencies (contexts, events) for this fiber, if any dependencies: Dependencies | null; }; (You can see the full definition of the Fiber type as of React 18 here.) During a rendering pass, React will iterate over this tree of fiber objects, and construct an updated tree as it calculates the new rendering results. Note that these "fiber" objects store the real component props and state values. When you use props and state in your components, React is actually giving you access to the values that were stored on the fiber objects. In fact, for class components specifically, React explicitly copies componentInstance.props = newProps over to the component right before rendering it. So, this.props does exist, but it only exists because React copied the reference over from its internal data structures. In that sense, components are sort of a facade over React's fiber objects. Similarly, React hooks work because React stores all of the hooks for a component as a linked list attached to that component's fiber object. When React renders a function component, it gets that linked list of hook description entries from the fiber, and every time you call another hook, it returns the appropriate values that were stored in the hook description object (like the state and dispatch values for useReducer. When a parent component renders a given child component for the first time, React creates a fiber object to track that "instance" of a component. For class components, it literally calls const instance = new YourComponentType(props) and saves the actual component instance onto the fiber object. For function components, React just calls YourComponentType(props) as a function. Component Types and Reconciliation [?] As described in the "Reconciliation" docs page, React tries to be efficient during re-renders, by reusing as much of the existing component tree and DOM structure as possible. If you ask React to render the same type of component or HTML node in the same place in the tree, React will reuse that and just apply updates if appropriate, instead of re-creating it from scratch. That means that React will keep component instances alive as long as you keep asking React to render that component type in the same place. For class components, it actually does use the same actual instance of your component. A function component has no true "instance" the way a class does, but we can think of as representing an "instance" in terms of "a component of this type is being shown here and kept alive". So, how does React know when and how the output has actually changed? React's rendering logic compares elements based on their type field first, using === reference comparisons. If an element in a given spot has changed to a different type, such as going from
to or to , React will speed up the comparison process by assuming that entire tree has changed. As a result, React will destroy that entire existing component tree section, including all DOM nodes, and recreate it from scratch with new component instances. This means that you must never create new component types while rendering! Whenever you create a new component type, it's a different reference, and that will cause React to repeatedly destroy and recreate the child component tree. In other words, don't do this: // BAD! // This creates a new `ChildComponent` reference every time! function ParentComponent() { function ChildComponent() { return
Hi
; } return ; } Instead, always define components separately: // GOOD // This only creates one component type reference function ChildComponent() { return
Hi
; } function ParentComponent() { return ; } Keys and Reconciliation [?] The other way that React identifies component "instances" is via the key pseudo-prop. React uses key as a unique identifier that it can use to differentiate specific instances of a component type. Note that key isn't actually a real prop - it's an instruction to React. React will always strip that off, and it will never be passed through to the actual component, so you can never have props.key - it will always be undefined. The main place we use keys is rendering lists. Keys are especially important here if you are rendering data that may be changed in some way, such as reordering, adding, or deleting list entries. It's particularly important here that keys should be some kind of unique IDs from your data if at all possible - only use array indices as keys as a last resort fallback! // Use a data object ID as the key for list items todos.map((todo) => ); Here's an example of why this matters. Say I render a list of 10 components, using array indices as keys. React sees 10 items, with keys of 0..9. Now, if we delete items 6 and 7, and add three new entries at the end, we end up rendering items with keys of 0..10. So, it looks to React like I really just added one new entry at the end because we went from 10 list items to 11. React will happily reuse the existing DOM nodes and component instances. But, that means that we're probably now rendering with the todo item that was being passed to list item #8. So, the component instance is still alive, but now it's getting a different data object as a prop than it was previously. This may work, but it may also produce unexpected behavior. Also, React will now have to go apply updates to several of the list items to change the text and other DOM contents, because the existing list items are now having to show different data than before. Those updates really shouldn't be necessary here, since none of those list items changed. If instead we were using key={todo.id} for each list item, React will correctly see that we deleted two items and added three new ones. It will destroy the two deleted component instances and their associated DOM, and create three new component instances and their DOM. This is better than having to unnecessarily update the components that didn't actually change. Keys are useful for component instance identity beyond lists as well. You can add a key to any React component at any time to indicate its identity, and changing that key will cause React to destroy the old component instance and DOM and create new ones. A common use case for this is a list + details form combination, where the form shows the data for the currently selected list item. Rendering will cause React to destroy and re-create the form when the selected item changes, thus avoiding any issues with stale state inside the form. Render Batching and Timing [?] By default, each call to setState() causes React to start a new render pass, execute it synchronously, and return. However, React also applies a sort of optimization automatically, in the form of render batching. Render batching is when multiple calls to setState() result in a single render pass being queued and executed, usually on a slight delay. The React community often describes this as "state updates may be asynchronous". The new React docs also describe it as "State is a Snapshot". That's a reference to this render batching behavior. In React 17 and earlier, React only did batching in React event handlers such as onClick callbacks. Updates queued outside of event handlers, such as in a setTimeout, after an await, or in a plain JS event handler, were not queued, and would each result in a separate re-render. However, React 18 now does "automatic batching" of all updates queued in any single event loop tick. This helps cut down on the overall number of renders needed. Let's look at a specific example. const [counter, setCounter] = useState(0); const onClick = async () => { setCounter(0); setCounter(1); const data = await fetchSomeData(); setCounter(2); setCounter(3); }; With React 17, this executed three render passes. The first pass will batch together setCounter(0) and setCounter(1), because both of them are occurring during the original event handler call stack, and so they're both occurring inside the unstable_batchedUpdates() call. However, the call to setCounter(2) is happening after an await. This means the original synchronous call stack is done, and the second half of the function is running much later in a totally separate event loop call stack. Because of that, React will execute an entire render pass synchronously as the last step inside the setCounter(2) call, finish the pass, and return from setCounter(2). The same thing will then happen for setCounter(3), because it's also running outside the original event handler, and thus outside the batching. However, with React 18, this executes two render passes. The first two, setCounter(0) and setCounter(1), are batched together because they're in one event loop tick. Later, after the await, both setCounter(2) and setCounter(3) are batched together - even though they're much later, that's also two state updates queued in the same event loop, so they get batched into a second render. Async Rendering, Closures, and State Snapshots [?] One extremely common mistake we see all the time is when a user sets a new value, then tries to log the existing variable name. However, the original value gets logged, not the updated value. function MyComponent() { const [counter, setCounter] = useState(0); const handleClick = () => { setCounter(counter + 1); // THIS WON'T WORK! console.log(counter); // Original value was logged - why is this not updated yet?????? }; } So, why doesn't this work? As mentioned above, it's common for experienced users to say "React state updates are async". This is sort of true, but there's a bit more nuance then that, and actually a couple different problems at work here. Strictly speaking, the React render is literally synchronous - it will be executed in a "microtask" at the very end of this event loop tick. (This is admittedly being pedantic, but the goal of this article is exact details and clarity.) However, yes, from the point of view of that handleClick function, it's "async" in that you can't immediately see the results, and the actual update occurs much later than the setCounter() call. However, there's a bigger reason why this doesn't work. The handleClick function is a "closure" - it can only see the values of variables as they existed when the function was defined. In other words, these state variables are a snapshot in time. Since handleClick was defined during the most recent render of this function component, it can only see the value of counter as it existed during that render pass. When we call setCounter(), it queues up a future render pass, and that future render will have a new counter variable with the new value and a new handleClick function... but this copy of handleClick will never be able to see that new value. The new React docs cover this in more detail in the section State as a Snapshot, which is highly recommended reading. Going back to the original example: trying to use a variable right after you set an updated value is almost always the wrong approach, and suggests you need to rethink how you are trying to use that value. Render Behavior Edge Cases [?] Commit Phase Lifecycles [?] There's some additional edge cases inside of the commit-phase lifecycle methods: componentDidMount, componentDidUpdate, and useLayoutEffect. These largely exist to allow you to perform additional logic after a render, but before the browser has had a chance to paint. In particular, a common use case is: * Render a component the first time with some partial but incomplete data * In a commit-phase lifecycle, use refs to measure the real size of the actual DOM nodes in the page * Set some state in the component based on those measurements * Immediately re-render with the updated data In this use case, we don't want the initial "partial" rendered UI to be visible to the user at all - we only want the "final" UI to show up. Browsers will recalculate the DOM structure as it's being modified, but they won't actually paint anything to the screen while a JS script is still executing and blocking the event loop. So, you can perform multiple DOM mutations, like div.innerHTML = "a"; div.innerHTML = b";, and the "a" will never appear. Because of this, React will always run renders in commit-phase lifecycles synchronously. That way, if you do try to perform an update like that "partial->final" switch, only the "final" content will ever be visible on screen. As far as I know, state updates in useEffect callbacks are queued up, and flushed at the end of the "Passive Effects" phase once all the useEffect callbacks have completed. Reconciler Batching Methods [?] React reconcilers (ReactDOM, React Native) have methods to alter render batching. For React 17 and earlier, you can wrap multiple updates that are outside of event handlers in unstable_batchedUpdates() to batch them together. (Note that despite the unstable_ prefix, it's heavily used and depended on by code at Facebook and public libraries - React-Redux v7 used unstable_batchedUpdates internally) Since React 18 automatically batches by default, React 18 has a flushSync() API that you can use to force immediate renders and opt out of automatic batching. Note that since these are reconciler-specific APIs, alternate reconcilers like react-three-fiber and ink may not have them exposed. Check the API declarations or implementation details to see what's available. [?] React will double-render components inside of a tag in development. That means the number of times your rendering logic runs is not the same as the number of committed render passes, and you cannot rely on console.log() statements while rendering to count the number of renders that have occurred. Instead, either use the React DevTools Profiler to capture a trace and count the number of committed renders overall, or add logging inside of a useEffect hook or componentDidMount/Update lifecycle. That way the logs will only get printed when React has actually completed a render pass and committed it. Setting State While Rendering [?] In normal situations, you should never queue a state update while in the actual rendering logic. In other words, it's fine to create a click callback that will call setSomeState() when the click happens, but you should not call setSomeState() as part of the actual rendering behavior. However, there is one exception to this. Function components may call setSomeState() directly while rendering, as long as it's done conditionally and isn't going to execute every time this component renders. This acts as the function component equivalent of getDerivedStateFromProps in class components. If a function component queues a state update while rendering, React will immediately apply the state update and synchronously re-render that one component before moving onwards. If the component infinitely keeps queueing state updates and forcing React to re-render it, React will break the loop after a set number of retries and throw an error (currently 50 attempts). This technique can be used to immediately force an update to a state value based on a prop change, without requiring a re-render + a call to setSomeState() inside of a useEffect. Improving Rendering Performance [?] Although renders are the normal expected part of how React works, it's also true that that render work can be "wasted" effort at times. If a component's render output didn't change, and that part of the DOM didn't need to be updated, then the work of rendering that component was really kind of a waste of time. React component render output should always be entirely based on current props and current component state. Therefore, if we know ahead of time that a component's props and state haven't changed, we should also know that the render output would be the same, that no changes are necessary for this component, and that we can safely skip the work of rendering it. When trying to improve software performance in general, there are two basic approaches: 1) do the same work faster, and 2) do less work. Optimizing React rendering is primarily about doing less work by skipping rendering components when appropriate. Component Render Optimization Techniques [?] React offers three primary APIs that allow us to potentially skip rendering a component: The primary method is React.memo(), a built-in "higher order component" type. It accepts your own component type as an argument, and returns a new wrapper component. The wrapper component's default behavior is to check to see if any of the props have changed, and if not, prevent a re-render. Both function components and class components can be wrapped using React.memo(). (A custom comparison callback may be passed in, but it really can only compare the old and new props anyway, so the main use case for a custom compare callback would be only comparing specific props fields instead of all of them.) The other options are: * React.Component.shouldComponentUpdate: an optional class component lifecycle method that will be called early in the render process. If it returns false, React will skip rendering the component. It may contain any logic you want to use to calculate that boolean result, but the most common approach is to check if the component's props and state have changed since last time, and return false if they're unchanged. * React.PureComponent: since that comparison of props and state is the most common way to implement shouldComponentUpdate, the PureComponent base class implements that behavior by default, and may be used instead of Component + shouldComponentUpdate. All of these approaches use a comparison technique called "shallow equality". This means checking every individual field in two different objects, and seeing if any of the contents of the objects are a different value. In other words, obj1.a === obj2.a && obj1.b == = obj2.b && ......... This is typically a fast process, because === comparisons are very simple for the JS engine to do. So, these three approaches do the equivalent of const shouldRender = !shallowEqual (newProps, prevProps). There's also a lesser-known technique as well: if a React component returns the exact same element reference in its render output as it did the last time, React will skip re-rendering that particular child. There's at least a couple ways to implement this technique: * If you include props.children in your output, that element is the same if this component does a state update * If you wrap some elements with useMemo(), those will stay the same until the dependencies change Examples: // The `props.children` content won't re-render if we update state function SomeProvider({ children }) { const [counter, setCounter] = useState(0); return (
{children}
); } function OptimizedParent() { const [counter1, setCounter1] = useState(0); const [counter2, setCounter2] = useState(0); const memoizedElement = useMemo(() => { // This element stays the same reference if counter 2 is updated, // so it won't re-render unless counter 1 changes return ; }, [counter1]); return (
{memoizedElement}
); } Conceptually, we could say that the difference between these two approaches is: * React.memo(): controlled by the child component * Same-element references: controlled by the parent component For all of these techniques, skipping rendering a component means React will also skip rendering that entire subtree, because it's effectively putting a stop sign up to halt the default "render children recursively" behavior. How New Props References Affect Render Optimizations [?] We've already seen that by default, React re-renders all nested components even if their props haven't changed. That also means that passing new references as props to a child component doesn't matter, because it will render whether or not you pass the same props. So, something like this is totally fine: function ParentComponent() { const onClick = () => { console.log('Button clicked'); }; const data = { a: 1, b: 2 }; return ; } Every time ParentComponent renders, it will create a new onClick function reference and a new data object reference, then pass them as props to NormalChildComponent. (Note that it doesn't matter whether we're defining onClick using the function keyword or as an arrow function - it's a new function reference either way.) That also means there's no point in trying to optimize renders for "host components" like a
or a