Memoization, not memorization, is a technique commonly used in functional programming to enhance application performance. In simple terms, memoization involves caching the result of a function. When the function is called again with the same inputs, the cached result is returned instead of re-executing the function. It is important to note that the memoized function would need to be a pure function. meaning:
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 strongly promotes a functional programming style, offers built-in mechanisms for optimizing applications through memoization. Using React.memo(...), useCallback(...), and useMemo(...), developers can better control their applications, avoiding unnecessary component re-renders and heavy data recalculations.
React.memo(...)
The purpose of React.memo(...)
is to avoid unnecessary re-rendering of a child component when its props remain unchanged, even if the parent component re-renders.
The Problem
Consider this <Parent/>
component. It contains a button that increments a total
state variable when clicked and displays the current value of total
. Additionally, it contains a <Child/>
component that does not receive 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 observe what happens when we click the button to increment the total
state variable in the parent component.
The child component re-renders along with the parent component. This seems unusual, particularly because the child component's props are not changing—in fact, it doesn't have any props at all.
The Solution
Fortunately, the solution is straightforward: wrap the child component in React.memo(Component, propsComparatorFunction)
. 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
Once again, the solution is straightforward: avoid recreating the function and instead utilize the existing function with the useCallback(fn, dependencyList)
hook. The first argument defines the function for the necessary work, and similar to useEffect(...)
, the second argument consists of a list of state variables that useCallback
will monitor. As long as none of the state dependencies in the list change, the same function will be returned on every render, rather than creating a new one. Because the function remains unchanged, even from a referential perspective, React.memo(...)
's shallow comparison won't 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 a function that produces a value through an expensive calculation, and the inputs to the function are not changing, there's no need to recalculate 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 similarly to how we used useCallback(...)
. However, this time the result of useMemo(...)
is a value, not a function. Like useCallback(...)
, it takes a dependency list, and as long as the state variables in the dependency list do not change, the result does not need 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, this might be overkill for most scenarios. In fact, the React documentation advises using these techniques only for specific cases. Ultimately, it's up to you to decide when and where to use them, based on what is best for your application.
Should you add React.memo
everywhere?
Should you add useCallback
everywhere?
Should you add useMemo
everywhere?
Conclusion
React provides developers with mechanisms to optimize their applications through memoization.
By using 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 it if its props have changed.
Utilizing useCallback(...)
, we can prevent the unnecessary recreation of functions each time a component re-renders. Additionally, 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 the dependent state for the calculation does not change, there is no reason to perform the operation again.
Thank you for taking the time to read my article. I hope you found it helpful.
If you notice anything in the article that is incorrect or unclear, please let me know. I always appreciate the feedback.