Evanss
Evanss

Reputation: 23593

useMemo with an array dependency?

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

Answers (2)

justdvl
justdvl

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:

  • 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.

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

Evanss
Evanss

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

Related Questions