React, in its purest form, is a library for building user interfaces. It's so simple that the entire mental model can be represented as a formula, v = f(s)
– where your view is simply a function of your state.
Though this equation gives us a simple mental model for how React works, there's one aspect of the equation that still, after all these years, seems to confuse people. Exactly when and how is f
invoked? Or said differently, exactly when and how does React update the view?
Many blog posts, conference talks, and tweet threads have been dedicated to this seemingly simple topic. And yet, for some reason, it's still a topic that even experienced React developers have some (often unknown) misconceptions about.
To better answer this, instead of starting with how or when, let's go back one step further and start from first principles. First, what is rendering?
What is rendering?
When React renders a component, two things happen.
First, React creates a snapshot of your component which captures everything React needs to update the view at that particular moment in time. props, state, event handlers, and a description of the UI (based on those props and state) are all captured in this snapshot.
From there, React takes that description of the UI and uses it to update the View.
In order to get the starting UI for your application, React will do an initial render, starting at the root
of your application.
createRoot
When we say root
, we literally mean root
.
To create a root
, you first get the HTML element you want to mount your React app to, pass that to React DOM's createRoot
function, then call root.render
, passing it a React element which React will use as the starting point to get the initial UI of your application.
import { createRoot } from "react-dom/client";import App from "./App";const rootElement = document.getElementById("root");const root = createRoot(rootElement);root.render(<App />);
Unless you're building a React app from scratch, this is usually done for you by whatever generates your app (e.g. create-react-app
, create-next-app
, Codesandbox, etc.).
Of course, this initial render is the most uninteresting one. Without the ability to re-render, React would be mostly useless. It's how React treats all subsequent renders that's what makes it more interesting.
That naturally leads us to our next question, when exactly does React re-render a component?
Looking back to our v = f(s)
equation, your intuition might be that f
is invoked whenever s
changes. That would make sense. We wouldn't want to recalculate the View
unless the State
had changed. In fact, it's as simple as that.
When does React re-render?
This may be surprising, but it's true. The only thing that can trigger a re-render of a component in React is a state change.
With that, we now have our final , how does React actually know when the state of a component has changed? At this point it's fairly trivial and, once again, it has to do with our snapshot.
When an event handler is invoked, that event handler has access to the props and state as they were in the moment in time when the snapshot .
From there, if the event handler contains an invocation of useState
's updater function and React sees that the new state is different than the state in the snapshot, React will trigger a re-render of the component – creating a new snapshot and updating the view.
At this point, you have a high-level, theoretical mental model for how React renders, and then re-renders whenever state changes. That's nice, and it makes for some fun visuals, but like any mental model, it's only helpful in as much as it can withstand the stress test of reality.
Let's say we wanted a simple app that allowed a user to click a button and toggle different "greetings" – something like this.
To do this, we'll stick all our greetings in an array, and then we'll have a piece of state that keeps track of the index of the greeting we want to display.
We'll then have a button
that when it's clicked, will either increment index
or reset it back to 0
if we've reached the end of the array.
Now whenever our button
is clicked, our handleClick
event handler will run. The state (index
) inside of handleClick
will be the same as the state in the most recent snapshot. From there, React sees there's a call to setIndex
and that the value passed to it is different than the state in the snapshot – triggering a re-render.
That's a lot of words. Here's what it would look like if we visualized it.
Let's look at another example (without the visuals now).
In this code, when the button
is clicked, what gets alerted?
Don't trick yourself into thinking that all of a sudden things are now somehow different or more complex than they were prior to you seeing this example. The same rules apply.
We know that when our handleClick
event handler runs, it has access to the props and state as they were in the moment in time when the snapshot was created – in that moment, status
was clean
. Therefore, when we alert status
, we get clean
.
Now click the button
again. You'll notice that because our previous button
click triggered a re-render and created a new snapshot with the status
of dirty
, on any clicks after the initial click we get dirty
.
Let's try another one. What happens when we click the button
in this example?
When the button is clicked, React runs our event handler and sees that we invoke an updater function inside of it. From there, it calculates the new state to be 0
. It then notices the new state, 0
, is the same as the state in the snapshot, 0
. Therefore, React does not trigger a re-render and the snapshot and View remain the same.
Again, React will only re-render if the event handler contains an invocation of useState
's updater function (✅) and React sees that the new state is different than the state in the snapshot (❌).
How about this one. What will count
be after the button
is clicked?
Again, we know that when our handleClick
event handler runs, it has access to the props and state as they were in the moment in time when the snapshot was created – in that moment, count
was 0
.
So eventually, once React is done calculating the new state, it'll see that the new state, 1
, is different than the state in the snapshot, 0
. From there, React will re-render the component, creating a new snapshot and updating the View.
Once you understand how rendering works, these kind of questions become trivial to walk through. But there is one question that may have come up after looking at the last example.
How many times will the Counter
component re-render when the button is clicked?
Your intuition might be that React will re-render for every updater function it encounters, so 3 times in our example.
const handleClick = () => {setCount(0 + 1)setCount(0 + 1)setCount(0 + 1)}
Thankfully, that's not right since it would lead to a lot of unnecessary re-renders.
Instead, React will only re-render after it's taken into account every updater function inside of the event handler and it's sure what the final state is. So in our example, React will only re-render once per click.
Batching: How React Calculates State
React only re-rendering once it's taken into account every updater function inside of the event handler implies that React has some sort of internal algorithm it uses to calculate the new state. React refers to this algorithm as "batching".
Thankfully, it's pretty straight forward.
Whenever React encounters multiple invocations of the same updater function (e.g. setCount
in our example), it will keep track of each of them, but only the result of the last invocation will be used as the new state.
const handleClick = () => {setCount(1)setCount(2)setCount(3)}
So in this example, the new state will of course be 3
.
Now it's uncommon, but there is a way to tell React to use the value of the previous invocation of the updater function instead of replacing it. To do that, you pass the updater function a function itself that will take in the value from the most recent invocation as its argument.
const handleClick = () => {setCount(1)setCount(2)setCount((c) => c + 3)}
In this example, c
will be 2
since that's what was passed to the most recent invocation of setCount
before our callback function ran. Therefore, the final state will be 2
+ 3
, or 5
.
What about this one?
const handleClick = () => {setCount(1)setCount((c) => c + 3)setCount(7)setCount((c) => c + 10)}
Let's walk through it.
The state will be 1
, then it will be 1
+ 3
, or 4
, then it will be 7
, then it will be 7
+ 10
, or 17
.
Notice that we don't use 4
in the third invocation even though that's what was returned in the second invocation. That's because we're just telling React to forget everything it knew and use 7
as the new state.
Another way to think about our code above is like this.
const handleClick = () => {setCount((c) => 1)setCount((c) => c + 3)setCount((c) => 7)setCount((c) => c + 10)}
Where React just ignores the previous value unless you explicitly use it.
Are you having yet? Let's look at another example.
After clicking on our button
3 times, what will the UI show, what will be logged to the console, and how many times will App have re-rendered?
The first time the button
is clicked, the UI will show 1, 2
, the console will show { linear: 0, exponential: 1 }
, and the App
component will have re-rendered once.
The second time the button
is clicked, the UI will show 2, 4
, the console will show { linear: 1, exponential: 2 }
, and the App
component will have re-rendered twice.
And the third time the button
is clicked, the UI will show 3, 8
, the console will show { linear: 2, exponential: 4 }
, and the App
component will have re-rendered three times.
This example not only , but it also shows us another interesting aspect of how React re-renders. That is, React will only re-render once per event handler, even if that event handler contains updates for multiple pieces of state.
This is yet another example of how React will only re-render a component when it absolutely has to. With that in mind, let's take a look at another example that may surprise you.
Let's start with our Greeting
code from earlier.
Now let's say we wanted to make our Greeting
component a little more welcoming. To do that, we'll create and render a Wave
component inside of Greeting
that will add a 👋 emoji in the top right of the UI.
Very welcoming. Notice anything peculiar about how our app works, though? Before you play with it, try to guess when our nested Wave
component will re-render.
Your intuition is probably thinking never. After all, if React truly only re-renders when it absolutely has to, why would Wave
ever re-render since it doesn't accept any props and has no state?
I've added a log to Wave
so we can see when it renders. Go ahead and try it now.
Notice that Wave
re-renders whenever we click the button
(changing the index
state inside of Greeting
). This may not be intuitive, but it demonstrates an important aspect of React. Whenever state changes, React will re-render the component that owns that state and all of its child components – regardless of whether or not those child components accept any props.
I get this may seem like a strange default. Shouldn't React only re-render child components if their props change? Anything else seems like a waste.
First, React is very good at rendering. If you have a performance problem, the reality is it's rarely because of .
Second, the assumption that React should only re-render child components if their props change works in a world where React components are always pure functions and props are the only thing these components need to render. The problem, as anyone who has built a real world React app knows, is that isn't always the case.
To be a pragmatic tool and not just a philosophical one we discuss in computer science courses, React provides some to break out of its normal v = fn(s)
paradigm. We'll cover these later in the course, but know that we can't just assume a component should only re-render when its props change.
Third, if you do have an expensive component and you want that component to opt out of this default behavior and only re-render when its props change, you can use React's React.memo
higher-order component.
React.memo
is a function that takes in a React component as an argument and returns a new component that will only re-render if its props change.
Now, regardless of how many times we click our button
, Wave
will only render once, on the initial render.
But again, even when dealing with child components, our mental model holds strong. Any time a React component renders, regardless of why or where it's located in the component tree, React creates a snapshot of the component which captures everything React needs to update the view at that particular moment in time. props, state, event handlers, and a description of the UI (based on those props and state) are all captured in this snapshot.
From there, React takes that description of the UI and uses it to update the View.
Now you may have heard of React's StrictMode
component. It's React's way of saying "What if we took this really simple mental model and just totally blew it up?"
That's an exaggeration, but it does change things just a little bit.
Whenever you have StrictMode
enabled, React will re-render your components an extra time.
All our examples until this point have had strict mode disabled, for obvious reasons. But so you can see it in action, here's our Wave
example now in StrictMode
.
Notice that every time we click the button, our app renders twice.
This may seem strange, but StrictMode
makes sure your app is resilient to re-renders and that your components are pure. If not, it'll become obvious when React renders the 2nd time.
Regardless of if React renders once or 100 times, because your view should be a function of your state, it shouldn't matter. StrictMode
helps you make sure that's the case.
The way you enable StrictMode
is by wrapping it around your App
like this.
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';const root = createRoot(document.getElementById('root'));root.render(<StrictMode><App /></StrictMode>);
Similar to creating your root
, unless you're building a React app from scratch, this is usually done for you by whatever generates your app.
Now one last question you probably have, doesn't this have performance implications? Yes, but React will only respect StrictMode
when you're in development
mode. In production
, it'll be ignored.
Now that you know about StrictMode
, we'll include a toggle on every code preview that'll allow you to toggle it on and off.