The idea of returning data that was already stored is not new, but understanding how it works under the hood is very important for all developers.
The memo is memory, and that development technic helps to optimize the calculation process for expensive functions. The usage of memorization is a very important practice and is laid as crucial knowledge for development in general. React has good instruments for memorization.
But how does it work, why is it important, and how do you use it? Let’s find it out in this article in detail with practical examples.
Memorization is the function that just has a store. Closure helps store data and take it from there by key to get the result. Very simple idea: key and value.
Let’s say that we have a function that has two number arguments, and the result is an addition of both of those arguments. What do we do? We’ll pass this as an argument this function to the memo function.
With the below function, we’ll create a memory
property as an empty object and then returned the new function which is going to expect arguments. We’ll use those arguments as a key for storing and returning our results from the memorized function.
function memorize(fn) {
let memory = {};
return function (...args) {
let key = JSON.stringify(args) || 'no_args';
if (memory[key]) {
return memory[key];
}
let result = fn.apply(this, args);
memory[key] = result;
return result;
};
}
You can see that after other calls of the memorized function, there is a condition where we check the existing results based on keys from stringified arguments. If the result already exists, we just return the result itself.
First of all, let’s put some console.log
in our function to understand better the state changes.
// ...
let result = fn.apply(this, args);
console.log('result', result);
memory[key] = result;
// ...
Now, let’s create a function for memo:
function addition(a, b) {
return a + b;
}
Time to memorize it and reuse the function multiple times, and check how many times the memorized function was called actually.
const memorizedAddition = memorize(addition);
console.log(memorizedAddition(1, 2));
console.log(memorizedAddition(2, 2));
console.log(memorizedAddition(1, 2));
console.log(memorizedAddition(1, 2));
console.log(memorizedAddition(3, 3));
We created the memorized function and called console.log
five times but have passed the same arguments in three invocations.
result 3
result 4
result 6
The memorized function will be called only three times because there is no need to call the function again if the result is already known — in which case, the result will be returned from memory.
What happens if we apply this to react component?
Let’s create a very simple component in a React application using yarn create vite
without React.StrictMode
.
Strict mode is not our target. It only makes additional rendering for highlighting potential problems from inherited APIs — for instance, some imported components. We’ll not use any libraries. Instead, we’ll focus on the render process.
let key = JSON.stringify(args) || "no_args";
console.log("key", key);
Let’s put another console.log
to our memorize function, and run the app.
const Block = ({ count }) => {
return (
<div>
<h1>Block: {count}</h1>
</div>
);
};
Then memorize our Block
component using memorize
function:
const MemorizedBlock = memorize(Block);
Let’s try to use the same MemorizedBlock
component several times to make sure that Block
was called only once.
function App() {
const [count, setCount] = useState(0);
return (
<div>
<MemorizedBlock count={count} />
<MemorizedBlock count={count} />
<MemorizedBlock count={count} />
</div>
);
}
If you check the results from our console.log
, you’re going to see the result:
key [{"count":0},{}]
result {$$typeof: Symbol(react.element), type: 'div', key: null, ref: null, props: {…}, …}
key [{"count":0},{}]
key [{"count":0},{}]
You can see that usage of the MemorizedBlock
was many times, but the results from all the times were the same, which means that we calculate it only once. Other iterations were from memory. The same idea React provides for memorization functionality.
Now, let’s create more components in order to play with all of them around, and check what was updated and at which time.
export const Content = ({ value }) => {
console.log("=== render Content ===");
return (
<div>
<h1>
Value: {value}
</h1>
</div>
);
};
export const Header = ({ setCount }) => {
console.log("=== render Header ===");
return (
<header>
<h1>Header</h1>
<button
onClick={() => {
setCount((count) => count + 1);
}}
>
+
</button>
<button
onClick={() => {
setCount((count) => count - 1);
}}
>
-
</button>
</header>
);
};
export const Footer = () => {
console.log("=== render Footer ===");
return (
<footer>
<h1>Footer</h1>
</footer>
);
};
In each component, let's put console.log
in order to see what has been rendered. Finally, summarize everything in our application:
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Header setCount={setCount} />
<Content value={0} />
<Footer />
</div>
);
}
Run the application, and you have to see in the console the following output:
=== render Header ===
=== render Content ===
=== render Footer ===
Now, if you hit on for the button, you will see that all components will be rendered. Why does it happen? Because those components (functions) actually have not been memorized.
Let’s wrap Footer
to React.memo
in order to store properties, and calculate only stored results from memory as it’s been described before.
export const Footer = React.memo(() => {
console.log("=== render Footer ===");
return (
<footer>
<h1>Footer</h1>
</footer>
);
});
And render the app again, and you can see that, after hitting the button, only two components (Header
and Content)
have been updated, except Footer
because the Footer
has no changes in props. Let’s do the same with the Content
component.
export const Content = React.memo(({ value }) => {
console.log("=== render Content ===");
return (
<div>
<h1>Value: {value}</h1>
</div>
);
});
After hitting the button, you will find out that it rerenders only Header
because the Header
is not memorized. But as you can see, we got some value in the Content
, and it’s not rerendered because the value is always the same.
<Content value={0} />
If you check the file react.development.js
for memo
function, there will be the function:
memo(type, compare) {
//...
}
And comparison happens by the following condition:
compare === undefined ? null : compare
It works with twoprevProps
and nextProps
arguments in the additional argument for memo
function like that:
export const Content = React.memo(({ value }) => {
console.log("=== render Content ===");
return (
<div>
<h1>Value: {value}</h1>
</div>
);
}, (prevProps, nextProps) => {
return prevProps.value === nextProps.value;
});
Thus, you can fine-tune the props comparison for component updates. Now, add to the content the value:
<Content value={count} />
and memorize the Header
component:
export const Header = React.memo(({ setCount }) => {
console.log("=== render Header ===");
return (
<header>
<h1>Header</h1>
<button
onClick={() => {
setCount((count) => count + 1);
}}
>
+
</button>
<button
onClick={() => {
setCount((count) => count - 1);
}}
>
-
</button>
</header>
);
});
After hitting the button, you find that only Content
was rendered. It’s because the setCount
same function from the useState
and React care about it. But what if we will create our own function for the handle state, like this:
function App() {
const [count, setCount] = useState(0);
const setCountHandler = (isIncrement) => {
setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
};
return (
<div>
<Header setCount={setCountHandler} />
<Content value={count} />
<Footer />
</div>
);
}
And update the Header
for function in props:
export const Header = React.memo(({ setCount }) => {
console.log("=== render Header ===");
return (
<header>
<h1>Header</h1>
<button
onClick={() => {
setCount(true);
}}
>
+
</button>
<button
onClick={() => {
setCount();
}}
>
-
</button>
</header>
);
});
By updating the state, both components Header
and Content
updating too. It’s because the function setCountHandler
has a
For fixing the referential integrity issue, React has a special hook — useCallback
. Let’s update the handler to:
const setCountHandler = useCallback((isIncrement) => {
setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
}, []);
Now updating only Content
because it refers to the same for setCountHandler
. But if it is provided to dependencies, the empty object is like this:
const dependency = {}
const setCountHandler = useCallback((isIncrement) => {
setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
}, [dependency]);
The problem will be back because each object has a different reference on each render. To fix that issue, let’s go to the following part.
This hook solves the issue with values with references. Literally, it’s returned memorized value. The hook also has a dependency on updating rules.
const dependency = useMemo(() => ({}), []);
const setCountHandler = useCallback((isIncrement) => {
setCount((prevCount) => (isIncrement ? prevCount + 1 : prevCount - 1));
}, [dependency]);
The difference between useMemo
and useCallback
is that useMemo
returns memorized value, but useCallback
returns memorized function. For example, if you provide value to Context
like this:
<Content value={{ count: 0 }} />
The component will keep updating even with a static count value. In this case, useMemo
also will help to solve that issue.
const value = useMemo(
() => ({
count: 0,
}),
[]
);
// ...
<Content value={value} />
Now, the value is memorized, and after clicking by buttons nothing happens because the value is always the same. What happens if use state count
right in useMemo
hook?
const value = useMemo(
() => ({
count,
}),
[]
);
After the update, nothing’s gonna change, and the state Content
will be not updated at all. It’s because the dependency useMemo
has not been provided in order to update that value. To fix that, add count from state to dependency:
const value = useMemo(
() => ({
count,
}),
[count]
);
Now everything works fine. The dependency updates the useMemo
correct, and value updating updates the Content
component.
It's important to keep in mind that there is no need to memorize simple, lightweight functions. When dealing with straightforward operations that do not involve complex computations, there's little benefit in memorizing them as the cost of calculating them is minimal.
On the other hand, for more intricate functions that involve expensive calculations, it may be worthwhile to store the results to avoid unnecessary and time-consuming recalculations.
Therefore, it is recommended to exercise judgment when deciding whether to memorize a function or not, taking into account its complexity and the cost of computing it.
React.memo
: React.useCallback
: React.useMemo
: Also published here