Memoization, not memorization, is a technique commonly used in functional programming to improve performance of an application. To put it simply, memoization is just caching the result of a function, then whenever that function is invoked again, as long the inputs are the same, the cached result can be returned instead of re-running the function. It is important to note that the memoized function would need to be a pure function. That is:
The returned value is always the same for an identical set of inputs.
The function does note mutate state outside of its scope, i.e., there are no side-effects.
React, which heavily encourages the use of a functional style of programming, naturally provides its own mechanisms for developers to optimize their application through memoization. With React.memo(...)
, useCallback(...)
, and useMemo(...)
, developers can have finer control over their application to prevent unnecessary re-rendering of components or heavy recalculations of data.
React.memo(...)
The purpose of React.memo(...)
is to prevent unwanted re-rendering of a child component if its props have not changed when the parent component is re-rendered.
The Problem
Consider this <Parent/>
component. It contains a button that, when clicked, increments a total
state variable and it displays the current value of total
. Lastly, it contains a <Child/>
component that does not take any props.
import { useState } from "react"
import Child from "./Child"
const Parent = () => {
const [total, setTotal] = useState(0)
return (
<div className="flex flex-col gap-1 w-1/5">
<h1 className="text-3xl">Parent Component</h1>
<div>Total: {total}</div>
<button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
<div className="mt-3" />
<Child />
</div>
)
}
export default Parent
The <Child/>
component only displays how many times it has been rendered. There's a useRef(...)
paired with a useEffect(...)
that keeps track of the number of renders for the component, and it displays that count.
import { useEffect, useRef } from "react"
const Child = () => {
console.log("Child Rendered")
const renderCount = useRef(1)
useEffect(() => {
renderCount.current = renderCount.current + 1
})
return (
<div>
<div className="text-3xl">Child Component</div>
<div>Render Count: {renderCount.current}</div>
</div>
)
}
export default Child
All together these two components create this UI
Let's see what happens when we press the button to increment the total
state variable of the parent component.
The child component re-renders with the parent component. This seems odd, especially since the child component's props are not changing. In fact, it does not even have any props.
The Solution
Fortunately, the solution is simple; wrap the child component in React.memo(Component, proprsComparatorFunction)
. Now when the parent component re-renders the child component does not.
import { useEffect, useRef, memo } from "react" // import
const Child = () => {
...
}
export default memo(Child) // wrapping component
The Comparator Function
React.memo(...)
has a second argument for a comparator function, which wasn't utilized in the example. By default, if no function is specified, then React will do a shallow comparison of the previous and new prop values. A shallow comparison is fine if using simple data types like comparing numbers or booleans, but if the prop is something more complex like an object, then a custom implementation might be necessary.
useCallback(...)
The purpose of useCallback(...)
is to memoize a function that's defined in the component. If a function is defined in a component, then it will be recreated during each re-render. useCallback(...)
can prevent his. The benefit to stopping the recreation of the function is exemplified best when functions are being passed down as props to child components.
The Problem
Consider the same <Parent/>
and <Child/>
components from before with the same React.memo(...)
solution used to prevent re-renders. Except this time <Child/>
defines a prop for a callback function, and <Parent/>
defines a function called myFunction
and passes it down to <Child/>
. Also, aside from defining the callback prop, <Child/>
doesn't even do anything with it.
import { useState } from "react"
import Child from "./Child"
const Parent = () => {
const [total, setTotal] = useState(0)
const myFunction = () => {
console.log(`NON-Callback function called`)
}
return (
<div className="flex flex-col gap-1 w-1/5">
<h1 className="text-3xl">Parent Component</h1>
<div>Total: {total}</div>
<button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
<div className="mt-3" />
<Child callback={myFunction} />
</div>
)
}
export default Parent
import { memo, useEffect, useRef } from "react"
const Child = ({ callback }: { callback: () => void }) => {
console.log("Child Rendered")
const renderCount = useRef(1)
useEffect(() => {
renderCount.current = renderCount.current + 1
})
return (
<div>
<div className="text-3xl">Child Component</div>
<div>Render Count: {renderCount.current}</div>
</div>
)
}
export default memo(Child)
Again, let's see what happens when we change state in the parent component.
All of React.memo(...)
's hard work is undone. Although, React.memo(...)
is doing exactly what it's supposed to be doing, but there are two problems:
Each re-render of
<Parent/>
,myFunction(...)
gets recreated completely, and occupies a different space in memory, even though nothing has changed about it.React.memo(...)
is still doing a shallow comparison, but since the old function prop and the new function prop are not referencing the same spot in memory, it thinks that the prop is changed, so<Child/>
needs to be re-rendered. Remember functions in JavaScript are objects and objects are compared by reference.
The Solution
Again, the solution is simple; stop recreating the function and just use the old function by using the useCallback(fn, dependencyList)
hook. The first argument is the function to define whatever work needs to be done, and the second argument, similar to useEffect(...)
, is for a list of state variables that useCallback
will check. As long as none of the state dependencies in the list change, then on every render the same function will be returned instead of creating a new one. Now since the function is the exact same, even from a referential perspective, React.memo(...)
's shallow comparison will not trigger a re-render of <Child/>
import { useCallback, useState } from "react"
import Child from "./Child"
const Parent = () => {
const [total, setTotal] = useState(0)
const myFunction = useCallback(() => {
console.log(`Callback function called`)
}, [])
return (
<div className="flex flex-col gap-1 w-1/5">
<h1 className="text-3xl">Parent Component</h1>
<div>Total: {total}</div>
<button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
<div className="mt-3" />
<Child callback={myFunction} />
</div>
)
}
export default Parent
useMemo(...)
Lastly, useMemo(...)
is used to cache the result of a function. When rendering involves calling some function that produces some value, and the function is doing an expensive calculation but no inputs to the function are changing, then there's no point in the recalculation during each re-render.
The Problem
Continuing from the last code snippet, let's add a new state variable called myName
, a function called uppercaseName
, and then in the UI let's add a text field and a <p>
tag to display the uppercase name.
import { useCallback, useState } from "react"
import Child from "./Child"
const Parent = () => {
const [total, setTotal] = useState(0)
const [myName, setMyText] = useState("michael scott")
const myFunction = useCallback(() => {
console.log(`Callback function called`)
}, [])
const uppercaseName = () => {
console.log("Processing Name...")
return myName.toUpperCase()
}
return (
<div className="flex flex-col gap-1 w-1/5">
<h1 className="text-3xl">Parent Component</h1>
<div>Total: {total}</div>
<input className="border-2 p-1" type="text" value={myName} onChange={(e) => setMyText(e.target.value)} />
<button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
<p>{`Hello, this is ${uppercaseName()}`}</p>
<div className="mt-3" />
<Child callback={myFunction} />
</div>
)
}
export default Parent
Typing something in the text field will display an uppercase version in the paragraph below the button.
However, if we open the console and start using the button to increment the count, we can see the function is being ran each time during the re-render even though the name has not changed.
The Solution
We can use useMemo(...)
to wrap the uppercaseName
function in a similar way we used useCallback(...)
. Except this time the result of useMemo(...)
is a value, and not a function. Like useCallback(...)
, it takes a dependency list, and as long as the state in the dependency list does not change, the result does not have to be recalculated.
import { useCallback, useState } from "react"import Child from "./Child"const Parent = () => { const [total, setTotal] = useState(0) const [myName, setMyText] = useState("michael scott") const myFunction = useCallback(() => { console.log(`Callback function called`) }, []) const uppercaseName = useMemo(() => { console.log("Processing Name...") return myName.toUpperCase() }, [myName]) return ( <div className="flex flex-col gap-1 w-1/5"> <h1 className="text-3xl">Parent Component</h1> <div>Total: {total}</div> <input className="border-2 p-1" type="text" value={myName} onChange={(e) => setMyText(e.target.value)} /> <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button> <p>{`Hello, this is ${uppercaseName}`}</p> <div className="mt-3" /> <Child callback={myFunction} /> </div> )}export default Parent
Over Optimization
After learning about these optimization techniques it may be tempting to start using them everywhere. However that might be overkill for most scenarios. In fact, the React documentation advises to only use these techniques for specific scenarios. However, it is ultimately up to you to decide what is best for your application and when to use what and where to use it.
Should you add React.memo
everywhere?
Should you add useCallback
everywhere?
Should you add useMemo
everywhere?
Conclusion
React provides its own mechanisms to developers to optimize their application through memoization.
Through the use of React.memo(...)
we can limit the number of re-renders of a child component when the parent component re-renders. React.memo(...)
will memoize the child component and only re-render if its props have changed.
Utilizing useCallback(...)
we can prevent the unnecessary recreation of functions each time a component gets re-rendered. Furthermore, we saw how this can be used in conjunction with React.memo(...)
to limit child component re-renders when passing functions down as props.
Lastly, useMemo(...)
can be used to avoid duplicate recalculations and data processing, especially if it is an expensive operation. As long as dependent state for the calculation does not change, there is no reason to perform the operation.
Thank you for taking the time to read my article. I hope it was helpful.
If you noticed anything in the article that is incorrect or isn't clear, please let me know. I always appreciate the feedback.
Top comments (0)