Reputation: 177
I'm fairly new to React and I'm having some trouble understanding exactly why an unchanging component is getting rerendered, even though I'm using the React.memo
higher-order component.
I have a sidebar which contains a number of row elements. Rows contain data that's used in other components; all components share the 'selection' status of the rows. In the sidebar, I change the styling to show the selection state of every element.
Everything behaves as expected, but performance scales poorly as the list gets longer. I think part of this is due to React re-rendering every row element in the sidebar list, including ones whose selection state has not changed. I thought I could prevent this re-rendering by using React.memo
, but it doesn't seem to make a difference.
Here is the code for each list entry:
import React from 'react';
// The only props that might change value are the labels string and
// the styles rowStyle and labelStyle, which caller populates
// with 'selected' or 'unselected' styles based on row state
const Row = React.memo(({
rowId, labels = "", rowStyle = {}, labelStyle = {},
onClicked // callback from grandparent, which updates selections (w/ modifier keys)
}) => {
console.log(`Rendering row ${rowId}`) // to report when rows rerender
return (
<div
key={rowId}
style={rowStyle}
onClick={(event) => onClicked(rowId, event)}
>
<span>{rowId}</span>
<span style={labelStyle}>{ labels }</span>
</div>
);
})
export default Row;
This component is called from a parent which represents the entire sidebar list. In order to minimize the amount of needless function calls (and make very clear that there's nothing with any side effects happening within the individual rows), I build a list of tuples for each row that has its id, style, labels, and label-style.
The contents of the list are passed to the Row
component, and most of the time should be identical between calls (thus triggering memoization and avoiding the rerender), but don't seem to be.
import React from 'react';
import Row from '../pluginComponents/Row';
import Styles from './common/Styles'; // to ensure the references aren't changing
// onClicked is passed in from the parent component and handles changing the selections
const ListDiv = React.memo(({ rowIds, onClicked, rowLabels, styling, selections }) => {
const tuples = rowIds.reduce((priors, rowId) => {
return {
...priors,
[rowId]: {
'style': Styles.unselectedStyle,
'labelStyle': Styles.unselectedLabelStyle,
'labels': ((rowLabels[rowId] || {}).labels || []).join(", ")
}
}
}, {});
Object.keys(selections).forEach((rowId) => {
if (!tuples[rowId]) return;
tuples[rowId]['style'] = Styles.selectedStyle;
tuples[rowId]['labelStyle'] = Styles.selectedLabelStyle;
});
return (
<div style={styling}>
{rowIds.map((rowId) => (
<Row
key={rowId}
rowId={rowId}
labels={tuples[rowId]['labels']}
rowStyle={tuples[rowId]['style']}
labelStyle={tuples[rowId]['labelStyle']}
onClicked={onClicked}
/>
))}
</div>
)
})
const RowList = ({ list, selections = {}, onClicked, labels={}, styling }) => {
if (!list) return (<div>Empty list</div>);
return (
<div>
<ListDiv
rowIds={list}
onClicked={onClicked}
rowLabels={labels}
styling={styling}
selections={selections}
/>
</div>
);
}
export default RowList;
which is itself called from a grandparent class that manages all the state:
const Grandparent = (props) => {
...
return (
...
<div>
{
(status !== 'complete') ? (
<div><CircularProgress /></div>
) : (
<RowList list={data.list}
selections={selections} // tracked with useState
onClicked={handleClicked} // calls some functions defined in this component
labels={data.labels || {}}
styling={foo}
/>
)
}
...
);
...
Why are my ought-to-be-memoized entries of the Row
component getting rerendered, and what can I do to fix it?
Upvotes: 0
Views: 755
Reputation: 428
The onClicked function in the Grandparent could be getting recreated on each render, so making your row component re-render as well.
The solution is to use React.useCallback in the Grandparent.
const handleClicked = React.useCallback(() => {
...
}, [a, b])
Where a and b are dependencies that if change will require a re-render.
React useCallback docs
Upvotes: 2