Memoization, useCallback & memo (using them wisely)
In this article, we’ll learn how to implement memoization in React Native and touch base with the major topics around it.
Focus Points of the Blog:
- Memoization and its importance.
- Important concepts related to Pure Component in class and memo in functional components.
- How to implement memoization.
- Different use cases with and without memoization.
Let's get started!
Memoization and Its Importance:
Memoization is just caching the result of an expensive function and returning it when the function is called again with the same input and hence improving the performance significantly.
So, memoization is a great optimization technique that enhances an app's performance significantly when used correctly.
Concepts to Know Before Memoization:
Pure component:- Pure component was introduced in react to minimize the
wasted renders
(wasted renders are the re-renders that occur even when the value of the state is not being changed explicitly). So basically, it implicitly hasshouldComponentUpdate
in PureComponent class in React. So, it does a shallow check If the previous state and props data are the same as the following props or state, the component is not Re-rendered. You can read the article here to learn more about Pure Component.Now, here is a catch: React.PureComponent is only for class components!
Stay with me; I will tell you how to achieve the same in functional components. 😄
- Shallow Comparison: Shallow comparison ("===") is used to compare objects, only comparing the type (numbers, strings) and their value, but it is inefficient in the case of deeply nested objects and may result in false positives. So to sum it up - when comparing objects, it does not compare their attributes only their references are compared.
- Rendering: So rendering of a component occurs whenever there is an update in the props or state of the component. In this process, react picks up the required changes in the ui, current state, and props. But note that the
virtual DOM
still needs to be changed. (You can read more about Virtual DOM here.) - Reconciliation: The process in which a virtual representation of UI is placed in the memory, and the calculation and comparison of the changes are made to be applied to virtual DOM.
- DOM manipulation: Once React finishes the calculations of the changes needed in the application tree, it will apply all the required changes to the DOM. These changes are applied synchronously, and the DOM is updated. This is called DOM manipulation.
- Re-rendering: After the first render, re-render is caused when a prop in a component gets updated, when a state in a component gets updated and when a parent component’s render method gets called.
Since we know the core components, we can jump right back to our topic: Memoization!
We will now quickly discuss how you can implement memoization in 3 aspects:
1. React.Memo():
As I promised earlier, I will tell you a way to achieve shouldComponentUpdate and prevent unnecessary renders in functional components, here is one key to that! React.memo() is a HOC (Higher Order component) that enhances the performance of the functional components.
note:- React.memo(...) is to functional components what React.PureComponent is to class components. So, if the component is supplied with the same state or the same props, it will skip the re-rendering. No rendering, no reconciliation, just reusing the last rendered result.
It does a shallow check so we need to add our comparison function to tell when to update in case of deep nesting.
Let's see an example:
Consider this functional component:
const SimpleComponent = ()=> {
return (
<View>
<Text> I am a Functional component </Text>
</View>
)
}
We pass the SimpleComponent as an argument to React.memo():
const SimpleComponent = ()=> {
return (
<View>
<Text> I am a Functional component </Text>
</View>
)
}
const MemoizedComponent = React.memo(SimpleComponent);
Now consider this parent-child scenario; in this example, I will also tell you why memoization is important:
import React, { useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
const ChildComponent = (props) => {
console.log("Child component rendered");
return (
<View>
<Text>Hi, i am {props?.name} component! </Text>
</View>
);
};
function App() {
const [counter, setCounter] = useState(0);
console.log("Parent component rendered");
return (
<View style={styles.mainContainer}>
<Text>Hi, i am parent! </Text>
<TouchableOpacity
style={styles.button}
onPress={() => setCounter(counter + 1)}
>
<Text>Click me!</Text>
</TouchableOpacity>
<ChildComponent name="child" />
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
button: {
borderWidth: 1,
marginVertical: 10,
paddingHorizontal: 20,
paddingVertical: 10
}
});
export default App;
Here is the above code preview:
So in the above code, every time we press the 'Click me!` button, the child component is rendered as well along with the parent as the parent's state is being updated.
We can see here that, even when there was no actual change in the child component, the new virtual DOM was created, and a difference check was performed. For small React components, this performance is negligible, but for large components, the performance impact is significant. To avoid this re-render and virtual DOM check, we use memoization.
So, now just by adding React.memo()
to the ChildComponent
:
import React, { useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
const ChildComponent = React.memo((props) => {
console.log("Child component rendered");
return (
<View>
<Text>Hi, i am {props?.name} component! </Text>
</View>
);
});
function App() {
const [counter, setCounter] = useState(0);
console.log("Parent component rendered");
return (
<View style={styles.mainContainer}>
<Text>Hi, i am parent! </Text>
<TouchableOpacity
style={styles.button}
onPress={() => setCounter(counter + 1)}
>
<Text>Click me!</Text>
</TouchableOpacity>
<ChildComponent name="child" />
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
button: {
borderWidth: 1,
marginVertical: 10,
paddingHorizontal: 20,
paddingVertical: 10
}
});
export default App;
Voila! the re-renders for the ChildComponent
are gone.
Every time we press Click me!
button, apart from the initial render the parent component only gets rendered.
The Problem with React.memo():
In the above example, we saw that when we used the React.memo()
, the child component didn’t re-render, even if the parent component did. Now, in the above code if we pass a function as a prop to our ChildComponent
, even after using React.memo()
, the child component will re-render!
import React, { useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
const ChildComponent = React.memo((props) => {
console.log("Child component rendered");
return (
<View>
<Text>Hi, i am {props?.name} component! </Text>
</View>
);
});
function App() {
const [counter, setCounter] = useState(0);
console.log("Parent component rendered");
return (
<View style={styles.mainContainer}>
<Text>Hi, i am parent! </Text>
<TouchableOpacity
style={styles.button}
onPress={() => setCounter(counter + 1)}
>
<Text>Click me!</Text>
</TouchableOpacity>
<ChildComponent name="child" fun={() => {}} />
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
button: {
borderWidth: 1,
marginVertical: 10,
paddingHorizontal: 20,
paddingVertical: 10
}
});
export default App;
As you see, we just passed an empty function fun as a prop to our ChildComponent, we didn't even use it anywhere, still, our ChildComponent will re-render with every click.
To solve this issue, we have our second key:
2. useCallback():
The main issue that caused the child to re-render is the recreation of the handler function fun
. To prevent re-rendering of our ChildComponent
we will we will wrap our callback function fun
with useCallback
. So, our useCallback
hook will cache our function fun
and will create only a new function when its dependency changes.
Consider the following example:
import React, { useCallback, useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
const ChildComponent = React.memo((props) => {
console.log("Child component rendered");
return (
<View>
<Text>Hi, i am {props?.name} component! </Text>
</View>
);
});
function App() {
const [counter, setCounter] = useState(0);
console.log("Parent component rendered");
// useCallback
const funHandler = useCallback(() => {}, []); //dependency array
return (
<View style={styles.mainContainer}>
<Text>Hi, i am parent! </Text>
<TouchableOpacity
style={styles.button}
onPress={() => setCounter(counter + 1)}
>
<Text>Click me!</Text>
</TouchableOpacity>
<ChildComponent name="child" fun={funHandler} />
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
button: {
borderWidth: 1,
marginVertical: 10,
paddingHorizontal: 20,
paddingVertical: 10
}
});
export default App;
You can have a look at this preview:-
now with the use of useCallback()
, the ChildComponent
will only re-render, when there is any change in the dependency leading to re-creation of the callback function.
Our third and last key is:
3. useMemo():
useMemo
is similar to useCallback
. The difference being, useMemo
memoize the result of the callback function instead of memoizing a callback itself. We have to pass the dependency list to the useMemo as well
. So whenever the dependencies change it will call the function again and do a recalculation and memoize new value. This can prevent re-rendering of huge functions and significantly increase the app's performance by just caching the result itself.
Consider this below example:
Without useMemo():
keep an eye at the function sumOfNumbers
-
import React, { useCallback, useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
const ChildComponent = React.memo((props) => {
console.log("Child component rendered");
return (
<View>
<Text>Hi, i am {props?.name} component! </Text>
</View>
);
});
function App() {
const [counter, setCounter] = useState(0);
const [numbers, setNumbers] = useState({ Num1: 1, Num2: 2 });
console.log("Parent component rendered");
// useCallback
const funHandler = useCallback(() => {}, []); //dependency array
const sumOfNumbers = () => {
console.log("Sum of numbers function is called");
return numbers.Num1 + numbers.Num2;
};
return (
<View style={styles.mainContainer}>
<Text>Hi, i am parent! </Text>
<TouchableOpacity
style={styles.button}
onPress={() => setCounter(counter + 1)}
>
<Text>Click me!</Text>
</TouchableOpacity>
<ChildComponent name="child" fun={funHandler} />
<Text>Sum of numbers is {sumOfNumbers()}</Text>
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
button: {
borderWidth: 1,
marginVertical: 10,
paddingHorizontal: 20,
paddingVertical: 10
}
});
export default App;
Preview for the code above:
the callback function sumOfNumbers
renders every time with the update in parent component.
with useMemo():
import React, { useCallback, useMemo, useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
const ChildComponent = React.memo((props) => {
console.log("Child component rendered");
return (
<View>
<Text>Hi, i am {props?.name} component! </Text>
</View>
);
});
function App() {
const [counter, setCounter] = useState(0);
const [numbers, setNumbers] = useState({ Num1: 1, Num2: 2 });
console.log("Parent component rendered");
// useCallback
const funHandler = useCallback(() => {}, []); //dependency array
const sumOfNumbers = useMemo(() => {
console.log("Sum of numbers function is called");
return numbers.Num1 + numbers.Num2;
}, [numbers]);
return (
<View style={styles.mainContainer}>
<Text>Hi, i am parent! </Text>
<TouchableOpacity
style={styles.button}
onPress={() => setCounter(counter + 1)}
>
<Text>Click me!</Text>
</TouchableOpacity>
<ChildComponent name="child" fun={funHandler} />
<Text>Sum of numbers is {sumOfNumbers}</Text>
</View>
);
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
button: {
borderWidth: 1,
marginVertical: 10,
paddingHorizontal: 20,
paddingVertical: 10
}
});
export default App;
As you can see, the sumOfNumbers
only gets called once.
Great with this, we have unlocked maximum optimisation with memoization and have significantly improved our app's performance!
That was pretty cool, right? Also huge!
Things to Remember:
Memoization is a great solution for improving performance in apps by eliminating wasted renders of a component. You might think of just adding memoization for all the components, but that’s not a good way to build your React components as no doubt this gives us a huge optimization way, but with a compromise in the RAM usage. You should use memoization only in cases where the component:
- returns the same output when given the same props
- has multiple UI elements, and a virtual DOM check will impact the performance
- is often provided with the same props.
Conclusion:
- Basic react concepts.
- Importance of memoization.
- Important concepts related to Pure Component in class and memo in functional components.
- How to implement memoization.
- Important use cases covering React.memo(), useCallback(), useMemo() and functions with and without memoization.
References:
RN Docs, Pure Components, React Docs
I hope this article helped you in some way! If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email or DM me. Till then stay healthy, keep learning, and keep growing.
Subscribe to my newsletter
Read articles from Ishtjot Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by