Time to take a quick dive into the differences between a memoization library, in our case memoize-one, and the memo
function supplied by React.
First, memoize-one is an arbitrary library in this example. We could have picked any memoization library, such as reselect, lodash, or ramda. Memoize-one is a tiny library that only memoizes the latest arguments and results. To understand that, let's jump into what memoizing means.
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
Let's break this down into a code example,
function add(a, b) {
return a + b;
}
add(1, 2); //result is 3
add(1, 2); //we already know the result, return 3
This is a somewhat arbitrary example, as usual, since this calculation is incredibly cheap. But imagine if the execution was more intense, included cleaning data or mapping properties, this calculation could take some time. If our function is memoized, we can recognize that these arguments have been passed in before, and therefore return the result.
Our current example reruns the function, let's try that with memoize-one.
import memoize from "memoize-one";
function add(a, b) {
console.log("add");
return a + b;
}
const memAdd = memoize(add);
console.log(memAdd(1, 2));
console.log(memAdd(1, 2));
If you were to run this code, it would print out something like this:
add;
3;
3;
This should be what's expected. The first time we call our add function, we've never run it before. So we traverse the function, as usual, we print out the string add
followed up by printing the result. The second time though, we've already run the function, so we never actually enter the function, and never print out the add
string again.
This points out a fundamental rule when using the memoization function. You should only ever use memoization with pure functions. For our purposes, there should be no side effects in our function. Given a set of arguments to a function, we should always expect the same result. Let's take an example where that isn't true
let c = 1;
function sideEffectAdd(a, b) {
console.log("seAdd");
return a + b + c;
}
const memAdd = memoize(sideEffectAdd);
console.log(memAdd(1, 2));
console.log(memAdd(1, 2));
c++;
console.log(memAdd(1, 2));
Our result looks like:
seAdd;
4;
4;
4;
Notice that we are using a variable outside of our scoped function. That mean's this function is not pure. The final number should be 5, but since the input is the same, and the library assumes this function is pure, it never runs the internal code and returns the original, and incorrect, result to us.
For a more in-depth description of pure functions, check this out
What is reacts memo? We are specifically going to look into the memo
function, intended to be wrapped around a functional component. Let's create a similar Add component as our previous function.
import React, { memo } from "react";
import "./App.css";
const Add = memo((props) => {
const result = props.number * 2;
console.log("component rendered");
return <div>Component - {result}</div>;
});
function App() {
return (
<div className="App">
<Add number={2} />
<Add number={2} />
<Add number={2} />
</div>
);
}
export default App;
Now, what do you expect to be printed in the console? We've supplied the same props, and do similar math as our memoize-one add function. Well, here's the result
component rendered
component rendered
component rendered
Note, your's might have a circled 3, instead of individual prints
Why did it run the component all three times, when the input was the same each time? Well, memo
does attempt to do memoization, but not to the component generation, but rather to the instance of the component. That is to say, for a given component, if there is an attempt to render the same component, but the props have not changed, we will instead render the same result as last time. Let's look at an example
import React, { memo, useState } from "react";
import "./App.css";
const Add = memo((props) => {
const result = props.number * 2;
console.log("component rendered");
return <div>Component - {result}</div>;
});
function App() {
const [value, setValue] = useState(0);
console.log("outter component rendered");
return (
<div className="App">
<Add number={2} />
<button onClick={() => setValue(value + 1)}>Click me</button>
</div>
);
}
export default App;
As you can see, despite the parent component being rerendered, the internal component never actually rerenders, as it has the same props, and therefore just returns the previous result.
So, memoize-one (and most memoization libraries) remembers the result of a given function with a set of arguments, despite where the last execution occurs. React.memo, on the other hand, is for memoizing a single occurrence of a component when attempting to re-render, and will not work outside of its instance.
Before we end off this quick glance, I want to point out that there is a new useMemo
hook, which works similarly to memoize-one, and is intended to memoize given functions in the context of a functional component. In fact, it can be used in conjunction with React.memo, to both memoize the component and any internal functions. We can take a deeper dive another day, but I hope that clears up a bit of confusion.
All of the code used here today is available on Github.If there's something you're interested in to see on this blog, or you think I should check out, be sure to contact me @gitinbit. Cheers!
https://reactjs.org/docs/hooks-reference.html#usememo
https://github.com/DennyScott/memo-vs-memoize
https://www.freecodecamp.org/news/what-is-a-pure-function-in-javascript-acb887375dfe/
https://github.com/reduxjs/reselect
https://lodash.com/docs/4.17.11#memoize
https://ramdajs.com/docs/#memoizeWith
https://reactjs.org/docs/react-api.html#reactmemo
https://github.com/alexreardon/memoize-one