I am implementing the client side for a tic-tac-toe web game.
I have 2 typescript files:
When testing I get the following error:
TicTacToeAPI.tsx:5 Warning: React has detected a change in the order of Hooks called by TicTacToe. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render | Next render | |
---|---|---|
1. | useState | useState |
2. | useState | useState |
3. | useState | useState |
4. | useState | useRef |
I'm not sure what the standard way to resolve this issue is.
Here are the files:
TicTacToe.tsx
import React, { useState } from "react";
import TicTacToeAPI from "./TicTacToeAPI";
export default function TicTacToe() {
const [menuState, setMenuState] = useState("login");
const [nickname, setNickname] = useState("");
const [roomNumber, setRoomNumber] = useState(0);
return (
<div className="TicTacToe">
<h1>Tic Tac Toe</h1>
{(() => {
switch (menuState) {
case "login":
return input(setMenuState, setNickname, setRoomNumber);
case "game":
return game(setMenuState, roomNumber, nickname);
case "end":
return end();
default:
return null;
}
})()}
</div>
);
}
function game(
setMenuState: React.Dispatch<React.SetStateAction<string>>,
roomNumber: number,
nickname: string
) {
return (
<>
<div id="metaData">
<p>Room number: {roomNumber}</p>
<p>Nickname: {nickname}</p>
</div>
{TicTacToeAPI(nickname, roomNumber)}
<div id="abandonButton">
<button onClick={() => setMenuState("login")}>rage quit</button>
</div>
</>
);
}
function end() {
return <p>Game ended</p>;
}
function input(
setMenuState: React.Dispatch<React.SetStateAction<string>>,
setNickname: React.Dispatch<React.SetStateAction<string>>,
setRoomNumber: React.Dispatch<React.SetStateAction<number>>
) {
const [formData, setFormData] = useState({
nickname: "genericName",
roomNumber: 420,
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.id]: e.target.value });
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// validate input
if (!formData.nickname || !formData.roomNumber) {
alert("Please enter a nickname and room number");
return;
}
// set parameters
setNickname(formData.nickname);
setRoomNumber(formData.roomNumber);
// change menu state
setMenuState("game");
};
return (
<div id="playerInfoInput">
<form className="playerInfo" onSubmit={handleSubmit}>
<h3>Nickname</h3>
<input
type="text"
id="nickname"
value={formData.nickname}
onChange={handleInputChange}
/>
<br />
<h3>Room number</h3>
<input
type="number"
id="roomNumber"
value={formData.roomNumber}
onChange={handleInputChange}
/>
<br />
<input type="submit" value="Submit" />
</form>
</div>
);
}
TicTacToeAPI.tsx
import React, { useEffect, useRef, useState } from "react";
export default function TicTacToeAPI(nickname: string, roomNumber: number) {
//websocket
const wsRef = useRef(new WebSocket("ws://localhost:8080/ticTacToe"));
//set other data
const [authToken, setAuthToken] = useState("");
(...)
//websocket
useEffect(() => {
//configure message handler
wsRef.current.onmessage = (message) => {
handleMessage(message, stateData);
};
// Cleanup WebSocket connection when the component unmounts
return () => {
wsRef.current.close();
};
}, []);
return (<p>board</p>)
}
According to my research since the hooks are in their own function components so this switching shouldn't be an issue, but apparently I have misunderstood something.
I'm surprised you are not getting the React hooks error that hooks are being called from nested functions. None of TicTacToeAPI
, input
, or game
are valid React function components.
Update the code such that these functions are valid React components and that they are rendered as JSX instead of being directly invoked. React function components take a single props object, the various props can be destructured from it.
Example:
TicTacTieAPI.tsx
import React, { useEffect, useRef, useState } from "react";
interface TicTacToeApiProps {
nickname: string;
roomNumber: number;
};
export default function TicTacToeAPI({ nickname, roomNumber }: TicTacToeApiProps) {
//websocket
const wsRef = useRef(new WebSocket("ws://localhost:8080/ticTacToe"));
//set other data
const [authToken, setAuthToken] = useState("");
(...)
//websocket
useEffect(() => {
//configure message handler
wsRef.current.onmessage = (message) => {
handleMessage(message, stateData);
};
// Cleanup WebSocket connection when the component unmounts
return () => {
wsRef.current.close();
};
}, []);
return (<p>board</p>)
}
TicTacToe.tsx
import React, { useState } from "react";
import TicTacToeAPI from "./TicTacToeAPI";
interface GameProps {
setMenuState: React.Dispatch<React.SetStateAction<string>>;
roomNumber: number;
nickname: string;
}
function Game({ setMenuState, roomNumber, nickname }: GameProps) {
return (
<>
<div id="metaData">
<p>Room number: {roomNumber}</p>
<p>Nickname: {nickname}</p>
</div>
<TicTacToeAPI nickname={nickname} roomNumber={roomNumber} /> // <-- JSX!
<div id="abandonButton">
<button onClick={() => setMenuState("login")}>rage quit</button>
</div>
</>
);
}
function End() {
return <p>Game ended</p>;
}
interface InputProps {
setMenuState: React.Dispatch<React.SetStateAction<string>>;
setNickname: React.Dispatch<React.SetStateAction<string>>;
setRoomNumber: React.Dispatch<React.SetStateAction<number>>;
}
function Input({ setMenuState, setNickname, setRoomNumber }: InputProps) {
const [formData, setFormData] = useState({
nickname: "genericName",
roomNumber: 420,
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.id]: e.target.value });
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// validate input
if (!formData.nickname || !formData.roomNumber) {
alert("Please enter a nickname and room number");
return;
}
// set parameters
setNickname(formData.nickname);
setRoomNumber(formData.roomNumber);
// change menu state
setMenuState("game");
};
return (
<div id="playerInfoInput">
<form className="playerInfo" onSubmit={handleSubmit}>
<h3>Nickname</h3>
<input
type="text"
id="nickname"
value={formData.nickname}
onChange={handleInputChange}
/>
<br />
<h3>Room number</h3>
<input
type="number"
id="roomNumber"
value={formData.roomNumber}
onChange={handleInputChange}
/>
<br />
<input type="submit" value="Submit" />
</form>
</div>
);
}
export default function TicTacToe() {
const [menuState, setMenuState] = useState("login");
const [nickname, setNickname] = useState("");
const [roomNumber, setRoomNumber] = useState(0);
const renderUi = () => {
switch (menuState) {
case "login":
return (
<Input
setMenuState={setMenuState}
setNickname={setNickname}
setRoomNumber={setRoomNumber}
/>
);
case "game":
return <Game {...{ setMenuState, roomNumber, nickname }} />;
case "end":
return <End />;
default:
return null;
}
};
return (
<div className="TicTacToe">
<h1>Tic Tac Toe</h1>
{renderUi()}
</div>
);
}