Photo by Lautaro Andreani on Unsplash
Understanding React Re-renders: Demystifying the What, When, and Why
Understanding when React re-renders and when it doesn't can be a hard thing to learn for many developers, including myself. The issue is not necessarily the complexity of the topic, but the numerous conflicting explanations I encountered while researching online or consulting with colleagues.
Consequently, I had to conduct some research, experiment with multiple examples, and gain practical experience to grasp when a component was re-rendering and when it wasn't.
In this article, I will provide a tutorial that I wished I had access to when I was starting as a React developer, and even as an experienced intermediate-level developer.
First, Let’s start by discussing the main misconceptions about this topic.
Re-render misconceptions
Having spent over 5 years working with React, I have observed a couple of misunderstandings regarding to why React re-renders. These misconceptions often result in poor code architecture and composition patterns, and can also spread false information about a core topic in React.
This can be particularly detrimental to junior developers who may receive this misinformation.
Let's explore what I believe are the two primary misconceptions on this topic.
Props change
“A component re-renders itself when it’s props change”.
I consider this phrase to be one of the biggest myths of React.
In reality, a component is usually re-rendered when its state updates or when its parent component re-renders. This concise statement summarizes the fundamental principles of when re-rendering occurs most of the time in React.
props change !== re-render
To illustrate this point, let’s take a look at the following example:
export const Content = () => {
const [name, setName] = React.useState("");
return (
<div>
<Header />
<Form setName={setName} />
<p>User: {name}</p>
</div>
);
};
const Form = ({ setName }) => {
return <input onChange={(e) => setName(e.target.value)} />;
};
const Header = () => {
console.log("Header render");
return (
<header>
<h1>
<a href="#">My website</a>
</h1>
</header>
);
};
Whenever the user types in the input and updates the name
state, the <Content>
component re-renders itself. However, it's important to note that when a component re-renders, it attempts to re-render all of its children, regardless of whether any changes have been made to their descendant props or not.
As a result, every time the user types into the input, the <Header>
component also re-renders, as the onChange
event triggers a state update, which causes a re-render.
You may ask yourself, why does this happen?
The short answer is that this is the default behavior from React. React is unable to know if the child components depend on the state value that was updated, so it goes for the “safer” approach of re-rendering them anyway.
We have to remember that for React, the purpose of re-rendering is to determine how a state change will impact the UI. Hence, it is necessary to re-render all components that might be affected to obtain an accurate representation of the final state of the UI.
Something that helps me remember this is to think about it like this, every component that is not memoized***** will be re-render when its parent re-renders.
Learn more about how to prevent re-renders using memoization in React.
*****memoized component follow their own “rules” on when they should be re-rendered.
App re-renders when the state changes
I have come across different claims that any state update in a React application would trigger a complete re-render of the entire tree, regardless of where the update occurred.
However, this is a misconception. The truth is that not every state update would cause the entire app to be re-rendered.
It is important to keep in mind that:
Component state changes || Parent re-renders === Component re-renders
More on how the components tree re-renders in the Parent re-renders section.
When React re-renders
Now that we've covered when React does not re-render, let's dive into the specific situations that trigger a component re-rendering.
There are three primary reasons why a React component could be re-rendered:
Changes in State
Re-rendering of the parent component
Changes in Context
In the context of application usage, this could be rephrased to these main actions:
User interacting with the page/application (State change/Context change)
Asynchronous request (CRUI operations: State change/Context change)
Changes in State
Typically this is one of the first rules that new React developers learn: When the state of a component changes it will automatically re-render itself.
state change === re-render
Usually, state changes can be considered the main reason for a component being re-rendered.
Let's take a look at how this looks using hooks:
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
console.log("Re-render triggered");
return (
<>
<button onClick={handleClick}>Increment count</button>
<div>{count}</div>
</>
);
}
Now, this is what it looks when using classes:
class App extends React.Component {
constructor() {
super();
this.state = { count: 0 };
}
increment = () => {
this.setState({
...this.state,
count: this.state.count + 1
});
};
render() {
console.log("Re-render triggered");
return (
<>
<button onClick={this.increment}>Increment count</button>
<div>{this.state.count}</div>
</>
);
}
}
Each time the "Increment count" button is clicked, a state update of the component is triggered, resulting in the component re-rendering itself.
Parent re-renders
The other rule of re-render that you should remember goes as follow: When a component re-renders, it will also try***** to re-render all of its children.
Parent re-render === Children re-render
This is mainly due to how updates happen in React. React updates from 🔝 to👇 bottom. The re-rendering process travels down the hierarchy of the component tree, beginning with the host (the component that initiated the re-render), and then continuing to its children and so forth.
const Parent = () => {
const [randomNumber, setNumber] = React.useState(Math.random());
const setRandomNumber = () => setNumber(Math.random());
return (
<>
<button onClick={setRandomNumber}>Set random number!</button>
<div>{randomNumber}</div>
{/* Children will be re-rendered if the Parent re-renders */}
<Child1 />
<Child2 />
<Child3 />
</>
)
}
Every-time that the <Parent>
component re-render, by its parent re-rendering or by any internal state/context update, all of the <Child>
components will re-render as well.
*****Parent components will try to re-render all descendants, but the children can “skip” the re-render if they are memoized.
Changes in Context
Finally, we have the last piece of the re-rendering puzzle: context.
Every component that “consumes” from context, will be re-render if the context changes, regardless if the specific piece of the context being consumed by a component changed or not.
Context changes === Every context consumer gets re-rendered
Wait, are you telling me that if a component uses a part of the context that didn’t it will still get re-rendered!?
Yes, React only knows that the previousContext !== currentContext
, therefore it will re-render any context consumer.
Let's provide an example to illustrate this point:
const Context = createContext();
const Provider = ({ children }) => {
const [todos, setTodos] = React.useState([]);
useEffect(() => {
fetch('<https://jsonplaceholder.typicode.com/todos>')
.then(res => res.json())
.then(todos => setTodos(todos));
}, []);
const clearTodos = useCallback(() => setTodos([]), []);
const value = useMemo(() => ({
todos,
clearTodos,
}), [todos, clearTodos]);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}
const App = () => (
<Provider>
<Clear />
<List />
<Footer />
</Provider>
);
const List = React.memo(() => {
console.log('List component rendered');
const { todos } = useContext(Context);
return <ul>{todos.map(todo => <li>{todo.title}</li>)}</ul>;
});
const Clear = () => {
console.log('Clear component rendered');
const { clearTodos } = useContext(Context);
return <button onClick={clearTodos}>Clear list</button>
}
const Footer = () => {
console.log('Footer component rendered');
return <footer><p>© 2023 made with ❤️ by Fer Codes</p></footer>
}
In this example, we are sharing our context with the <List>
and <Clear>
components since they are both children of the <Provider>
.
You may be able to spot right away that it is expected for the <List>
component to re-render when the list of todos changes. We are starting with a []
which after our API call gets populated with a list of todos.
What might not be super intuitive at first is that the <Clear>
component will also be re-rendered when the todos change. You might think that since the component is only consuming the clearTodos
function, which we also memoized with useCallback meaning that it is guaranteed that the function will not change, this should be enough to prevent the <Clear>
component from re-rendering.
The thing is that React only knows that the context value changed, it doesn’t know which parts of it did, so as long as the context is different, React will re-render all context consumers.
Finally, the only component that does not re-render on the context
update is the <Footer>
component since it’s not “consuming” from the context.
Conclusion
Remember that re-renders have three main causes:
Changes in State
Parent re-renders
Changes in Context
Finally don’t forget that:
Props changes don’t cause the re-render
Re-renders happen from top to bottom, they don’t affect the entire application
I truly hope you learn something new, and that this guide helps you understand when and why React components re-render.