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?
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:
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
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} />
);
}
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
).