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

Posted

技术标签:

【中文标题】反应:通过更改键重新安装后,对子组件的引用为空【英文标题】:React: ref to child component is null after remounting by changing key 【发布时间】:2021-11-21 16:26:31 【问题描述】:

对于 1v1 数独游戏,我的 GamePage 组件会渲染主要的 Game 组件,其中包含每个玩家的 Clock。当双方玩家同意重赛时,整个 Game 将通过简单地将其 key 增加 1 来重置(在更改 GamePage 状态以反映新游戏的设置之后)。

我的问题:Game 将两个参考值 this.myClockthis.opponentClock 存储到两个时钟内的倒计时,因此当玩家填满一个正方形时,它们可以暂停/开始.这对于第一场比赛非常有效。然而,Game 重新挂载后,任何移动都会抛出“Cannot read properties of null (reading 'start')”,例如this.opponentClock.current.start().

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

对于任何有关可能原因的提示或评论,我将非常感激,我已经被困在这个问题上两天了,我的东西已经用完了谷歌。

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',...) 在卸载旧游戏时没有正确清理。

【问题讨论】:

你有这个的github吗?我想看看。 这里是回购:Link。要重现我的问题,请使用 npm start 运行它,使用 npm run dev 运行后端。然后只需开始游戏,将链接复制到另一个选项卡,投降并单击两个选项卡中的重新匹配。不幸的是,一切仍然很混乱。 谢谢我去看看 【参考方案1】:

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

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

看看我这里的做法,把功能清理干净,你的问题就解决了:

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);
  

【讨论】:

非常感谢您的努力!我早上第一件事就试试看! 按预期工作,我想这种行为应该让我对套接字更加怀疑。再次感谢,在您的频道上也放了一个订阅者。

以上是关于反应:通过更改键重新安装后,对子组件的引用为空的主要内容,如果未能解决你的问题,请参考以下文章

对路由器更改做出反应重新安装

组件不会重新渲染,但 redux 状态已通过反应钩子更改

使用 Lodash.remove 通过 Vuex 突变更改状态不会触发我的 Vue 组件中的反应式重新渲染

属性更改时如何重新渲染反应组件

页面重新渲染/更改后如何维护组件状态?

反应我必须重新渲染父母才能重新渲染孩子吗?