Tiercelet
Tiercelet

Reputation: 177

React functional component rerenders despite use of React.memo()

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

Answers (1)

k123
k123

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

Related Questions