Reputation: 124
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
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:
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