Вы можете использовать эффект каждый раз, когда доска изменяет этот эффект. Когда ходов больше нет, доска не меняется, и эффект больше не запускается.
Вот пример того, как вы можете это сделать:
const { useState, useEffect } = React;
const getCell = (board) => {
const row = board.findIndex((row) =>
row.some((cell) => !cell.checked)
);
if (row === -1) {
return [-1, -1, false];
}
const col = board[row].findIndex((cell) => !cell.checked);
return [row, col, true];
};
const initialState = [
[{ checked: false }, { checked: false }],
[{ checked: false }, { checked: false }],
];
const App = () => {
const [board, setBoard] = useState(initialState);
useEffect(() => {
const timer = setTimeout(
() =>
setBoard((board) => {
const [row, col, set] = getCell(board);
if (set) {//do we need to set board (there are moves)
const boardCopy = [...board];
boardCopy[row] = [...board[row]];
boardCopy[row][col] = {
...boardCopy[row][col],
checked: true,
};
return boardCopy;
}
//this will return the same as passed in
// so App will not re render because board
// will not have changed
return board;//no moves left because set is false
}),
1000
);
return () => clearTimeout(timer);
}, [board]);
return (
<div>
<button onClick={() => setBoard(initialState)}>
reset
</button>
<table>
<tbody>
{board.map((row, i) => (
<tr key={i}>
{row.map(({ checked }, i) => (
<td key={i}>{checked ? 'X' : 'O'}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>