Home > Enterprise >  Prevent child rerenders when parent state is changed?
Prevent child rerenders when parent state is changed?

Time:11-20

This is a simplified version of my problem

https://codesandbox.io/s/withered-night-6jbgft

import React, { useState, useCallback } from "react";
import "./styles.css";

const Box = React.memo(({ c, i, handleClick }) => {
  return (
    <div
      className="box"
      style={{ background: c }}
      onClick={() => handleClick(i)}
    ></div>
  );
});

export default function App() {
  const COUNT_BOXES = 250;
  const [colour, setColour] = useState("#00ff00");
  const [boxes, setBoxes] = useState(Array(COUNT_BOXES).fill("#ff00ff"));

  const changeColour = useCallback(
    (i) => {
      setBoxes((prevBoxes) => {
        prevBoxes[i] = colour;
        return [...prevBoxes];
      });
    },
    [colour]
  );

  return (
    <div className="App">
      <input
        type="color"
        value={colour}
        onChange={(e) => setColour(e.target.value)}
      />
      Change colour
      <div className="wrapper">
        {boxes.map((c, i) => (
          <Box key={i} c={c} i={i} handleClick={changeColour} />
        ))}
      </div>
    </div>
  );
}

I have a series of components that can be interacted with (the boxes). When you click a box, it changes colour. The colour it gets changed to is another piece of state in the parent component. The current implementation means that when the colour state changes, then useCallback creates a new function for changeColour, which is passed to every box. So every box rerenders.

How can I refactor this so that every box doesn't need to rerender when the parent colour changes?

CodePudding user response:

One possibility would be to store the parent's changeColour function inside a ref, so that the reference is stable. It's not the usual React way of doing things, but it works.

const { useState, useCallback, useEffect } = React;

const Box = React.memo(({ c, i, handleClickRef }) => {
  console.log('render');
  return (
    <div
      className="box"
      style={{ background: c }}
      onClick={() => handleClickRef.current(i)}
    ></div>
  );
});

function App() {
  console.log('app rendering');
  const COUNT_BOXES = 250;
  const [colour, setColour] = useState("#00ff00");
  const [boxes, setBoxes] = useState(Array(COUNT_BOXES).fill("#ff00ff"));
  const changeColourRef = React.useRef();
  const changeColour = useCallback(
    (i) => {
      setBoxes((prevBoxes) => {
        prevBoxes[i] = colour;
        return [...prevBoxes];
      });
    },
    [colour]
  );
  useEffect(() => {
    changeColourRef.current = changeColour;
  }, [colour]);

  return (
    <div className="App">
      <input
        type="color"
        value={colour}
        onChange={(e) => setColour(e.target.value)}
      />
      Change colour
      <div className="wrapper">
        {boxes.map((c, i) => (
          <Box key={i} c={c} i={i} handleClickRef={changeColourRef} />
        ))}
      </div>
    </div>
  );
}


ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
.App {
  font-family: sans-serif;
  text-align: center;
  padding: 30px;
}
.wrapper {
  display: flex;
  flex: 0 0 40px;
  flex-wrap: wrap;
}

.box {
  cursor: pointer;
  width: 20px;
  height: 20px;
  border: 1px solid black;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

It would be nicer if you could just let the children re-render - it usually won't be a problem (and if it is a problem, usually it's fixable by tweaking your code, while continuing to let the component re-render).

  • Related