Components and Hooks must be pure
TODO
- Components must be idempotent
- Side effects must run outside of render
- Props and state are immutable
- Return values and arguments to Hooks are immutable
Components must be idempotent
React components are assumed to always return the same output with respect to their props. This is known as idempotency.
Put simply, idempotence means that you always get the same result everytime you run that piece of code.
function NewsFeed({ items }) {
// ✅ Array.filter doesn't mutate `items`
const filteredItems = items.filter(item => item.isDisplayed === true);
return (
<ul>
{filteredItems.map(item => <li key={item.id}>{item.text}</li>}
</ul>
);
}
This means that all code that runs during render must also be idempotent in order for this rule to hold. For example, this line of code is not idempotent (and therefore, neither is the component) and breaks this rule:
function Clock() {
const date = new Date(); // ❌ always returns a different result!
return <div>{date}</div>
}
new Date()
is not idempotent as it always returns the current date and changes its result every time it’s called. When you render the above component, the time displayed on the screen will stay stuck on the time that the component was rendered. Similarly, functions like Math.random()
also aren’t idempotent, because they return different results every time they’re called, even when the inputs are the same.
Try building a component that displays the time in real-time in our challenge to see if you follow this rule!
Side effects must run outside of render
Side effects should not run in render, as React can render components multiple times to create the best possible user experience.
While render must be kept pure, side effects are necessary at some point in order for your app to do anything interesting, like showing something on the screen! The key point of this rule is that side effects should not run in render, as React can render components multiple times. In most cases, you’ll use event handlers to handle side effects.
For example, you might have an event handler that displays a confirmation dialog after the user clicks a button. Using an event handler explicitly tells React that this code doesn’t need to run during render, keeping render pure. If you’ve exhausted all options – and only as a last resort – you can also handle side effects using useEffect
.
Deep Dive
UI libraries like React take care of when your code runs for you so that your application has a great user experience. React is declarative: you tell React what to render in your component’s logic, and React will figure out how best to display it to your user!
When render is kept pure, React can understand how to prioritize which updates are most important for the user to see first. This is made possible because of render purity: since components don’t have side effects in render, React can pause rendering components that aren’t as important to update, and only come back to them later when it’s needed.
Concretely, this means that rendering logic can be run multiple times in a way that allows React to give your user a pleasant user experience. However, if your component has an untracked side effect – like modifying the value of a global variable during render – when React runs your rendering code again, your side effects will be triggered in a way that won’t match what you want. This often leads to unexpected bugs that can degrade how your users experience your app.
When is it okay to have mutation?
One common example of a side effect is mutation, which refers to changing the value of a non-primitive value. In general, while mutation is not idiomatic in React, local mutation is absolutely fine:
function FriendList({ friends }) {
let items = []; // ✅ locally created and mutated
for (let i = 0; i < friends.length; i++) {
let friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
);
}
return <section>{items}</section>;
}
There is no need to contort your code to avoid local mutation. In particular, Array.map
could also be used here for brevity, but there is nothing wrong with creating a local array and then pushing items into it during render.
Even though it looks like we are mutating items
, the key point to note is that this code only does so locally – the mutation isn’t “remembered” when the component is rendered again. In other words, items
only stays around as long as the component does. Because items
is always recreated every time <FriendList />
is rendered, the component will always returns the same result.
On the other hand, if items
was created outside of the component, it holds on to its previous values and remembers changes:
let items = []; // ❌ created outside of the component
function FriendList({ friends }) {
// Push `friends` into `items`...
return <section>{items}</section>;
}
When <FriendList />
runs again, we will continue appending friends
to items
every time that component is run, leading to multiple duplicated results. This version of <FriendList />
has observable side effects during render and breaks the rule.
Similarly, lazy initialization is fine despite not being fully “pure”:
function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Fine if it doesn't affect other components
// Continue rendering...
}
Side effects that are directly visible to the user are not allowed in the render logic of React components. In other words, merely calling a component function shouldn’t by itself produce a change on the screen.
function ProductDetailPage({ product }) {
document.window.title = product.title; // ❌
}
As long as calling a component multiple times is safe and doesn’t affect the rendering of other components, React doesn’t care if it’s 100% pure in the strict functional programming sense of the word. It is more important that components must be idempotent.
Props and state are immutable
A component’s props and state are immutable snapshots with respect to a single render. Never mutate them directly.
You can think of the props and state values as snapshots that are updated after rendering. For this reason, you don’t modify the props or state variables directly: instead you pass new props, or use the setter function provided to you to tell React that state needs to update the next time the component is rendered.
Don’t mutate Props
When followed, this rule allows React to understand that values that flow from props aren’t mutated when they’re passed as arguments to functions, allowing certain optimizations to be made. Mutating props may also indicate a bug in your app – changing values on the props object doesn’t cause the component to update, leaving your users with an outdated UI.
function Post({ item }) {
item.url = new Url(item.url, base); // ❌ never mutate props directly
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ make a copy instead
return <Link url={url}>{item.title}</Link>;
}
Don’t mutate State
useState
returns the state variable and a setter to update that state.
const [stateVariable, setter] = useState(0);
Rather than updating the state variable in-place, we need to update it using the setter function that is returned by useState
. Changing values on the state variable doesn’t cause the component to update, leaving your users with an outdated UI. Using the setter function informs React that the state has changed, and that we need to queue a re-render to update the UI.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // ❌ never mutate state directly
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ✅ use the setter function returned by useState
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
Return values and arguments to Hooks are immutable
Once values are passed to a Hook, neither the calling code nor the Hook should modify them. Like props in JSX, values become immutable when passed to a Hook.
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
// ❌ never mutate hook arguments directly
icon.className = computeStyle(icon, theme);
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
// ✅ make a copy instead
let newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
The custom Hook might have used the hook arguments as dependencies to memoize values inside it.
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => {
let newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}
Modifying the hook arguments after the hook call can cause issues, so it’s important to avoid doing that.
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon.enabled = false; // ❌ never mutate hook arguments directly
style = useIconStyle(icon); // previously memoized result is returned
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon = { ...icon, enabled: false }; // ✅ make a copy instead
style = useIconStyle(icon); // new value of `style` is calculated
Similarly, it’s important to not modify the return values of hooks, as they have been memoized.