🪝

[React] カスタムフックの独立性

2024/12/23に公開

概要

React のカスタムフックを複数回呼び出した際、それらが互いに独立していることを検証します。

検証内容としては、カスタムフック内部の state や useEffect 内の副作用などが干渉し合わないか、といった内容になります。

カスタムフックの独立性については公式に以下の記載があります。

カスタムフックは、state 自体ではなく、state を扱うロジックを共有できるようにするためのものです。フックの呼び出しは、同じフックの他の場所からの呼び出しとは完全に独立しています。

カスタムフックでロジックを再利用する

こちらについてさらに掘り下げ、カスタムフック呼び出し元が単一コンポーネントの場合と複数のコンポーネントにまたがる場合、さらにそれぞれについてカスタムフックが引数を取る場合と取らない場合について調査しました。

検証

1. カスタムフックをあるコンポーネント内で複数回呼んだ場合

a. カスタムフックが引数を持つ場合

先ほどの公式で挙げられている例を拡張し、以下のような React アプリを作成します。

import "./App.css";
import Form1 from "./components/Form1";

function App() {
  return <Form1 />;
}

export default App;
import { useFormInput } from "../customHooks/useFormInput";

const Form1 = () => {
  const firstNameProps = useFormInput("Mary");
  const familyNameProps = useFormInput("Poppins");

  return (
    <>
      <div>
        <label>
          First name:
          <input
            value={firstNameProps.value}
            onChange={firstNameProps.onChange}
          />
        </label>
      </div>
      <div>
        <label>
          Family name:
          <input
            value={familyNameProps.value}
            onChange={familyNameProps.onChange}
          />
        </label>
      </div>
      <p>
        <b>
          Good morning, {firstNameProps.value} {familyNameProps.value}.
        </b>
      </p>
    </>
  );
};

export default Form1;
import { SetStateAction, useEffect, useState } from "react";

export const useFormInput = (initialValue?: string) => {
  const [value, setValue] = useState<string>(initialValue ?? "");

  function handleChange(e: { target: { value: SetStateAction<string> } }) {
    setValue(e.target.value);
  }

  useEffect(() => {
    console.log(initialValue);
  });

  const inputProps = {
    value: value,
    onChange: handleChange,
  };

  return inputProps;
};

カスタムフック useFormInput は state である value とそれを変更する関数 onChange を提供し、さらに useEffect により初回レンダー後に処理を行います。

初回レンダー後、以下のように useEffect で指定した処理(console.log)が2 度呼び出されます。(初回レンダーを 1 度だけにするため strick モードはオフにしています。)

これはソースコードの以下の箇所にあたます。

const firstNameProps = useFormInput("Mary");
const familyNameProps = useFormInput("Poppins");

firstNameProps と familyNameProps でそれぞれ独立して内部の useEffect が呼び出されたため、 Mary と Poppins が表示されていることがわかります。

また入力フォームの内容を変えるとそれぞれ独立して表示が変わることから、それぞれの state の独立性も確認できます。

b. カスタムフックが引数を持たない場合

カスタムフックに引数を渡さないパターンについても検証を行いました。

- const firstNameProps = useFormInput("Mary");
- const familyNameProps = useFormInput("Poppins");
+ const firstNameProps = useFormInput();
+ const familyNameProps = useFormInput();

firstNameProps と familyNameProps には全く同じ関数の返り値が渡されており、互いに干渉し合ってもおかしくないような印象を受けます。

しかし実際には以下に示すように、useEffect は2度呼ばれ、state は独立して管理されます。

ここでもカスタムフックが互いに独立していることが確認できました。

2. カスタムフックを別々のコンポーネント内でそれぞれ呼んだ場合

c. カスタムフックが引数を持つ場合

今度は以下のように、別々のコンポーネントがカスタムフックを呼ぶ場合を検証します。

import FamilyName from "./FamilyName";
import FirstName from "./FirstName";

const Form2 = () => {
  return (
    <>
      <FirstName initialValue="Mary" />
      <FamilyName initialValue="Poppins" />
    </>
  );
};

export default Form2;
import { useFormInput } from "../customHooks/useFormInput";

const FirstName = ({ initialValue }: { initialValue?: string }) => {
  const firstNameProps = useFormInput(initialValue);

  return (
    <div>
      <label>
        Fist Name:
        <input
          value={firstNameProps.value}
          onChange={firstNameProps.onChange}
        />
      </label>
      <label>{firstNameProps.value}</label>
    </div>
  );
};

export default FirstName;
import { useFormInput } from "../customHooks/useFormInput";

const FamilyName = ({ initialValue }: { initialValue?: string }) => {
  const FamilyNameProps = useFormInput(initialValue);

  return (
    <div>
      <label>
        Family Name:
        <input
          value={FamilyNameProps.value}
          onChange={FamilyNameProps.onChange}
        />
      </label>
      <label>{FamilyNameProps.value}</label>
    </div>
  );
};

export default FamilyName;

コンポーネント FirstName と FamilyName からそれぞれカスタムフック useFormInput を呼び出しています。

この場合もやはり、useEffect 内の処理や state はそれぞれ独立して働くこと、つまりカスタムフックがそれぞれ独立していることが確認できます。

d. カスタムフックが引数を持たない場合

2-c のソースから以下のように変更し、カスタムフックが引数を持たない場合を考えます。

-   const firstNameProps = useFormInput(initialValue);
+   const firstNameProps = useFormInput();
-   const familyNameProps = useFormInput(initialValue);
+   const familyNameProps = useFormInput();

別々のコンポーネントから全く同じカスタムフックが呼ばれますが、やはりこれらも独立性が保たれます。

検証結果

カスタムフックを同一コンポーネントから呼ぶ場合と別々のコンポーネントから呼ぶ場合、さらにそれぞれ引数を取る場合と取らない場合を検証しましたが、全てのパターンでカスタムフックは互いに独立していることが検証できました。

まとめと考え方

カスタムフックは、state 自体ではなく、state を扱うロジックを共有できるようにするためのものです。

冒頭で挙げた公式の記載の通り、カスタムフックはあくまで特定のロジックを切り出して再利用できるようにしたものであり、特定のカスタムフックを複数回呼び出した際もそれぞれが保持する state 等は互いに独立しています。

また、

カスタムフックでロジックを再利用する(公式)

React には useState、useContext、useEffect など複数の組み込みフックが存在します。しかし、データの取得やユーザのオンライン状態の監視、チャットルームへの接続など、より特化した目的のためのフックが欲しいこともあります。React にこれらのフックはありませんが、アプリケーションの要求に合わせて独自のフックを作成することが可能です。

公式に記載のあるように、カスタムフックは useState や useEffect などの組み込みのフックと同等に扱うことのできる自作フックです。

よって以下のコードの useState や useEffect がそれぞれ独立して作用するのと同じように、カスタムフックも互いに独立していることが保障されている、という考え方が可能です。

const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
useEffect(() => {
  console.log("foo");
}, []);

useEffect(() => {
  console.log("bar");
}, []);
株式会社ブレイクエッジ 技術ブログ

Discussion