React修改key重挂载Game组件后子组件Clock的ref为null报错问题
问题根因
报错的核心原因是旧Game组件的socket事件监听器泄漏:
每次你通过修改key属性重挂载Game组件时,旧的Game实例会被卸载,但你在componentDidMount中给socket绑定的事件回调没有被同步清除。这些残留的回调仍然持有已经卸载的旧Game实例的引用,而旧实例卸载后内部的myClock和opponentClockref已经被置为null,当socket事件触发时,旧回调执行就会访问null的start/pause方法抛出错误。
新Game实例的ref本身是完全正常的,你在新实例的componentDidMount中可以正常调用ref方法就是证明。
解决方案
给Game类组件补充卸载逻辑,移除所有绑定的socket事件即可,注意移除事件时传入的回调必须和绑定时的是同一个引用,不能直接移除匿名函数,需要提前把回调定义为组件实例方法:
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(); // 提前定义socket事件回调并绑定this,保证绑定和移除用同一个引用 this.handleNewMove = this.handleNewMove.bind(this); // 其他socket事件(比如surrender)的回调也按同样方式定义 } // 抽离newMove事件的处理逻辑 handleNewMove(data) { if(data.isPlayerANext===this.props.isPlayerA){ this.opponentClock.current?.pause() this.myClock.current?.start() } else{ this.opponentClock.current?.start() this.myClock.current?.pause() } } componentDidMount(){ if(this.props.isPlayerA){ this.myClock.current.start() } else{ this.opponentClock.current.start() } // 绑定事件用提前定义的实例方法 socket.on('newMove', this.handleNewMove) // 其他socket事件同理绑定 // socket.on('surrender', this.handleSurrender) ... } // 新增卸载生命周期,清除所有绑定的socket事件 componentWillUnmount() { socket.off('newMove', this.handleNewMove) // 其他注册的socket事件也同步移除 // socket.off('surrender', this.handleSurrender) } render(){ return( <React.Fragment> <Clock ref={this.opponentClock} .../> <Board gameBoard={this.state.gameBoard} .../> <Clock ref={this.myClock} .../> </React.Fragment>) ... } }
额外优化建议
- 给ref访问加上可选链操作符
?.做防御性兼容,极端场景下也不会直接抛出错误导致应用崩溃 - 全局单例的socket绑定的所有事件都要遵循「谁绑定谁卸载」的原则,避免后续再出现同类的事件泄漏问题
内容的提问来源于stack exchange,提问作者jesse
相关产品推荐
相关产品推荐

