In this article, you will understand the actually use case of React.memo() , useMemo() and useCallback()
UseMemo()
- The value returned is stored in a cache, so that next time you can directly retrieve it from the cache.
- The
useMemo
Hook can be used to keep expensive, resource-intensive functions from needlessly running. - The
useMemo
Hook only runs when one of its dependencies is updated, hence performance increases.
import { useState, useMemo } from "react"; const UseStateHook = () => { const [counterOne, setCounterOne] = useState(0); const [counterTwo, setCounterTwo] = useState(0); const incrementOne = () => { console.log("one"); setCounterOne(counterOne + 1); }; //the expensive function is kept inside useMemo , and it onyl work when "counterOne" value changes const isEven = useMemo(() => { let i = 0; console.warn("hllo"); while (i < 2000000000) i++; return counterOne % 2 === 0; }, [counterOne]); const incrementTwo = () => { console.log("two"); setCounterTwo(counterTwo + 1); }; //useMemo - return value //useCallBack - return entire function console.log(isEven); //this is expesive fucntion which is making the entire DOM rendering slow // function isEven() { // let i = 0; // console.warn("hllo"); // while (i < 2000000000) i++; // return counterOne % 2 === 0; // } return ( <> <button onClick={incrementOne}>Counter One - {counterOne}</button> //<p> {isEven() ? "even" : "odd"}</p> <p> {isEven ? "even" : "odd"}</p> <button onClick={incrementTwo}>Counter Two - {counterTwo}</button> </> ); }; export default UseStateHook;
In the above example –
- isEven() was an expensive function, hence it was affecting the entire DOM performance even though is not related to the other logic.
- So isEven is wrapped inside useMemo() and given a condition that this will only run when “counterOne” value changes,
- Now Counter Two button performance is not affected, hence entire dom performance increased
useCallBack()
You can copy the above function and replace “useMemo” with “useCallback” , and the function will work fine.
The main difference is that useMemo returns a memoized value and useCallback returns a memoized function.
But for your better understanding, we will be dealing with a new example for useCallback .
Before writing code , you will notice we will also use React.memo() with useCallback , so let’s quickly understand this first.
React.memo()
memo() usually stops the re-rendring of the component if the props or state have not changed of that particular component
Some key points –
- Improves performance as it restricts unnecessary re-rendering of the component.
- Memoize(cache) a component hence faster reload
How to write memo()
To use memo , We usually wrap the export component within memo function
import React from "react"; const Title = () => { console.log("title component render"); return <div>Title</div>; }; export default React.memo(Title);
Now lets understand useCallback() with a code
Create 3 components –
Step 1 – Create 3 Compponent
- ParentComponent.jsx
- Button.jsx
- Count.jsx
Here the <ParentComponent /> will wrap the <Button/> and <Count/> Component component
Needless to say, the main <App /> component will be wrapping the <Parent/> component
Demo –
ParentComponent.jsx ------------------ <ParentComponent> <Count/> <Button/> <ParentComponnt> App.jsx --------- <App> <ParentComponent/> </App>
Now that you understood the heirarchy , let write some code
ParentComponent.jsx –
import { useState } from "react"; import Count from "./Count"; import Button from "./Button"; const ParentComponent = () => { const [age, setAge] = useState(25); const [salary, setSalary] = useState(5000); const incrementAge = () => { setAge(age + 1); }; const incrementSalary = () => { setSalary(salary + 1000); }; return ( <> <Count text="age" count={age} /> <Button handleClick={incrementAge} count={age}> Increase age </Button> <Count text="salary" count={salary} /> <Button handleClick={incrementSalary}>Increase Salary</Button> </> ); }; export default ParentComponent;
Button.jsx –
import React from "react"; const Button = ({ handleClick, children }) => { console.log("button component render ", children); return ( <div> <button onClick={handleClick}>{children}</button> </div> ); }; export default Button;
Count.jsx –
import React from "react"; const Count = (props) => { console.log("count component render", props.text); return ( <div> {props.text} - {props.count} </div> ); }; export default Count;
Let,s see the output of the above code –
On first render , all the 4 elements will be printed , as we have consoled all the elements
Why 4 times ?
-----------
<Count/> //for age
<Button>Increase Age</Button>
<Count/> //for salary
<Button>Increase Salary</Button>
Now if I click only on “Increase Age” button , you will see all the 4 elements again got re-rendered.
Similar thing happens , if I click on “Increase Salary” button
Well! to prevent this we can use React.memo() , which help in stopping the render of those component, whose props have not changed.
So let’s add that.
Button.jsx
import React from "react"; const Button = ({ handleClick, children }) => { console.log("button component render ", children); return ( <div> <button onClick={handleClick}>{children}</button> </div> ); }; export default React.memo(Button);
Count.jsx
import React from "react"; const Count = (props) => { console.log("count component render", props.text); return ( <div> {props.text} - {props.count} </div> ); }; export default React.memo(Count);
We don’t need to add React.memo() in the parent component.
Now lets see the output –
Wee now you can see, as we click on increase age” button ,the <Count /> component render only once i.e only for age. This is what we wanted, But you can see the button for increase salary also render , which is not as expected.
Why is this happening?
In JavaScript, functions are objects, and like other objects, they are compared by reference, not by value. This means that two function expressions or declarations will only be equal if they refer to the same function object in memory, not just because they have the same body or behavior.
In the above example, even though dog1
and dog2
do the exact same thing, they are not considered equal because they are two separate function instances in memory.
const dog1 = function(){console.log('14/10')}; // has a unique object reference const dog2 = function(){console.log('14/10')}; // has a unique object reference console.log(dog1 == dog2); // false console.log(dog1 === dog2); // false
How to fix them
If you want two variables to be considered equal when comparing them, they must refer to the same function object. You can achieve this by assigning the same function reference to both variables.
Here, dog1 and dog2
both point to the same function, so the comparison returns true
//Improving the above function with callback : const dog1 = () => console.log("14/10"); const dog2 = () => console.log("14/10"); const doSomething = (cb) => { cb(); }; doSomething(dog1); doSomething(dog2); console.log(doSomething(dog1) === doSomething(dog1));
We can achive the same thing in react by using useCallback
function MyComponent() { // handleClick is re-created on each render const handleClick = () => { console.log('Clicked!'); }; // ... }
In the above function, on every render a new handleClick function is created because inline functions are cheap, the re-creation of functions on each rendering is not a problem.
That’s when useCallback(callbackFun, deps)
is helpful: given the same dependency values deps
, the hook returns the same function instance between renderings (aka memorization), meaning the function will have the same “referential equality”.
import { useCallback } from 'react'; function MyComponent() { // handleClick is the same function object const handleClick = useCallback(() => { console.log('Clicked!'); }, []); // ... }
Now lets add useCallback() to our functions
ParentComponent.jsx
import { useState, useCallback } from "react"; import Count from "./Count"; import Button from "./Button"; const ParentComponent = () => { const [age, setAge] = useState(25); const [salary, setSalary] = useState(5000); const incrementAge = useCallback(() => { setAge(age + 1); }, [age]); const incrementSalary = useCallback(() => { setSalary(salary + 1000); }, [salary]); console.log("reference", incrementAge === incrementSalary); //false return ( <> <Count text="age" count={age} /> <Button handleClick={incrementAge} count={age}> Increase age </Button> <Count text="salary" count={salary} /> <Button handleClick={incrementSalary}>Increase Salary</Button> </> ); }; export default ParentComponent;
Noe finally take a deeper breath and lets now see the output,
And hurray!!! We finally got the right output. Only the component related to increase age button got rendered.
We have successfully restricted our component for un-necessary re-render and increased the performance of our website.
Note - It is not advisable to apply useCallback() all the time, so always discuss with your team and then apply. Read this
Another example to understand useCallback()
Let understand the issue in the below code –
The “handleText” function in the App.jsx has no relation with the users object , still whenever we change the input data , the setText state updated and hence all the child (List component in this case) will be re-rendered.
Nothing to worry here because this is how react works. Whenever we change a parent state , the child component also re-render. This is generally fixed by React.memo()
Note : React.memo() is not linked with useMemo().
List.jsx
import React from "react"; const List = ({ users, onRemove }) => { console.log("rendering list"); return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} <span onClick={() => onRemove(user.id)}>X</span> </li> ))} </ul> ); }; export default React.memo(List);
App.jsx
import { useCallback, useState } from "react"; import List from "./components/List"; const initialUsers = [ { id: "1", name: "foo", }, { id: "2", name: "bar", }, { id: "3", name: "loo", }, ]; const App = () => { const [users, setUsers] = useState(initialUsers); const [text, setText] = useState(""); const handleRemove = (userId) => { console.log("handleRemove", userId); const filteredUsers = users.filter((user) => user.id !== userId); setUsers(filteredUsers); }; //this function has no relation with the users object const handleText = (event) => { setText(event.target.value); }; return ( <div> <input type="text" value={text} onChange={handleText} /> <List users={users} onRemove={handleRemove} /> </div> ); }; export default App;
But hope you have seen till now, the issue is still not fixed.
Its because , Every time we change the input , the App component re-renders , and hence everytime a new handleRemove() function is created in react , and that in turn is calling the List component again and again.
The fact that here React.memo() also failed is because memo() always compares the earlier state change with the new one, if anything is changes , it will re-render.
And here since every time a new inline function is created , everytime React.memo() finds a new function and hence, it initiated a new re-render.
Now to fix this we need to tell react to cache the function ,or basically keep the same reference of the function every time it is called, and that’s when useCallback() comes into picture.
Just wrap the handleRemove() function with callback
const handleRemove = useCallback( (userId) => { console.log("handleRemove", userId); const filteredUsers = users.filter((user) => user.id !== userId); setUsers(filteredUsers); }, [users] );
Conclusion
React.memo is a higher-order component (HOC) that prevents a functional component from re-rendering if its props haven’t changed.
useMemo memoizes the result of a function, preventing it from being recalculated on every render unless its dependencies change.
useCallback memoizes a function, preventing it from being recreated on every render unless its dependencies change.