reactjstypescriptreact-hooksjsx

How to dynamically change displayed content(with state) without violating reacts hookuse rules?


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.


Solution

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