この記事を作った動機

 React で useState なり useEffect なり hooks と呼ばれている状態変数と関係があるものを使うときは厳し目の条件があるようで、それが原因で動かないということがあった。

 そこでどこに気をつければ良さそうか、公式ドキュメントを見たり、調べてみたりして、自分なりの理解でとりあえず記録しようということになった。

環境

  • Vite
  • React
  • TypeScript
  • Tailwind CSS
  • zustand

hooks とは

 use〇〇 という感じで定義されている React の API 群の事っぽい。現状の体感としては、状態変数に関係する React の API のことを包括的に hooks とよんでいるように見える。以下は hooks の例である。

  • useState
  • useRef
  • useEffect
  • 状態変数管理ライブラリで使う関数の一部?
     zustand の例
// 定義 ---------------------------------------------
// 定義 ---------------------------------------------
import { useEffect, useState } from "react"
import { create } from "zustand";

type DatabaseState = {
  websocket: WebSocket | null;
  serverIP: string | null;
  changeServer: (ip: string) => void;
  closeConnection: () => void;
  getWebsocket: () => WebSocket | null;
};

// hooks 相当の部分の定義
export const useDatabaseStore = create<DatabaseState>((set, get) => ({
  websocket: null,
  serverIP: "ws://localhost:50097",

  changeServer: (ip: string) => {
    set({ serverIP: ip });
  },

  closeConnection: () => {
    const ws = get().websocket;
    ws?.close();
    set({ websocket: null });
  },

  getWebsocket: () => get().websocket,
}));
// 定義 ---------------------------------------------
// 定義 ---------------------------------------------

export function Acompornent(){
    // hooks 相当のコード
    const websocket = useDatabaseStore((s) => s.websocket);

    // 何かしらのコード
}
  • use〇〇 から始まる APIを組み合わせた関数
// 定義 ---------------------------------------------
// 定義 ---------------------------------------------
//      上記の ”zustand の例” と同じ
// 定義 ---------------------------------------------
// 定義 ---------------------------------------------

// useEffect の React API としての hooks や
// useDatabaseStore の zustand (状態変数管理ライブラリの関数) としての hooks 
// を組み合わせてた 任意の定義の hooks の例
export function useDatabaseEffects() {
  const serverIP = useDatabaseStore((s) => s.serverIP);
  const setWebsocket = useDatabaseStore.setState;

  useEffect(() => {
    if (!serverIP) return;

    const ws = new WebSocket(serverIP);
    setWebsocket({ websocket: ws });

    // return () => {
    //   ws.close();
    // };
  }, [serverIP, setWebsocket]);
}

気にしたほうが良さそうなこと

  • コンポーネント内に hooks は書く
  • コンポーネント内のなるべく先頭に hooks は書く
  • コンポーネントの関数内のスコープからハズレたところには hooks は定義、宣言はできない。以下の部分が基本的にポイントな気がする。
    • if 文の中
    • loop 内
    • コールバック関数など、コンポーネント関数のスコープから外れるところ
    • レンダリング部分内部
  • hooks で定義された状態変数の操作などは、コンポーネント関数内とかに限らず、コールバック内で呼び出したり、書き込んだり比較的自由にできる模様である。
// 何かしらのコード

export default function Selector() {
    const websocket             = useDatabaseStore((s) => s.websocket);
    const [visible,setVisible]  = useState(false)
    const init                  = useRef(true)
    const toolbarAddTool        = useToggleableStore((s) => s.addToggleable) // zustand (状態変数管理ライブラリ) も関係ある
    const [index, setIndex]     = useState<Info>({
        status: "init",
        errorMessage: "nothing",
        data: null,
    });

    useEffect(() => {
        if (!websocket) {
            setIndex({
                status: "error",
                errorMessage: "No data server connected to.",
                data: null,
            });
            return;
        }

        const handleMessage = (event: MessageEvent) => {
            const result = JSON.parse(String(event.data));
            console.log(result)
            if (!result.status.includes("error")) {
                setIndex(result);
            } else {
                setIndex({
                    status: result.status,
                    errorMessage: result.errorMessage,
                    data: null,
                });
            }
        };

        const whenOpened = () => {
            const request = JSON.stringify({ command: "info", data: null });
            websocket.send(request);
        }

        websocket.addEventListener("message", handleMessage);
        websocket.addEventListener("open",whenOpened)

        // cleanup
        return () => {
            websocket.removeEventListener("message", handleMessage);
        };
    }, [websocket]);

    // 条件分岐やループ、レンダリングや何かしらのコールバック関数内などで、
    // hooks を宣言したり、定義することはできないという感じの模様
    // しかし、hooks の定義で出てくる set〇〇 などを使ったりして、状態変数を操作したりなどはできる模様
    if(init.current){
        const toggleable:toggleable = {
            name: "Selector",
            setVisibility: setVisible,
            visibility:visible
        }
        toolbarAddTool(toggleable)
        init.current = false
    }

    // 何かしらのコード

}

エラーの発生と修正例

 実際に私がOnenote代替品の作成でコードを書いているときに起こったエラーについて、記録してみる。

 具体的な内容としては if 文 の中に、zustand の状態変数の定義、宣言をしたために、正しく hooks が動作しなかったという内容になっていると思う。

 私的には、Rendered fewer hooks than expected. と怒られている部分が気になった。これはつまり、if 文内に hooks の定義や宣言を書いてしまうと、条件分岐次第でコンポーネント内の hooks の数が変わってしまうということに、つながっているのではないかと考えてみた。

hooks の条件を満たしていないときに見られるエラー例

ToggleToolsBar.tsx:21 Cannot update a component (`ToolsBar`) while rendering a different component (`Selector`). To locate the bad setState() call inside `Selector`, follow the stack trace as described in https://react.dev/link/setstate-in-render
react-dom_client.js?v=79552f97:4247 Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
    at finishRenderingHooks (react-dom_client.js?v=79552f97:4247:17)
    at renderWithHooks (react-dom_client.js?v=79552f97:4225:9)
    at updateFunctionComponent (react-dom_client.js?v=79552f97:6617:21)
    at beginWork (react-dom_client.js?v=79552f97:7652:20)
    at runWithFiberInDEV (react-dom_client.js?v=79552f97:1483:72)
    at performUnitOfWork (react-dom_client.js?v=79552f97:10866:98)
    at workLoopSync (react-dom_client.js?v=79552f97:10726:43)
    at renderRootSync (react-dom_client.js?v=79552f97:10709:13)
    at performWorkOnRoot (react-dom_client.js?v=79552f97:10357:46)
    at performSyncWorkOnRoot (react-dom_client.js?v=79552f97:11633:9)
window.tsx:29 An error occurred in the <Selector> component.

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

修正前


export default function Selector() {
    const websocket = useDatabaseStore((s) => s.websocket);
    const [visible,setVisible] = useState(false)
    const init = useRef(true)

    const [index, setIndex] = useState<Info>({
        status: "init",
        errorMessage: "nothing",
        data: null,
    });

    // 何かしらのコード

    // if 文 内部で hooks を定義、宣言してしまっている例
    if(init.current){

        // 問題の部分 ----------------------------------
        // 問題の部分 ----------------------------------
        const toolbarAddTool = useToggleableStore((s) => s.addToggleable)
        // 問題の部分 ----------------------------------
        // 問題の部分 ----------------------------------

        const toggleable:toggleable = {
            name: "Selector",
            setVisibility: setVisible,
            visibility:visible
        }
        toolbarAddTool(toggleable)
        init.current = false
    }

    // 何かしらのコード
}

修正後

export default function Selector() {
    const websocket = useDatabaseStore((s) => s.websocket);
    const [visible,setVisible] = useState(false)
    const init = useRef(true)

    // 修正した部分 ---------------------------------
    // 修正した部分 ---------------------------------
    const toolbarAddTool = useToggleableStore((s) => s.addToggleable)
    // 修正した部分 ---------------------------------
    // 修正した部分 ---------------------------------

    const [index, setIndex] = useState<Info>({
        status: "init",
        errorMessage: "nothing",
        data: null,
    });

    // 何かしらのコード

    if(init.current){
        const toggleable:toggleable = {
            name: "Selector",
            setVisibility: setVisible,
            visibility:visible
        }
        toolbarAddTool(toggleable)
        init.current = false
    }

    // 何かしらのコード
}

参考にしたサイトとか