useMemo with an array dependency?

Viewed 20254

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.

2 Answers

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>
  );
}

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:

  • Stringifying and parsing array is generally safe only for primitive array's values, for others, such as Date, functions first consult documentation.
  • These operations might be performance intensive.
Related