When you should use useCallback, useMemo React JS
When you should use useCallback, useMemo React JS
A common mistake React devs make is utilizing useState
for every mutable value they need to persist between renders. useState
is a good solution if the rendered output depends on the value, otherwise useRef
would be a more optimal solution.
Consider the following example:
const [firstName, setFirstName] = useState();
return (
<form onSubmit={() => alert(firstName)}>
<input onChange={(e) => { setFirstName(e.target.value) }} />
<button>Submit</button>
</form>
);
In this example every time the user types, the firstName
state is updated. When a state is updated a re-render is triggered, meaning a re-render is happening every time the user types.
Since firstName
isn’t being used in the rendered output we can replace it with useRef
, and prevent the re-rendering.
const firstName = useRef();
return (
<form onSubmit={() => alert(firstName.current)}>
<input onChange={(e) => { firstName.current = e.target.value}}/>
<button>Submit</button>
</form>
);
memo
One of the most important concepts to understand for optimizing React is memoization. Memoization is the process of caching the results of a function, and returning the cache for subsequent requests.
Re-rendering a component simply means calling the component’s function again. If that component has children components it will call those components’ functions, and so on all the way down the tree. The results are then diffed with the DOM to determine if the UI should be updated. This diffing process is called reconciliation.
Since components are just functions though, they can be memoized using React.memo()
. This prevents the component from re-rendering unless the dependencies (props) have changed. If you have a particularly heavy component then it is best to memoize it, but don’t memoize every component. Memoization uses memory and can be less performant in certain cases.
When a component is memoized, instead of re-rendering it, React diffs the component’s new props with its previous props. The trade off that needs to be considered here is how intensive it is to compare the props vs running the function. If you have a large object in your props, it could be less performant to memoize that component.
const HeavyComponent: FC = () => { return <div/>}
export const Heavy = React.memo(HeavyComponent);
useCallback
An important tool to prevent components that are memoized from re-rendering needlessly is useCallback
. When passing a function into a memoized component you can unknowingly remove the memoizing effect by not memoizing that function using useCallback
. The reason for this is referential equality. As mentioned previously, every re-render calls a component’s function. This means if we’re declaring a function in the component, a new function is created
every re-render. If we are passing that function as a prop to another component, even though the contents of the function do not actually change, the reference changes which causes the child component to re-render, even if it is memoized.
export const ParentComponent = () => {
const handleSomething = () => {};
return <HeavyComponent onSomething={handleSomething} />
};
In this example every time ParentComponent
is re-rendered, HeavyComponent
will re-render as well, even though it is memoized
. We can fix this by using useCallback
and prevent the reference from changing.
export const ParentComponent = () => {
const handleSomething = useCallback(() => {}, []);
return <HeavyComponent onSomething={handleSomething} />
};
ℹ️ It is important to know when to use useCallback, and when not.
useMemo
By now we know that every re-render means the component’s function is getting called
again. This means if your Component Function includes a call to an expensive function — that expensive function is being called every re-render. To avoid running the expensive function every re-render you can memoize it. The first render will call the function, and following re-renders will return the cached results of the function, rather than running it again.
The useMemo
hook makes implementing memoization very simple:
const value = useMemo(() => expensiveFunction(aDep), [aDep]);
In our example value will be cached, and only updated when aDep
changes.
ℹ️ It is important to know when to use useMemo, and when not.
useState lazy initialization
A lesser known feature of useState
is the ability to lazily set the initial state. If you pass a function to useState
it will only call the function when the component is initially rendered. This prevents the initial value from being set on every re-render, which is useful when your initial state is computationally heavy. If your initial value is not computationally heavy, lazy initialization is not recommended.
const initialState = () => calculateSomethingExpensive(props);
const [count, setCount] = useState(initialState);
ℹ️ Read more about useState lazy initialization
Conclusion
If this article was helpful to you, or if you know any tips to optimize performance in React, leave a message and let me know. 🙂