For a 1v1 Sudoku game, my GamePage
component renders the main Game
component, which contains a Clock
for each player. When both players agree to a rematch, the entire Game
is reset by simply incrementing its key
by 1 (after changing the GamePage state to reflect the settings of the new game).
My Problem:
Game
stores two refs this.myClock
and this.opponentClock
to the countdowns inside of both clocks, so they can be paused/started when a player fills a square. This works perfectly fine for the first game. However, after Game
remounts, any move will throw "Cannot read properties of null (reading 'start')" at e.g. this.opponentClock.current.start()
.
I know that refs are set to null when a component unmounts, but by rendering a new version of Game
, I would expect them to be set in the constructor again. To my surprise, the new timers are set correctly and one of them is running (which is also done in componentDidMount
of Game
using the refs), but any access afterwards breaks the app.
I would be incredibly grateful for any tips or remarks about possible causes, I've been stuck on this for two days now and I'm running out of things to 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);
CodePudding user response:
I am very happy to let you know that after around 2 hours of debugging (lol) I have found the source of your problem.
The problem is you were not cleaning up your socket.on functions on component unmount so the old ones were still there with references to old refs.
Look at the way I am doing it here, to clean up the functions and your problem will be solved:
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);
}