如何防止在 ReactJS 中使用回调作为道具重新渲染?

How to prevent re-rendering with callbacks as props in ReactJS?

我正在通过使用 TypeScript 重构此 tutorial 中的代码来练习 ReactJS 中的新钩子功能。

我正在将父组件的回调传递给子组件抛出必须在单击按钮事件上执行的道具。

我的问题是这样的:我有一个 alert 对话框在游戏获胜时出现 两次 而不是一次。 我假设这是由组件重新渲染引起的,所以我使用 useCallback 挂钩来记忆 handleClickOnSquare 回调。问题是,警告对话框仍然出现两次。

我想我遗漏了一些与重新渲染有关的东西,有人知道是什么导致了这种行为吗?

这是我的代码:

Game.tsx

import React, { useState, useCallback } from 'react';
import './Game.css';

interface SquareProps {
    onClickOnSquare: HandleClickOnSquare
    value: string;
    index: number;
}

const Square: React.FC<SquareProps> = (props) => {
    return (
        <button
            className="square"
            onClick={() => props.onClickOnSquare(props.index)}
        >
            {props.value}
        </button>
    );
};

interface BoardProps {
    squares: Array<string>;
    onClickOnSquare: HandleClickOnSquare
}

const Board: React.FC<BoardProps> = (props) => {

    function renderSquare(i: number) {
        return (
            <Square
                value={props.squares[i]}
                onClickOnSquare={props.onClickOnSquare}
                index={i}
            />);
    }

    return (
        <div>
            <div className="board-row">
                {renderSquare(0)}
                {renderSquare(1)}
                {renderSquare(2)}
            </div>
            <div className="board-row">
                {renderSquare(3)}
                {renderSquare(4)}
                {renderSquare(5)}
            </div>
            <div className="board-row">
                {renderSquare(6)}
                {renderSquare(7)}
                {renderSquare(8)}
            </div>
        </div>
    );
};

export const Game: React.FC = () => {

    const [history, setHistory] = useState<GameHistory>(
        [
            {
                squares: Array(9).fill(null),
            }
        ]
    );
    const [stepNumber, setStepNumber] = useState(0);
    const [xIsNext, setXIsNext] = useState(true);

    const handleClickOnSquare = useCallback((index: number) => {
        const tmpHistory = history.slice(0, stepNumber + 1);
        const current = tmpHistory[tmpHistory.length - 1];
        const squares = current.squares.slice();

        // Ignore click if has won or square is already filled
        if (calculateWinner(squares) || squares[index]) return;

        squares[index] = xIsNext ? 'X' : 'O';
        setHistory(tmpHistory.concat(
            [{
                squares: squares,
            }]
        ));
        setStepNumber(tmpHistory.length);
        setXIsNext(!xIsNext);
    }, [history, stepNumber, xIsNext]);

    const jumpTo = useCallback((step: number) => {
        setHistory(
            history.slice(0, step + 1)
        );
        setStepNumber(step);
        setXIsNext((step % 2) === 0);
    }, [history]);

    const current = history[stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
        const desc = move ?
            'Go back to move n°' + move :
            'Go back to the start of the party';

        return (
            <li key={move}>
                <button onClick={() => jumpTo(move)}>{desc}</button>
            </li>
        );
    });

    let status: string;
    if (winner) {
        status = winner + ' won !';
        alert(status);
    } else {
        status = 'Next player: ' + (xIsNext ? 'X' : 'O');
    }

    return (
        <div className="game">
            <div className="game-board">
                <Board
                    squares={current.squares}
                    onClickOnSquare={handleClickOnSquare}
                />
            </div>
            <div className="game-info">
                <div>{status}</div>
                <ol>{moves}</ol>
            </div>
        </div>
    );
}

function calculateWinner(squares: Array<string>): string | null {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a];
        }
    }
    return null;
}

types.d.ts

type GameHistory = Array<{
    squares: Array<string>
}>;

type HandleClickOnSquare = (index: number) => void;

谢谢

您的代码太长,无法找到额外重新渲染的原因。请注意,React 可能需要 extra render.

为了避免额外的 alert 使用 useEffect:

let status: string;
if (winner) {
    status = winner + ' won !';
} else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
useEffect(() => {
    if (winner) alert(status)
}, [winner]);