反应:通过更改键重新安装后,对子组件的引用为空
React: ref to child component is null after remounting by changing key
对于 1v1 数独游戏,我的 GamePage
组件呈现主 Game
组件,其中包含每个玩家的 Clock
。当两个玩家都同意重新比赛时,整个 Game
通过简单地将其 key
增加 1 来重置(在更改 GamePage 状态以反映新游戏)。
我的问题:
Game
将两个引用 this.myClock
和 this.opponentClock
存储到两个时钟内的倒计时,因此当玩家填满一个方块时它们可以是 paused/started。这对于第一场比赛非常有效。但是,在 Game
重新安装后,任何移动都会抛出“无法读取 null 的属性(读取 'start')”,例如this.opponentClock.current.start()
.
我知道当组件卸载时 refs 被设置为 null,但是通过呈现 Game
的新版本,我希望它们在构造函数中再次设置。令我惊讶的是,新计时器设置正确,其中之一是 运行(也使用 refs 在 Game
的 componentDidMount
中完成),但之后的任何访问都会破坏应用程序。
对于任何关于可能原因的提示或评论,我将非常感激,我已经坚持了两天了,我 运行 无法 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);
}
对于 1v1 数独游戏,我的 GamePage
组件呈现主 Game
组件,其中包含每个玩家的 Clock
。当两个玩家都同意重新比赛时,整个 Game
通过简单地将其 key
增加 1 来重置(在更改 GamePage 状态以反映新游戏)。
我的问题:
Game
将两个引用 this.myClock
和 this.opponentClock
存储到两个时钟内的倒计时,因此当玩家填满一个方块时它们可以是 paused/started。这对于第一场比赛非常有效。但是,在 Game
重新安装后,任何移动都会抛出“无法读取 null 的属性(读取 'start')”,例如this.opponentClock.current.start()
.
我知道当组件卸载时 refs 被设置为 null,但是通过呈现 Game
的新版本,我希望它们在构造函数中再次设置。令我惊讶的是,新计时器设置正确,其中之一是 运行(也使用 refs 在 Game
的 componentDidMount
中完成),但之后的任何访问都会破坏应用程序。
对于任何关于可能原因的提示或评论,我将非常感激,我已经坚持了两天了,我 运行 无法 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);
}