I have two "expensive" functions that I would like to memoize in my react application, as they take a long time to render. The expensive functions are used in an array map. I would like to memoize each result of the array map, so that if one element of the array is changed, only that element's expensive functions will be recalculated. (And to memoize the expensive functions independently, as sometimes only one will need to be recalculated.) I'm struggling with how to memoize and pass in the current array value.
Here is the working demo, without memoization:
import React, { useMemo, useState } from "react";
const input = ["apple", "banana", "cherry", "durian", "elderberry", "apple"];
export default function App() {
const output = input.map((msg, key) => createItem(msg, key));
return <div className="App">{output}</div>;
}
const createItem = (message, key) => { // expensive function 1
console.log("in createItem:", key, message);
return (
<div key={key}>
<strong>{"Message " + (1 + key)}: </strong>
{message} =>{" "}
<span
id={"preview-" + key}
dangerouslySetInnerHTML={{ __html: msgSub(message) }}
/>
</div>
);
};
const msgSub = message => { // expensive function 2
const messageSub = message.replace(/[a]/g, "A").replace(/[e]/g, "E");
console.log("in msgSub:", message, messageSub);
return messageSub;
};
(I don't have it functioning on SO's editor, so view and run it on codesandbox.)
Here is one of my attempts using custom hooks and useMemo hooks.
Any guidance would be greatly appreciated!
And bonus points for illustrating how to get react to work in SO's editor!
My first step was to change createItem
so that Item was its only Function Component. This meant I could then memoize the <Item/>
component so that it would only render if the props changed, which importantly is message and key/index (as you were rendering the value previously).
Working example of this https://codesandbox.io/s/funny-chaplygin-pvfoj.
const Item = React.memo(({ message, index }) => {
console.log("Rendering:", index, message);
const calculatedMessage = msgSub(message);
return (
<div>
<strong>{"Message " + (1 + index)}: </strong>
{message} =>{" "}
<span
id={"preview-" + index}
dangerouslySetInnerHTML={{ __html: calculatedMessage }}
/>
</div>
);
});
const msgSub = message => {
// expensive function 2
const messageSub = message.replace(/[a]/g, "A").replace(/[e]/g, "E");
console.log("in msgSub:", message, messageSub);
return messageSub;
};
You can see that at initial render, it renders all Items with their message
but especially apple
twice.
If two components are being rendered independent of each other, and happen to use the same props, these two components will be rendered. React.memo doesn't save component renders.
It must render the Item component twice <Item message="apple" />
, once for apple
at index 0 and again at index 5 apple
.
You'll noticed I placed a button in the App, which when clicked, will change the contents of the array.
Changing the original array a little, I placed carrot
at index 4 in the original array and moved it to index 2 in the new array, when I updated it.
const [stringArray, setStringArray] = React.useState([
"apple",
"banana",
"cherry",
"durian",
"carrot",
"apple" // duplicate Apple
]);
const changeArray = () =>
setStringArray([
"apple",
"banana",
"carrot", // durian removed, carrot moved from index 4 to index 2
"dates", // new item
"elderberry", // new item
"apple"
]);
If you look at the console, you'll see that on the first render you see in msgSub: carrot cArrot
but when we update the array, in msgSub: carrot cArrot
is called again. This is because the key on <Item />
so it's forced to rerender. This is correct, because our key is based on index, and carrot changed position. However, you said msgSub
is an expensive function...
I noticed in your array you had apple
twice.
const input = ["apple", "banana", "cherry", "durian", "elderberry", "apple"];
I felt that you wanted to memoize the calculation so that apple
wasn't calculated again, if apple
was previously calculated.
We can store the calculated value in our own memoization state, so we can look up the message value, and see if we have calculated it previously.
const [localMemoization, setLocalMemoization] = useState({});
We want to make sure we update this localMemoization when stringArray
changes.
React.useEffect(() => {
setLocalMemoization(prevState =>
stringArray.reduce((store, value) => {
const calculateValue =
prevState[value] ?? store[value] ?? msgSub(value);
return store[value] ? store : { ...store, [value]: calculateValue };
})
);
}, [stringArray]);
This line const calculateValue = prevState[value] ?? store[value] ?? msgSub(value);
Using the same logic as the previous render, we now look up the calculated value and pass that into <Item>
so that it's React.memo will prevent rerenders of that component if App was to render again.
const output = stringArray.map((msg, key) => {
const expensiveMessage = localMemoization[msg];
return (
<Item
key={key}
message={msg}
calculatedValue={expensiveMessage}
index={key}
/>
);
});
Working example here https://codesandbox.io/s/attempt-with-custom-hooks-and-usememo-y3lm5?file=/src/App.js:759-1007
From this, in the console, we can see that on the first render, apple
is only calculated once.
When the array changes, we don't calculate carrot
again and only the changed items dates
and elderberry
.