Avoiding re-renders for sibling React components with array of state stored in parent

Viewed 223

I'm rendering a series of frames from a video, each in their own component. The frames are stored as a state within a parent component called Timeline.

Since I need to manipulate elements of the array of frames from the children components, I pass in callback functions which have the index i with a closure. To avoid changes in individual frames affecting all frames, I've used the memo higher-order component around each Frame.

Nevertheless, I've found that any time I select a frame, all of the frames are re-rendered. Using the whyDidyouUpdate hook (https://usehooks.com/useWhyDidYouUpdate/), I've pinpointed it down to my callback changing on each render.

I tried doing onClick={useCallback(() => selectFrame(i), [])}, but instead, the frame simply does not change colors, indicating to me that this has somehow impeded the callback from properly functioning.

let Frame = memo(({frame, ...}) => (
    <img style={{border: frame.selected ? "green" : "red";}} />
);

let Timeline = () => {

    let [frames, setFrames] = useState([
        { id: "00000", selected: True, ...more image information },
        { id: "00001", selected: True, ...more image information },
        { id: "00002", selected: True, ...more image information },
    ]);

    let selectFrame = (i) => {
        let newFrames = [...frames];
        newFrames[i].mask = base64;
        setFrames(newFrames);
    }

    return (
        <div>
        {frames.map((f, i) => (
            <Frame
                frame={f}
                key={f.id}
                onClick={() => selectFrame(i)}
            />
        )}
        </div>
    ));
}

Any suggestions for the correct way to handle this? Thanks

EDIT: Drew's solution below pointed me in the correct direction. The proper syntax is:

const selectFrame = (i) => useCallback(() =>
    setFrames(frames.map((f, j) => (
        (i === j) ? {...f, selected: !f.selected} : f
    ))), []);

You need to have a different memoized callback for each function.

1 Answers

The memo HOC simply does a shallow comparison of props, each time the frames are mapped a new onClick callback is created. Convert to a curried callback that encloses the index and returns, and uses a functional state update, and memoize the callback using the useCallback hook.

const Timeline = () => {

    const [frames, setFrames] = useState([
        { id: "00000", selected: True, ...more image information },
        { id: "00001", selected: True, ...more image information },
        { id: "00002", selected: True, ...more image information },
    ]);

    const selectFrame = useCallback(index => () => {
        setFrames(frames => frames.map((frame, i) => i === index ? {
          ...frame,
          mask: base64;
        } : frame));
    }, []);

    return (
        <div>
        {frames.map((f, i) => (
            <Frame
                frame={f}
                key={f.id}
                onClick={selectFrame(i)}
            />
        )}
        </div>
    ));
}
Related