Memoization in React
Techniques to optimize react application using memoization
Introduction ๐โโ๏ธ
Hello everyone! Welcome to this blog on Memoization in React where I'll be talking about what is memoization, how, when, and why we need to use memoization.
This is going to be a long one, so grab your glasses, tea/ coffee, snacks and stick with me till the end!
Prerequisutes ๐
- Basic understanding of JavaScript
- Basic understading of React
- useState hook in react
What is memoization? ๐ง
- Memoization is a technique in programming to enhance performance, where computers cache results (called memoize) of long, compute intensive task and use the cached values to calculate results, when the same input is provided again.
- A classic example of memoization is calculating the results of factorial of a number or the nth fibonacci number.
- Understanding how to memoize result of factorial of a number or nth fibonacci is not required to read this blog. But if you are interested, then check this out- Improving efficiency of recursive functions .
Moving on...
Memoization in react
- After reading the definition of memoization, if one of the thoughts that you had was
But what "values" do we exactly have in react to memoize?
...then you are heading in the right direction!
- In react, we deal with the UI and the DOM. What could be the task that is there to optimize using memoization? Well, it would be re-renders.
- So memoization in react is optimizing performance such that we reduce the number of re-renders and make the application faster.
- Now that we have our end goal -
optimize application such that we have very few optimized re-renders
, let's move on to understand the techniques react gives us to minimize renders.
Let's begin!
React.memo
- One of the ways to optimize our application is to use the
React.memo
, which is a higher order component, HOC, that ensures your component renders only and only if the props passed to it changes.
const MemoizedComponent = React.memo(ComponentToBeMemoized);
- To understand how this component works, let us take an example of the very cliched counter.
- First, let's create a new app (I am using code sandbox for this). In the
App.js
component, implement a basic counter with 2 buttons for increasing/ decreasing the counter value and a container to display the count value. - Below is the implementation of the counter. Click on the buttons to increase/ decrease the count and it's working!!
- Next, let us create a child component
User.js
that renders the username we pass to it as props. - For passing the name, let us create an input in
App.js
. Whatever text user inputs should be passed as props to the child. If the username is empty, a default name is rendered.
Below is the implmentation:
I have also added a
console.log()
before thereturn()
statement in both the components to show that the respective components ran.
NOTE: Since the codesandbox is in the strict mode, everything runs twice to ensure that there are only pure updates.
Now that the basic set up is done, let's move ahead.
- Notice that despite no change in the username, when the counter value is changed, the child
User.js
is re-rendered. But we really don't want that. We only want the
User
component to be rendered when there is a change in the username.So, let's avoid this. Shall we?
- The solution is very simple. We pass the component that needs to be memoized to
React.memo()
. This returns the memoized component back that is optimized for performance. - So this means
const MemoizedUser = React.memo(User);
Below is codesandbox that renders the memoized component
Now increase/ decrease the counter... and it works! The
User
component runs only during the initial render and thenThat's it we did it! Yaay! ๐ฅณ
This is a very basic example, but then think of a component that takes a prop and does some very intensive task, (say factorial of a number!!) and is rendered everytime even when the prop remains unchanged.
So what happened here? Let's break down.
- The
memo()
HOC ensures that the component it returns is only re rendered when there is a change in the props. - So in our example, the memoized version of the
User
component ran during the initial render. Then it ran only when we changed the username. - Even when the parent component re-rendered (when the counter value changed), the
User
component did not since the props passed to it remained same.
NOTE : memo() checks only if the previous props is equal to the current props. If the props changes, the component is rendered.
But if the component has a state or is wrapped within a provider, it will still be rendered if there's a change in state, despite no change in props.
- You can also pass your very own function as second arguments to memo, which uses the function passes to compare the new and previous props.
const ExampleComponent = (props) => {
// The component that needs to be memoized
}
const memoizedComponent = memo(ExampleComponent, (prevProps, newProps) => {
// logic to return if the props are equal or not
});
So the below would be a custom implementation of
propsAreEqual
for theUser
component example (It does the same thing!) -const MemoizedUser = memo( User, (prevProps, newProps) => prevProps.username === newProps.username );
So this was all about
memo
! It is a very good technique to improve optimization, but you should know when to and not to use.React.memo()
should not be used just because you want to avoid re-render.- So when should you use it?
- When your components contains a good number or elements as well as computations, and the props passed to it often remain unchanged.
- You can also use
memo
to improve the efficiency when most of the props remain the same but only certain props change frequently.
- It is also worthwile to note here that because of the virtual DOM, the
reconcilliation
anddiffing
process, DOM updates are already pretty fast (as the entire DOM is not repainted, rather only the root elements where changes are detected).React.memo
only speeds up this process. - Also, if any component receives different props every time, then memoization isn't the choice for that component.
- So, you should avoid (at all cost) to do premature optimizations. Do this only when necessary.
useMemo()
- React provides another hook,
useMemo()
that helps with memoization and improving performance. - Whice
React.memo()
memoizes components, useMemo() memoizes value.const memoizedValue = useMemo(() => { // returns new, re calculated value when deps passed within [] changes }, [])
- Now, we'll try to understand this hook with an example. We will use the initial setup for
App
component from the previous example. - Let us create 2 other components:
Todos
andTodoItem
.Todos
component is responsible for fetching the todo items, and filtering them.TodoItem
takes every todo as a prop and displays each item. - Inside the
Todos
component, we also have agetFilteredTodos()
function that returns the filtered todos (if any applied). Like in the previous example, I have also added a
console.log()
before the return ofApp
,TodoItem
and one within thegetFilteredTodos()
as well.Below is the basic implementation
- Try clicking on the buttons to see if both counter and filtering is working. Yayy! It does.
Did you notice how when you click on the buttons to change the counter values, the
getFilteredTodos()
function still runs and filters?But that is insane, isn't it? Why do we need to run the
getFilteredTodos()
when the previous filter hasn't even changed! Yikess.So what's the way around this? Can we avoid this?
- Yes, we can! ๐ช
useMemo()
FTW! - We know the end goal
get the filtered todos only when- either the list of todos changes or the filter applied changes
. - And we saw earlier in the syntax above that
useMemo()
takse a callback that computes the new value when a bunch of dependency passed to it changes! - You see where I'm going with this??
- Yes, that's what we need to do. Pass
getFilteredTodos
as a callback touseMemo()
and addtodos
,filter
as dependency.
const memoizedTodos = useMemo(getFilteredTodos, [filter, todos]);
And, that's it. One line brings in a whole lot of optimization!
- Below is the implementation
Let us take a step back and analyze.
- When any of the dependencies passed to
useMemo()
changes, the callback function is called and the new value is computed. - In our example, we passed the
getFilteredTodos()
as a callback touseMemo()
and addedtodos
,filter
as a dependency. So anytime the
filter
applied changes or thetodo
list changes, thegetFilteredTodos()
is called and the new state of todos are returned.Even if you say click on the same filter multiple times, the
getFilteredTodos()
is not called since the previous filtered value has been memoized!- This is crazy huge. Think of a huge application (like an e-commerce platform) that contains multiple filters. If for every re-render the filtering was performed again for the same filters, then imagine how slow the app would be.
- But then again since the values are memoized, it might still cost in terms of space. So, if you are using
useMemo
, think of the tradeoffs and see if you even have to memoize a value between re-renders.
useCallback()
useCallback()
is the final hook I will be covering as a part of this blog.- While
memo()
memoizes a component,useMemo()
memoizes a value,useCallback
memoizes a function. - It takes a callback function as the first argument, and an array of dependencies as the second argument.
useCallback
memoizes a function and re-creates the function provided as callback only when any of the dependency value changes. - This hook is very useful when you are passing callbacks as props to children component and want to optimize re-renders.
const memoizedCallback = useCallback(() => {
// callback function that is memoized unless one of the deps inside [] changes
}, []);
- Let us try to understand this hook with an example. Let's modify the todos example we used above and move ahead.
- Below is the starter code. I have removed the filters, and the
getFilteredTodos()
. - And for ease of explanation, I have rendered only 5 todo items and memoized the
TodoItem
component to prevent unnecessary renders.
Click on the counter buttons to and check if it works. Yaay it does!
Now, let us add a handler to the list items that displays the id and title of the clicked item below the todos list.
- Here's the sandbox.
Notice how, if you click any of the items or increase/ decrease, the log says
TodoItem ran...
.But how is that possible? We memoized the component right? And the props passed remain constant.
The component is re-rendered despite being memoized because of the callback that was passed as props to the component.
Functions are reference types. Let us see what this means-
const incrementOne = n => n+1;
const incrementTwo = n => n+2;
console.log(incrementOne === incrementTwo); // false
console.log(incrementOne === incrementOne); // true
console.log(incrementTwo === incrementTwo); // true
- Unlike primitive values, objects (reference types) are only equal to themselves. This is because when checking for equality of reference types, they are considered equal based on the memory address and not just the value.
Hence even though the callback passed to the
TodoItem
component essentially is the same code, they are referentially different.So how can we prevent this? If we have a huge list of n items, it makes no sense for n different references of the same function to be created.
useCallback
to the rescue. ๐ชI already explained the syntax that
useCallback
takes a callback that is created only when the values in the dependency array changes.- If the dependencies remain the same, then the memoized callback is used.
- That is exactly what we shall be doing.
const memoizedCallback = useCallback(itemId => {
setClickedTodo(itemId);
}, [])
- That's all! We are all set now. ๐ฅ
- Try clicking the counter buttons as well as the todo items. You'll see that the
TodoItems
are rendered initially, and then they aren't re-rendered.
- Let's take a step back and understand.
- The callback we pass to
useCallback
is created during the initial render and is memoized. Now since we haven't passed in any dependencies, the same memoized callback is passed on as props to all the todo items whenever the parent component changes. - And since we have memoized the
TodoItem
component and the props remain the same, the item is not re-rendered! - And this is how we have avoided the re-creation of the same callback for all the children whenever parent component changes!
What about dependencies?
- Adding the right dependencies is very very important. Because sometimes you will want React to re-create the memoized value (or function in the case of
useCallback()
). - If you have used
useEffect()
, then you might know the importance of deps! - In the example for
useMemo()
, I added[filter, todos]
as dependencies.
const memoizedTodos = useMemo(getFilteredTodos, [filter, todos]);
- Do we even need this? Yes, but why?
- Well, think about this. The entire point of adding dependencies is to tell react when to (thus when not to) re-create the todos value.
- If you remove
filter
from the deps list, whatever filter you apply (no matter how many times), thememoizedTodos
will not be recreated because according to React, the todos have not changed, hence todos would be the same. Hence, the filtered list will never be returned. - Similarly, if you remove
todos
from the dependencies after the initial render, the todos will be fetched. But sincegetFilteredTodos
is only dependent on change in filters, the function would not even be executed after the first render! So all you'll get is an empty list of memoizedTodos!
Play around here for useMemo example
- Below is another codesandbox link you can play around with that demonstrates the need for deps in
useCallback
. Play around here for useCallback example
As a rule of thumb, if your memoized value (or function) depends on any input, then add it inside the deps. Be truthful to react and you'll be saved from a lot of (unnecessary) debugging!
That's all for this blog. Phew. That was long. I know. But trust me these topics are really good (and important) enough to spend time.
- I was surprised when I found a use case to add
useCallback
while I was working on a recent project. - I added debouncing to the search feature and had to memoize the callback that delayed generating the search results until user stoped typing!
- Special thanks and mention to Rohan Mathur who helped me debug the code and explained about this!
TL;DR ๐
Let's do a quick recap, shall we?
React.memo
is a HOC that memoizes a component and renders only if the props passed to it changes. It does a shallow compare of the current props with the old props.useMemo()
is a hook that memoizes a value, and re-calculates the new value using the callback passed to it as the first argument, only if any values in the dependency array changes.useCallback()
is a hook that memoizes a function passed as a callback to it, and re-creates the callback only if any of the values passed as dependencies changes.- It is important to tell react when to re-create the memoized value (or function) by adding the right dependency values.
Closing Thoughts ๐ญ
- Like everything else in life, overdoing performance optimization is also a caveat. There's a fine gray area between premature optimization and actual optimization.
- Also there are other techniques worth checking out to optimize performance in react. Check out this page from the docs.
- While the techniques mentioned above optimize by eliminating unnecessary re-renders, they should not be used to prevent re-rendering of components.
- I know this was a lot, so it is okay if you did not understand everything. Even I did not. I looked around at blogs, videos, and documentation to have if not full, a basic understanding of how you memoize in react.
And thank you so much for sticking with me till the end. ๐ If this blog helped you then do drop your favorite reaction(s) and tell me what you liked or any feedback you want me to work on. Special shoutout to Vinayak Goyal for helping me with the resources! This blog wouldn't have been possible without that.