Benjamin Liu
Posted on March 13, 2021
Memoisation is an optimisation technique that caches the result of previous computations so that they can be quickly accessed without repeating the same computation.
React introduces quite a few memoisation functions being React.memo
, useMemo
and useCallback
.
1. React.memo
React.memo
is a higher order component when wrapped around a component, memoises the result of the component and does a shallow comparison before the next render. If the new props are the same the component doesn't re-render and uses the memoised result.
By default memo
does a shallow comparison of props, however, the second argument allows you to define a custom equality check function. From React's official docs:
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
However, if you're looking to do a deep comparison between 2 values and want to take the easy route you can use isEqual
from lodash
.
Now let's have a look at this example:
// App.js
import React, { useState } from "react";
import Child from "./Child.js";
const App = () => {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
return (
<div className="App">
<input
onChange={(e) => setText(e.target.value)}
type="text"
value={text}
/>
<button onClick={() => setCounter(counter + 1)}>+ 1</button>
<Child counter={counter} />
</div>
);
};
export default App;
In this case we have a parent component called App
which takes in a <Child />
component.
import React from "react";
const Child = ({ counter }) => {
console.log("rendering...");
return <div>Count: {counter}</div>;
};
export default Child;
If you open up Console
you will notice that given each keystroke in the input field the <Child />
component re-renders. Obviously this doesn't have any performance overhead at this point in time, but imagine if the Child
component had child components of its own with state. Then you'd trigger a re-render of all components associated with the parent, that'd definitely add overhead to your application.
To prevent child components from unnecessarily re-rendering like that we have to use React.memo
. All we need to do is wrap our Child
component in our memo
and you see that no matter what we type in the input field it doesn't trigger a re-render of the <Child />
component.
import React, { memo } from "react";
const Child = ({ counter }) => {
console.log("rendering...");
return <div>Count: {counter}</div>;
};
export default memo(Child);
However, what if we wanted to pass down functions or anything that isn't a primitive value such as objects since memo
only does a shallow comparison? A shallow comparison in this case means it only checks if the props that you're passing down is referencing the same place in memory.
So let's say we want to update the counter
from <Child />
so we do something like this:
// App.js
import React, { useState } from "react";
import Child from "./Child.js";
const App = () => {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const updateCounterHandler = () => {
setCounter(counter + 1);
};
return (
<div className="App">
<input
onChange={(e) => setText(e.target.value)}
type="text"
value={text}
/>
<button onClick={() => setCounter(counter + 1)}>+ 1</button>
<Child counter={counter} updateCounter={updateCounterHandler} />
</div>
);
};
export default App;
and within Child.js:
import React, { memo } from "react";
const Child = ({ counter, updateCounter: pushUpdateCounter }) => {
console.log("rendering...");
return (
<div>
<strong>Count: {counter}</strong>
<button onClick={pushUpdateCounter}>Update Counter</button>
</div>
);
};
export default memo(Child);
However, you will notice that the <Child />
component still gets rendered whenever we type something into the input field. This is because the updateCounterHandler
inside App.js
gets recreated each time the state changes.
So the correct way to handle callback functions with memo is by using useCallback
.
2. useCallback
useCallback
is a hook that comes with react
that returns a memoised function. It takes in 2 arguments, the first one being the callback function, the second being an array of dependencies.
So all that needs to be done is wrap useCallback
around our updateCounterHandler
function to prevent the <Child />
component from re-rendering whenever we type in the input field.
const updateCounterHandler = useCallback(() => {
setCounter(counter + 1);
}, [counter]);
3. useMemo
Like useCallback
, useMemo
is a hook that takes in a function, however, instead of returning a memoised function it returns a memoised value. This makes it useful when performing heavy calculations.
import React, { memo, useMemo } from "react";
const Child = ({ counter, updateCounter: pushUpdateCounter }) => {
console.log("rendering...");
const outputNumber = useMemo(() => {
let output = 0;
for (let i = 0; i < 10000000; i++) {
output++;
}
return output;
}, []);
return (
<div>
<strong>Count: {counter}</strong>
<div>Output Number: {outputNumber}</div>
<button onClick={pushUpdateCounter}>Update Counter</button>
</div>
);
};
export default memo(Child);
Using useMemo
in the example above, we're able to cache the return value of outputNumber
, so that we're not recalling the function each time.
After learning these techniques, I hope you're able to apply it to where it's truly needed, because premature optimisation is the root of all evil! It's about finding the fine line between compromising space and time as speed optimisation techniques such as memoisation eat up space (RAM) in return for a faster time. So always question yourself before optimising your code, "do the performance gains really justify the usage?".
Posted on March 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024