Say I have the following code:
import React, { memo } from 'react';
const MyComponent = ({ arrayOfStuff }) => (
<div>
{arrayOfStuff.map(element => (
<p key={element.foo}>element.foo</p>
))}
</div>
);
const areEqual = (prevProps, nextProps) => {
const prevArrayOfStuff = prevProps.arrayOfStuff;
const nextArrayOfStuff = nextProps.arrayOfStuff;
if (prevArrayOfStuff.length !== nextArrayOfStuff.length)
return false;
for (let i; i < prevArrayOfStuff.length && i < nextArrayOfStuff.length; ++i) {
if (prevArrayOfStuff[i].foo !== nextArrayOfStuff[i].foo)
return false;
}
return true;
};
export default memo(MyComponent, areEqual);
And suppose arrayOfStuff is pretty large, maybe hundreds of elements. Am I really saving much time memoizing the component? I would think that in the event the props are the same, it would iterate over all the elements regardless of the memo since both areEqual and the render function do so.
The best answer to this is: Profile it and see. :-)
But although your array may have hundreds of entries in it, the checks you're doing aren't complex, they're quite straightforward and quick. (I'd add an if (prevArrayOfStuff === nextArrayOfStuff) { return true; }
at the beginning.)
Some pros and cons:
Pros:
Your check is quite straightforward and fast, even for hundreds of elements.
If you find no changes, you save:
Remember that your component will be called to re-render any time anything in its parent changes, even if those changes don't relate to your component.
Cons:
If there are change in the array often, you're just adding more work for no reward because areEqual
is going to return false
anyway.
There's ongoing maintenance cost to areEqual
, and it presents opportunities for bugs.
So it really comes down to what changes in your overall app, and in particular the parents of your component. If those parents have state or props that change often but not related to your component, your component doing its check could save a lot of time.
Here's something demonstrating how your component will get called to re-render when something in its parent changed even though nothing in its props did:
Without memoizing it (React won't actually update DOM elements if nothing changes, but your function is called and creates the React elements that React compares to the rendered ones):
const {useState, useEffect} = React;
// A stand-in for your component
const Example = ({items}) => {
console.log("Example rendering");
return <div>
{items.map(item => <span key={item}>{item}</span>)}
</div>;
};
// Some other component
const Other = ({counter}) => {
console.log("Other rendering");
return <div>{counter}</div>;
};
// A parent component
const App = () => {
// This changes every tick of our interval timer
const [counter, setCounter] = useState(0);
// This changes only every three ticks
const [items, setItems] = useState([1, 2, 3]);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => {
c = c + 1;
if (c % 3 === 0) {
// Third tick, change `items`
setItems(items => [...items, items.length + 1]);
}
// Stop after 6 ticks
if (c === 6) {
setTimeout(() => {
console.log("Done");
}, 0);
clearInterval(timer);
}
return c;
});
}, 500);
return () => clearInterval(timer);
}, []);
return <div>
<Example items={items} />
<Other counter={counter} />
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.as-console-wrapper {
max-height: 80% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
With memoizing it:
const {useState, useEffect} = React;
// A stand-in for your component
const Example = ({items}) => {
console.log("Example rendering");
return <div>
{items.map(item => <span key={item}>{item}</span>)}
</div>;
};
const examplePropsAreEqual = ({items: prevItems}, {items: nextItems}) => {
const areEqual = (
prevItems === nextItems ||
(
prevItems.length === nextItems.length &&
prevItems.every((item, index) => item === nextItems[index])
)
);
if (areEqual) {
console.log("(skipped Example)");
}
return areEqual;
}
const ExampleMemoized = React.memo(Example, examplePropsAreEqual);
// Some other component
const Other = ({counter}) => {
console.log("Other rendering");
return <div>{counter}</div>;
};
// A parent component
const App = () => {
// This changes every tick of our interval timer
const [counter, setCounter] = useState(0);
// This changes only every three ticks
const [items, setItems] = useState([1, 2, 3]);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => {
c = c + 1;
if (c % 3 === 0) {
// Third tick, change `items`
setItems(items => [...items, items.length + 1]);
}
// Stop after 6 ticks
if (c === 6) {
setTimeout(() => {
console.log("Done");
}, 0);
clearInterval(timer);
}
return c;
});
}, 500);
return () => clearInterval(timer);
}, []);
return <div>
<ExampleMemoized items={items} />
<Other counter={counter} />
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.as-console-wrapper {
max-height: 80% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>