Reputation: 23593
Can you have an array as a dependancy for the useMemo
hook?
I know this would be fine:
const dep1 : string = '1'
const thing = useMemo(()=>{
// stuff
},[dep1])
But what about this?:
const dep2 : string[] = ['1', '2']
const thing2 = useMemo(()=>{
// stuff
},[dep2])
I thought that I read once that objects and arrays will always return false for the equality check, effectively meaning thing2
will never be memoized. However I can't see this in the docs:
https://reactjs.org/docs/hooks-reference.html#usememo
This is a contrived example as dep1
and dep2
are constant variables but imagine they were a prop where the value would change.
Upvotes: 28
Views: 55176
Reputation: 884
One simple solution is to serialize this array using JSON.stringify()
, then use this stringified value in dependency array, and get the original array by deserializing serialized value:
const dep2Stringified = JSON.stringify(dep2)
const thing2 = useMemo(()=>{
const dep2Local = JSON.parse(dep2Stringified)
// stuff
},[dep2Stringified])
Same can be used for useEffect
or useCallback
.
Now the thing2 will be recalculated every time that dep2 array changes.
BTW You don't need to create a variable "dep2Local" inside useMemo, you can as well just use dep2 which should contain up-to-date value. However you will get Eslint warning about dep2 not being in dependency array.
NOTE:
EDIT: Needing to do this might be caused by wrong architecture used. Instead of thinking how to detect when value of the array change, think why is this array being re-created on every render? If array is output of some calculation, function or hook, you can memoize this calculation where you get this array. (And memoize all calculations leading up to this calculation). This way array variable will be persisted across pre-renders unless dependencies for it change, and won't trigger re-run of useEffect hook.
Upvotes: 5
Reputation: 23593
I think I was getting confused by React.memo
, from the docs:
By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.
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);
useMemo
appears to work fine with an array as a dependancy:
https://codesandbox.io/s/mystifying-cori-jr0vi?file=/src/App.js:0-647
import React, { useState, useMemo } from "react";
import "./styles.css";
export default function App() {
console.log("App rendered");
const [st, setStr] = useState("0");
const [arr, setArr] = useState([1]);
const thingToRender = useMemo(() => {
console.log("thingToRender ran");
return `Array length is ${arr.length}`;
}, [arr]);
return (
<div className="App">
<button onClick={() => setStr(`${Math.random()}`)}>Change str</button>
<h1>{st}</h1>
<p>{thingToRender}</p>
<button onClick={() => setArr([...arr, Math.round(Math.random() * 10)])}>
Change arr
</button>
</div>
);
}
However it's worth being aware that it wont work if you map
(or other methods that create a new array):
https://codesandbox.io/s/silent-silence-91j8s?file=/src/App.js
import React, { useState, useMemo } from "react";
import "./styles.css";
export default function App() {
console.log("App rendered");
const [st, setStr] = useState("0");
const [arr, setArr] = useState([1]);
const arr2 = arr.map((item) => item);
const thingToRender = useMemo(() => {
console.log("thingToRender ran");
return `Array length is ${arr2.length}`;
}, [arr2]);
return (
<div className="App">
<button onClick={() => setStr(`${Math.random()}`)}>Change str</button>
<h1>{st}</h1>
<p>{thingToRender}</p>
<button onClick={() => setArr([...arr, Math.round(Math.random() * 10)])}>
Change arr
</button>
</div>
);
}
Upvotes: 23