When you're starting out as a new developer, you just want to get things to work without any bugs, however as your projects grow and become more complex you need to start thinking about optimizations to ensure a smooth experience for users.
The main way to optimize a React app is to avoid components re-rendering unnecessarily. You only want a component to re-render if there is a change to what is shown to the user, however in an app that hasn't been optimized you will find components re-rendering just to render the exact same things as before. We can increase the performance of our apps by avoiding components re-rendering in these cases.
What causes a component to re-render?
A React component re-renders both when its own state or props change, and when any parent component re-renders. When React re-renders a component, it also re-renders every child component to be on the safe side as it can't be completely sure that it is a pure component. A pure component is a component that will always have the exact same output given the same inputs (state and props), i.e. it has no side effects. This means that when we have nested components, we may be unnecessarily re-rendering child components when it's a pure component and neither its state or props have actually changed.
How to check if a component has re-rendered?
The simplest way is just to use a console.log inside the main function body (I will be using functional components only throughout this article). If you want more insight, you can use the React developer tools to see which components have re-rendered and more importantly what caused the re-render.
import React, {useState} from 'react';
function Child() {
console.log("rendering child");
return (
<div>
<p>Child</p>
</div>
)
}
export default function App() {
const [parentState, setParentState] = useState(0);
const updateParent = () => {
setParentState(parentState+1);
}
console.log("rendering parent");
return (
<div>
{parentState}
<button onClick={updateParent}>Update Parent</button>
<Child />
</div>
)
}
In the example above, the child component is clearly a pure component, it has no state and no props. However, every time we click the update parent button, we will see that both the parent and child components have re-rendered.
React.memo
If we know that a child component is pure, we can stop React from re-rendering by wrapping it with React.memo.
const Child = React.memo(()=>{
console.log("rendering child");
return (
<div>
<p>Child</p>
</div>
)
})
Now, when we update the parent state and check the console, we will see that the child component has not re-rendered.
useCallback
To understand why to use the useCallback hook, you must first understand Javascript referential equality. Javascript objects are equal if they point to the same object in memory. If their contents are exactly the same but they point to different objects in memory, then they are not equal. Read this article for more information on equality in Javascript, https://www.geeksforgeeks.org/what-is-object-equality-in-javascript/ .
If we want to update the parent state from the child component, we can easily do so using props to pass the updateParent function to the child.
React.memo(function Child({updateParent}) {
console.log("rendering child...");
return (
<div>
<button onClick={updateParent}>Update Parent</button>
</div>
)
})
export default function App() {
const [parentState, setParentState] = useState(0);
const updateParent = () => {
setParentState(parentState+1);
}
console.log("rendering parent");
return (
<div>
{parentState}
<Child updateParent={updateParent} />
</div>
)
}
However, each time the parent component updates it will create a new updateParent function. This means that as far as React is concerned, the Child component's props have changed and so despite wrapping the child in React.memo, it will still re-render both the parent and child.
In this case, we can use the useCallback hook to wrap the updateParent function. This causes the updateParent function to maintain the same pointer between re-renders, therefore the child component's props have no longer changed. The useCallback hook also has a dependency array. A new function will be created if any of the values within the dependency array change.
const updateParent = useCallback(() => {
setParentState(parentState=>parentState+1);
}, [])
When using useCallback, remember to always set the state using a callback to avoid a stale reference for parentState.
setParentState(parentState+1)
Would get stuck at 1.
useMemo
The useMemo hook is very similar to the useCallback hook, except that it returns the value from the function rather than the function itself. It will recalulate the value if any of the values in its dependency array change.
Sometimes people use the useEffect hook when they should be using useMemo instead, leading to a component rendering twice.
const [parentState, setParentState] = useState(0);
const [derivedState, setDerivedState] = useState(0);
useEffect(()=>{
setDerivedState(parentState*10)
}, [parentState])
If we use useEffect here, the component will render once when parentState changes, then useEffect will fire, then the component will render again as we update the derived state inside useEffect.
const [parentState, setParentState] = useState(0);
const derivedState = useMemo(()=>{
return parentState*10;
}, [parentState])
Now, by using useMemo, the component will render once when parentState changes and re-calculate derivedState within the same render, therefore avoiding rendering the second time.
Top comments (0)