反应:通过更改键重新安装后,对子组件的引用为空

React: ref to child component is null after remounting by changing key

对于 1v1 数独游戏,我的 GamePage 组件呈现主 Game 组件,其中包含每个玩家的 Clock。当两个玩家都同意重新比赛时,整个 Game 通过简单地将其 key 增加 1 来重置(在更改 GamePage 状态以反映新游戏)。

我的问题:
Game 将两个引用 this.myClockthis.opponentClock 存储到两个时钟内的倒计时,因此当玩家填满一个方块时它们可以是 paused/started。这对于第一场比赛非常有效。但是,Game 重新安装后,任何移动都会抛出“无法读取 null 的属性(读取 'start')”,例如this.opponentClock.current.start().

我知道当组件卸载时 refs 被设置为 null,但是通过呈现 Game 的新版本,我希望它们在构造函数中再次设置。令我惊讶的是,新计时器设置正确,其中之一是 运行(也使用 refs 在 GamecomponentDidMount 中完成),但之后的任何访问都会破坏应用程序。

对于任何关于可能原因的提示或评论,我将非常感激,我已经坚持了两天了,我 运行 无法 google。

GamePage.js:

export default function GamePage(props) {
    const [gameCounter, setGameCounter] = useState(0) //This is increased to render a new game
    const [gameDuration, setGameDuration] = useState(0)
    ...
    useEffect(() =>{
        ...
        socket.on('startRematch', data=>{
            ...
            setGameDuration(data.timeInSeconds*1000)
            setGameBoard([data.generatedBoard, data.generatedSolution])
            setGameCounter(prevCount => prevCount+1)
        })
    },[]) 
    
    return (
        <Game key={gameCounter} initialBoard={gameBoard[0]} solvedBoard={gameBoard[1]} isPlayerA={isPlayerA} 
        id={gameid} timeInMs={gameDuration} onGameOver={handleGamePageOver}/> 
    )
}

Game.js:

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            gameBoard: props.initialBoard, 
            isPlayerANext: true,
            gameLoser: null, //null,'A','B'
        };
        this.myClock = React.createRef(); 
        this.opponentClock = React.createRef();
    }

    componentDidMount(){
        if(this.props.isPlayerA){
            this.myClock.current.start()
        }
        else{
            this.opponentClock.current.start()
        }
        socket.on('newMove', data =>{
            if(data.isPlayerANext===this.props.isPlayerA){
                this.opponentClock.current.pause()
                this.myClock.current.start()
            }
            else{
                this.opponentClock.current.start()
                this.myClock.current.pause()
            }
        })
        ...
    }
    
    render(){
        return( 
        <React.Fragment>
            <Clock ref={this.opponentClock} .../>
            <Board gameBoard={this.state.gameBoard} .../>
            <Clock ref={this.myClock} .../>
        </React.Fragment>)
        ...
    }
}

export default Game

Clock.js:

import Countdown, { zeroPad } from 'react-countdown';

const Clock = (props,ref) => {
    const [paused, setPaused] = useState(true);
    return <Countdown ref={ref} ... />
}

export default forwardRef(Clock);

编辑: 接受的答案就像一个魅力。问题不在于新 ref 本身,而是使用旧 ref 的 socket.on('newMove',...)socket.on('surrender',...) 在卸载旧游戏时没有正确清理。

很高兴地通知您,经过大约 2 小时的调试(笑),我找到了问题的根源。

问题是您没有在组件卸载时清理 socket.on 函数,所以旧的函数仍然存在并引用了旧的引用。

看我这里的做法,清理函数,你的问题就解决了:

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      gameBoard: props.initialBoard,
      isPlayerANext: true,
      gameLoser: null, //null,'A','B'
    };
    this.solvedBoard = props.solvedBoard;
    this.wrongIndex = -1;
    this.handleSquareChange = this.handleSquareChange.bind(this);
    this.myClock = React.createRef();
    this.opponentClock = React.createRef();
    this.endTime = Date.now() + props.timeInMs; //sets both clocks to the chosen time
    this.handleTimeOut = this.handleTimeOut.bind(this);
    this.onNewMove = this.onNewMove.bind(this);
    this.onSurrender = this.onSurrender.bind(this);
  }

  isDraw() {
    return !this.state.gameLoser && this.state.gameBoard === this.solvedBoard;
  }

  onNewMove(data) {
    console.log('NewMoveMyClock: ', this.myClock.current);
    if (data.isPlayerANext === this.props.isPlayerA) {
      console.log(
        'oppmove: ',
        this.myClock.current,
        this.opponentClock.current
      );
      this.opponentClock.current.pause();
      this.myClock.current.start();
    } else {
      console.log('mymove: ', this.myClock.current, this.opponentClock.current);
      this.opponentClock.current.start();
      this.myClock.current.pause();
    }
    let idx = data.col + 9 * data.row;
    let boardAfterOppMove =
      this.state.gameBoard.substring(0, idx) +
      data.val +
      this.state.gameBoard.substring(idx + 1);
    this.wrongIndex = data.gameLoser ? idx : this.wrongIndex;
    this.setState({
      gameBoard: boardAfterOppMove,
      gameLoser: data.gameLoser,
      isPlayerANext: data.isPlayerANext,
    });
    if (data.gameLoser) {
      this.handleGameOver(data.gameLoser);
    } else if (this.isDraw()) {
      this.handleGameOver(null);
    }
  }

  onSurrender(data) {
    this.handleSurrender(data.loserIsPlayerA);
  }

  componentDidMount() {
    console.log('component game did mount');
    console.log(
      this.myClock.current.initialTimestamp,
      this.myClock ? this.myClock.current.state.timeDelta.total : null,
      this.opponentClock
        ? this.opponentClock.current.state.timeDelta.total
        : null,
      this.props.gameCounter
    );
    if (this.props.isPlayerA) {
      this.myClock.current.start();
    } else {
      this.opponentClock.current.start();
    }
    socket.on('newMove', this.onNewMove);

    socket.on('surrender', this.onSurrender);
  }

  componentWillUnmount() {
    socket.off('newMove', this.onNewMove);
    socket.off('surrender', this.onSurrender);
  }