Tomáš Vrána
Tomáš Vrána

Reputation: 124

how to prevent child rerender on prop change

I'm building a tic tac toe game, but I run into a problem where the entire game board is re-rendered with each click, which causes a significant delay

re-rendered is caused by the player changing each time you click. Is there a way to ignore this prop when it changes so only the cell that is clicked on is re-renderd ?

This is a simplified version of my code:

Parent Component

import Cell from "../components/Cell";
import { useState } from "react";

export default function Home() {
  const [arr, setArr] = useState([0, 0, 0, 0, 0, 0, 0, 0, 0]);
  const [player, setPlayer] = useState(true);
  const changePlayer = () => {
    setPlayer(!player);
  };

  return (
    <div>
      {arr.map((component, index) => {
        return (
          <Cell key={index} changePlayer={changePlayer} player={player}></Cell>
        );
      })}
    </div>
  );
}

Chlid Component

import React, { useState } from "react";

export default function Cell({ player, changePlayer }) {
  const [played, setPlayed] = useState(false);
  const [playedBy, setPlayedBy] = useState();
  return (
    <div
      onClick={() => {
        if (!played) {
          changePlayer();
          setPlayed(true);
          setPlayedBy(player);
        }
      }}
    >
      {played ? (playedBy ? "true" : "false") : "not played"}
    </div>
  );
}

Upvotes: 0

Views: 71

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074295

Cell knows too much. :-) Because it knows too much, you can't memoize it, because, as you say, player changes each time.

Instead of having state, I suggest giving Cell only props, managed by the parent. Cell should only know the minimum it needs to know to do its job:

  • Whether it has nothing, an X, or an O
  • What its row and column are (which won't change)
  • How to tell the parent it's been clicked (for when it has nothing)

That way, you can use React.memo to memoize the component which, along with ensuring that the click handler function you pass it from Home is stable (by using useCallback in Home), will avoid re-rendering Cell instances that haven't changed.

Here's a simple example:

const { useState, useCallback, useEffect } = React;

const BLANK_GAME = {
    player: 1,
    rows: [
        [" ", " ", " "],
        [" ", " ", " "],
        [" ", " ", " "],
    ]
};

const Home = () => {
    const [game, setGame] = useState(BLANK_GAME);
    // `useCallback` to ensure `playCell` is stable
    const playCell = useCallback((rowIndex, colIndex) => {
        setGame(game => {
            game = {
                ...game,
                rows: game.rows.slice(),
            };
            const row = game.rows[rowIndex] = game.rows[rowIndex].slice();
            row[colIndex] = game.player === 1 ? "X" : "O";
            game.player = game.player === 1 ? 2 : 1;
            return game;
        });
    }, []);
    useEffect(() => {
        console.log("Initial render complete");
    }, []);
    return <div>
        <div>Current player: {game.player}</div>
        <div className="board">
            {game.rows.map((row, rowIndex) =>
                <div key={rowIndex} className="row">
                    {row.map((cell, colIndex) =>
                        <Cell
                            key={colIndex}
                            rowIndex={rowIndex}
                            colIndex={colIndex}
                            value={cell}
                            playCell={cell === " " ? playCell : undefined}
                        />
                    )}
                </div>
            )}
        </div>
    </div>;
};

// `React.memo` to memo-ize the Cell component
const Cell = React.memo(({value, rowIndex, colIndex, playCell}) => {
    console.log(`Cell ${rowIndex},${colIndex} rendering`);
    const cls = `cell${value === " " ? " playable" : ""}`;
    // `onClick` will be `undefined` if `playCell` is undefined, which
    // is what we want
    const onClick = playCell && (() => playCell(rowIndex, colIndex));
    return <div className={cls} onClick={onClick}>{value}</div>;
});

ReactDOM.render(<Home />, document.getElementById("root"));
html {
    font-size: 24px;
}
.board {
    border: 1px solid black;
    display: grid;
    grid-template-rows: 1.2rem 1.2rem 1.2rem;
    width: 3.6rem;
}
.row {
    display: grid;
    grid-template-columns: 1.2rem 1.2rem 1.2rem;
}
.cell {
    border: 1px solid black;
    width: 1.2rem;
    height: 1.2rem;
    text-align: center;
    vertical-align: center;
}
.playable {
    cursor: pointer;
}
<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>

Note: The only reason we can use the indexes as the keys of the rows and cells is that the same row index always refers to the same row, and the same column index always refers to the same cell. That's often not true, but it is in this specific case. In general, don't use indexes as keys.

Upvotes: 4

Related Questions