Search code examples
reactjsfrontendmemoization

Does passing a reference type in props make React.memo useless?


Suppose I have a PersonCard component, which receives props, and renders a card showing the person's information.

interface PersonProps { 
    firstName: string,
    lastName: string
}

const PersonCard: React.FC<PersonProps> = (firstName, lastName) => {
    // render the things
}

Now, if I were to use React.memo to prevent unnecessary re-rendering of this component, it would look like this:

const PersonCard: React.FC(<PersonProps>) => 
    React.memo(({firstName, lastName}) => {
    // render the things
    }
)

And this, to my understanding, would work: if my component was called twice with the same firstName and lastName, it would not re-render.


Now, my question arises when we add a reference type to the mix:

interface PersonProps { 
    firstName: string,
    lastName: string,
    hobbies: Array<string>
}

const PersonCard: React.FC(<PersonProps>) => 
    React.memo(({firstName, lastName, hobbies}) => {
    // render the things
    }
)

In this case React.memo does a shallow comparison (as its default behavior), and will not work on the hobbies array.

Therefore, PersonCard will always re-render, even if firstName, lastName and hobbies do not change, because it will think hobbies changed: this is effectively the same as not having React.memo at all.

So, my question is this: Am I wrong, or having any reference type passed as a prop without specifying a deep comparison callback completely nullifies the point of React.memo?


Solution

  • In general, you are correct: passing one or more reference types as a prop to a memoized component will prevent memoization, assuming the reference changes between every render (which is often the case). To play devil's advocate, here are a couple scenarios where you don't expect the reference to change between each render:

    Static Values

    If you pass a static reference to a const as a prop, you can reasonably expect that it won't change between renders, and should be safely memoized.

    const IDS = [1, 2, 3];
    
    const App = () => (
      <MyMemoizedComponent ids={IDS} />
    );
    

    This works with static let variables as well, with the caveat that the memoized component will rerender if the reference changes.

    let IDS = [1, 2, 3];
    IDS = IDS.slice(); // this would trigger a rerender
    

    refs

    Static values are generally avoided when working with components in favor of a ref. When using a ref where you don't expect the value of .current to change frequently, you can still get the benefit of memoization.

    const App = () => {
      const idsRef = React.useRef([1, 2, 3]);
    
      return (
        <MyMemoizedComponent ids={idsRef.current} />
      );
    }
    

    useMemo

    React.memo and React.useMemo aren't mutually exclusive! You can memoize a value with useMemo to prevent it from breaking memoization.

    const App = () => {
      const idsMemo = React.useMemo(() => [1, 2, 3], []);
    
      return (
        <MyMemoizedComponent ids={idsMemo} />
      );
    };
    

    These examples are a bit contrived, but you can imagine a scenario where the data isn't initialized locally but is instead fetched over the network, something like

    const App = ({ path }) => {
      const ids = React.useMemo(() => fetchFromNetwork(path), [path]);
    
      return (
        <MyMemoizedComponent ids={ids} />
      );
    };
    

    So, in summary, passing one or more references types as a prop to a memoized component will usually break memoization. If you expect the reference type to change value frequently (~once per render), just drop the memoization, it's causing extra work with no benefit. If you don't expect it to change frequently, memoize it and only update the reference when the inner value changes (or, as you noted, provide a custom deep equality comparison function to React.memo).