React 初心者がハマりがちな罠とその回避法 7 選

## はじめに

React で開発を進める上で、ハマりがちな罠や非効率な書き方、そしてパフォーマンス低下に繋がるようなアンチパターンは数多く存在します。
本記事では、React を始めた初心者の方がハマりがちな罠とその回避法を Bad/Good 例とともに解説します。
目次を載せておきますので、気になるところがあればぜひジャンプして見てみてください🖐️

- [その1：state はすぐに更新されるわけではない](#その1state-はすぐに更新されるわけではない)
- [その2：&& の左辺に数値を置く](#その2-の左辺に数値を置く)
- [その3：複数の state を用意する](#その3複数の-state-を用意する)
- [その4：不要なエフェクトを用意する](#その4不要なエフェクトを用意する)
- [その5：カスタムフックを使用しない](#その5カスタムフックを使用しない)
- [その6：クロージャが最新状態を見失う](#その6クロージャが最新状態を見失う)
- [その7：useEffect でリフェッチする](#その7useeffect-でリフェッチする)


## その1：state はすぐに更新されるわけではない

### Bad
ボタンを押下すると +1 されるシンプルなカウンタアプリです。

```tsx
const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <>
      <button className="bg-blue-300 p-3 rounded-md" onClick={handleClick}>
        Count Up
      </button>
      <p>{count}</p>
    </>
  );
};
```

ハンドラの中で複数回状態を更新したいケースがあるとします。
count を更新する処理を 2 回行いました。


```tsx
const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <>
      <button className="bg-blue-300 p-3 rounded-md" onClick={handleClick}>
        Count Up
      </button>
      <p>{count}</p>
    </>
  );
};
```

しかし、ボタンを押下したら +2 されることを期待しますが、実際は +1 しかされません。

![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/14325d30-98d4-2ecc-eee6-ff9bee5d6fe3.gif)

これは、**state はすぐに更新されるわけではない**からです。

state をセットしても、それが**実際に更新されるのは次回のレンダー**です。
ボタンを初めて押下する時、state の初期値は 0 なのでハンドラにおいて、`setCount(count + 1)` が呼ばれた後もその時点ではまだ count の値は 0 のままです。

```tsx
const handleClick = () => {
    setCount(0 + 1);
    setCount(0 + 1);
};
```


### Good

こういう時は、**[更新用関数 (updater function)](https://ja.react.dev/reference/react/useState#updating-state-based-on-the-previous-state)** を使用すれば改善できます。更新用関数は、`n => n + 1` の形式で記載します。

先ほどのハンドラを下記のように修正します。

```tsx
const handleClick = () => {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
};
```

これで、ボタンを押下したら +2 されるようになりました。

![counter_good.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/c744d36b-6fdb-9936-3cf4-81f794cfba2d.gif)


## その2：&& の左辺に数値を置く
### Bad
下記のようなコンポーネントがあるとします。

```tsx
const Message: React.FC<{ id: number }> = ({ id }) => {
  return id && <p>{id} New messages</p>;
};
```

呼び出し側で、id に 0 を渡すとどうなるでしょう。
```tsx
<Message id={0} />
```

0 は falty な値なので何も表示されないことを期待したかもしれませんが、実際には 0 が表示されます。

![message_bad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/05c43d4e-1418-e11f-c196-f1ebaed52297.png)

これは、**React 的に 0 が有効な子要素になる** ためです。

JavaScriptでは、&& 演算子を評価する際に、左辺が falsy であれば、**右辺を評価せずにそのまま左辺の値を返します**。

Reactでは、returnされた値が null や undefined や false の場合、何もレンダリングしませんが、0 の場合、数値なので **React はレンダリング可能な子要素と認識し、DOMにレンダリング**します。


### Good

0 の場合、何も表示させたくない場合は、下記のように条件を修正します。

```tsx
const Message: React.FC<{ id: number }> = ({ id }) => {
  return id > 0 && <p>{id} New messages</p>;
};
```

すると、何も表示されなくなりました。

![message_good.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/2456ac52-1962-1e68-d476-9513a8b4160d.png)



## その3：複数の state を用意する
### Bad

名前、メールアドレス、パスワードを入力するフォームがあるとします。
その際、それぞれを state で用意するのは管理する state が増えますし冗長です。

```tsx
const UserForm = () => {
  // 名前用の state
  const [name, setName] = useState("");
  // メール用の state
  const [email, setEmail] = useState("");
  // パスワード用の state
  const [password, setPassword] = useState("");

  // 名前用のハンドラ
  const handleChangeName = (e: ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };

  // メール用のハンドラ
  const handleChangeEmail = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  // パスワード用のハンドラ
  const handleChangePassword = (e: ChangeEvent<HTMLInputElement>) => {
    setPassword(e.target.value);
  };

  return (
    <form className="flex flex-col gap-3">
      <input
        className="border p-3"
        value={name}
        type="text"
        name="name"
        placeholder="name"
        onChange={handleChangeName}
      />
      <input
        className="border p-3"
        value={email}
        type="email"
        name="email"
        placeholder="email"
        onChange={handleChangeEmail}
      />
      <input
        className="border p-3"
        value={password}
        type="password"
        name="password"
        placeholder="password"
        onChange={handleChangePassword}
      />
      <input className="bg-blue-300 p-3 rounded-md" type="submit" />
    </form>
  );
};
```

### Good

state の値をオブジェクトにし、１つで管理できるようにします。
こうすることで管理する state が１つになり、シンプルになりました。

```tsx
const UserForm = () => {
  // フォーム共通の state
  const [userForm, setUserForm] = useState({
    name: "",
    email: "",
    password: "",
  });

  // 名前用のハンドラ
  const handleChangeName = (e: ChangeEvent<HTMLInputElement>) => {
    setUserForm((prev) => ({
      ...prev,
      name: e.target.value,
    }));
  };

  // メール用のハンドラ
  const handleChangeEmail = (e: ChangeEvent<HTMLInputElement>) => {
    setUserForm((prev) => ({
      ...prev,
      email: e.target.value,
    }));
  };

  // パスワード用のハンドラ
  const handleChangePassword = (e: ChangeEvent<HTMLInputElement>) => {
    setUserForm((prev) => ({
      ...prev,
      password: e.target.value,
    }));
  };

  return (
    <form className="flex flex-col gap-3">
      <input
        className="border p-3"
        value={userForm.name}
        type="text"
        name="name"
        placeholder="name"
        onChange={handleChangeName}
      />
      <input
        className="border p-3"
        value={userForm.email}
        type="email"
        name="email"
        placeholder="email"
        onChange={handleChangeEmail}
      />
      <input
        className="border p-3"
        value={userForm.password}
        type="password"
        name="password"
        placeholder="password"
        onChange={handleChangePassword}
      />
      <input className="bg-blue-300 p-3 rounded-md" type="submit" />
    </form>
  );
};
```

また、ハンドラがそれぞれのフォームに対して用意されており冗長なので、こちらも改善したいです。

ハンドラで更新しているオブジェクトの key をハードコードするのではなく、`e.target.name` を指定し、input 要素の name 属性を受け取るようにします。

```tsx
const UserForm = () => {
  // フォーム共通の state
  const [userForm, setUserForm] = useState({
    name: "",
    email: "",
    password: "",
  });

  // フォーム共通のハンドラ
  const handleChangeForm = (e: ChangeEvent<HTMLInputElement>) => {
    setUserForm((prev) => ({
      ...prev,
      [e.target.name]: e.target.value,
    }));
  };

  return (
    <form className="flex flex-col gap-3">
      <input
        className="border p-3"
        value={userForm.name}
        type="text"
        name="name"
        placeholder="name"
        onChange={handleChangeForm}
      />
      <input
        className="border p-3"
        value={userForm.email}
        type="email"
        name="email"
        placeholder="email"
        onChange={handleChangeForm}
      />
      <input
        className="border p-3"
        value={userForm.password}
        type="password"
        name="password"
        placeholder="password"
        onChange={handleChangeForm}
      />
      <input className="bg-blue-300 p-3 rounded-md" type="submit" />
    </form>
  );
};
```

これで、ハンドラを１つに集約することができました。

なんでもかんでも集約すれば良いということではないですが、無駄な state をできるだけ排除することでコードをシンプルに保つことができます。

## その4：不要なエフェクトを用意する
### Bad
ボタンを押下して商品をカートに追加し、商品１つにつき +2000 円し合計を算出するケースがあるとします。
商品数に変化があれば、それを検知して合計値を計算する useEffect を用意したくなるかもしれません。

```tsx
const PRICE_PER_ITEM = 2000;

const Cart = () => {
  const [cart, setCart] = useState(0);
  const [total, setTotal] = useState(0);

  const handleClick = () => {
    setCart(cart + 1);
  };

  useEffect(() => {
    setTotal(cart * PRICE_PER_ITEM);
  }, [cart]);

  return (
    <>
      <button className="bg-blue-300 p-3" onClick={handleClick}>
        Add Item
      </button>
      <p>cart: {cart}</p>
      <p>totalPrice: {total}</p>
    </>
  );
};
```

やりたいことは実現できてそうです。

![cart_bad.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/74ef50c6-341a-eb9a-b840-ea7789718971.gif)

しかし、合計値は useEffect を使用しなくても計算可能です。

### Good

useEffect を排除し、合計値を **レンダー中に計算** します。

```tsx
const PRICE_PER_ITEM = 2000;

const Cart = () => {
  const [cart, setCart] = useState(0);
  const total = cart * PRICE_PER_ITEM;

  const handleClick = () => {
    setCart(cart + 1);
  };

  return (
    <>
      <button className="bg-blue-300 p-3" onClick={handleClick}>
        Add Item
      </button>
      <p>cart: {cart}</p>
      <p>totalPrice: {total}</p>
    </>
  );
};
```

こうすることで、(無駄な useEffect を排除できたので)早く、シンプルになり、バグも起きにくくなりました。


## その5：カスタムフックを使用しない
### Bad

ネットワークのオンライン/オフラインを検知し、画面に表示するケースがあるとします。

グローバルの [online](https://developer.mozilla.org/ja/docs/Web/API/Window/online_event) および [offline](https://developer.mozilla.org/ja/docs/Web/API/Window/offline_event) イベントにリスナを登録し、state を更新するエフェクトを用意します。

```tsx
const Home = () => {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    // ネットワークがオンラインになった時に発火
    const handleOnline = () => setIsOnline(true);

    // ネットワークがオフラインになった時に発火
    const handleOffline = () => setIsOnline(false);

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    // アンマウント時に online と offline イベントリスナーを削除してメモリリークを防ぐ
    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return <h1>{isOnline ? "Online" : "Offline"}</h1>;
};
```

ここで、別のコンポーネントでも同じロジックを使用したくなったとします。

ボタンコンポーネントに、ネットワークがオンラインの間は “Save” を、オフラインの間は “Reconnecting…” と表示されて無効になるような保存ボタンを実装したいとします。

その際、上記ですでに実装したロジックが使えるので下記のようにするかもしれません。

```tsx
const SaveButton = () => {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);

    const handleOffline = () => setIsOnline(false);

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  const handleSaveClick = () => console.log("button clicked");

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? "Save" : "Reconnecting..."}
    </button>
  );
};
```

しかし、似たようなロジックを複数箇所で記載するのは冗長ですし、[DRYの原則](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)に違反しています。

### Good

重複しているロジックを移動して、カスタムフックを作成しましょう。
カスタムフック名は [`use` から始める](https://ja.react.dev/learn/reusing-logic-with-custom-hooks#hook-names-always-start-with-use)必要があります。

```tsx
const useNetworkStatus = () => {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);

    const handleOffline = () => setIsOnline(false);

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return isOnline;
};
```

これで、共通するロジックを１つのカスタムフックに共通化できました。
先ほどのコンポーネントから `useNetworkStatus` を呼びせばOKです。

```tsx
const Home = () => {
  const isOnline = useNetworkStatus();

  return <h1>{isOnline ? "オンライン" : "オフライン"}</h1>;
};
```

```tsx
const SaveButton = () => {
  const isOnline = useNetworkStatus();

  const handleSaveClick = () => console.log("button clicked");

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? "Save" : "Reconnecting..."}
    </button>
  );
};

```

こうすることで、コンポーネント間のロジックの重複が減り、ブラウザ API とのやり取りに関する詳細を隠蔽することができました。

## その6：クロージャが最新状態を見失う
### Bad
1秒ごとにカウントアップするケースを考えます。
useEffect 内で 1 秒ごとに +1 するコードを書きます。

```tsx
const StaleClosure = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, []);

  return <p>Count: {count}</p>;
};
```

しかし、初回に +1 されて以降、変化がありません。

![stale-closure_bad.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/6d2764b5-5498-7f1e-2cd7-4f99294d83df.gif)

これは、依存配列に `[]` を渡しているので、コンポーネントがマウントされるときに一度だけ useEffect の第一引数に渡した関数が実行されるためです。マウント時に一度だけなので、**関数が再作成されることはありません**。

countの初期値は 0 なので、この状態だと、1 秒ごとに実行されるのはずっと 0 + 1 になります。

```tsx
setInterval(() => {
  setCount(0 + 1);
}, 1000);
```

[クロージャ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures)内で新しい状態を参照するには、**関数を再作成させる必要があります**。

### Good

依存配列に count を追加し、countの値に変化があれば関数を再作成するようにします。

```tsx
const StaleClosure = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, [count]);

  return <p>Count: {count}</p>;
};
```

こうすることで、1秒ごとにカウントアップすることができます。

![stale-closure_good_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/0df2174b-e852-bf53-26f1-863036e9a781.gif)

うまく動いていそうな気がしますが、少し経つと挙動がおかしくなります。

![stale-closure_good_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/c0325427-7894-d706-59ac-822cc8b2a7ec.gif)

これは、count に変化があるたびに、別の setInterval を登録し続けているためです。
新しい setInterval を作成する前に、前回の setInterval をキャンセル必要があります。

具体的には、setIntervalで返されるid を保持しておいて、[クリーンアップ関数](https://ja.react.dev/reference/react/useEffect)内でクリアするようにします。

```tsx
const StaleClosure = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);

    return () => {
      clearInterval(id);
    };
  }, [count]);

  return <p>Count: {count}</p>;
};
```

これで正常に動作するようになります。

と、ここまで長々と解説しましたが、React の更新用関数を渡すことでも問題なく動作します。

```tsx
const StaleClosure = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setCount((prev) => prev + 1);
    }, 1000);
  }, []);

  return <p>Count: {count}</p>;
};
```

## その7：useEffect でリフェッチする
### Bad
ボタンを押下すると id が更新され、その id に紐づく post を useEffect で再取得する例を考えます。

```tsx
const RefetchInUseEffect = () => {
  const [id, setId] = useState(1);
  const [text, setText] = useState("");

  const handleClick = () => {
    setId(Math.floor(Math.random() * 10));
  };

  useEffect(() => {
    fetch(`https://jsonplaceholder.org/posts/${id}`)
      .then((response) => response.json())
      .then((data) => setText(data.content));
  }, [id]);

  return (
    <>
      <button className="bg-blue-300 p-3 rounded-md" onClick={handleClick}>
        Show post
      </button>
      <div>{text}</div>
    </>
  );
};
```

ボタンを押下するたびに id が更新され、id に変化があると useEffect 内で post が再取得されます。

![refetch-in-useEffect_bad.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/791f4ccf-47ea-aec5-8502-31ca72bddf35.gif)

うまく動作しているように見えますが、ボタンを連続で押下すると表示するテキストがちらつきます。
ネットワークが低速の状態だとわかりやすいです。

![refetch-in-useEffect_bad_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/ae1048ae-8a43-d086-855a-816e98ea9063.gif)

これは単純に間髪入れずにボタンを 3 回押下すると、id が3回変更され、3回リフェッチされるためです。

このちらつきを防ぐために、前の fetch 呼び出しをキャンセルし、最後のクリックだけを確認するようにする必要があります。


### Good

[AbortController](https://developer.mozilla.org/ja/docs/Web/API/AbortController) を作成し、インスタンスの [signal プロパティを fetch リクエストに渡します](https://developer.mozilla.org/ja/docs/Web/API/AbortController/signal)。
useEffect が再実行される前にクリーンアップ関数で `controller.abort()` を呼び、fetch がリクエストをキャンセルするようにします。


```tsx
const RefetchInUseEffect = () => {
  const [id, setId] = useState(1);
  const [text, setText] = useState("");

  const handleClick = () => {
    setId(Math.floor(Math.random() * 10));
  };

  useEffect(() => {
    const controller = new AbortController();

    fetch(`https://jsonplaceholder.org/posts/${id}`, {
      signal: controller.signal,
    })
      .then((response) => response.json())
      .then((data) => setText(data.content));

    return () => controller.abort();
  }, [id]);

  return (
    <>
      <button className="bg-blue-300 p-3 rounded-md" onClick={handleClick}>
        Show post
      </button>
      <div>{text}</div>
    </>
  );
};
```

こうするとことで、間髪入れずに連続でボタンを押下しても、最後のクリックだけに反応し、テキストが表示されるようになりました。

![refetch-in-useEffect_good.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1042715/3bd20dfb-6e2d-7273-da21-420ade3e9b42.gif)


## まとめ
この記事では、React を始めた頃に知っておきたかったことを Bad/Good 例とともに 7 つ紹介しました。

1. **state の更新**: state はすぐに更新されるわけではないので、更新関数を使用する
2. **条件式**: `&&` 演算子の左辺に数値を置く際は、意図しないレンダリングを防ぐために注意が必要
3. **state の管理**: 複数のstateを個別に管理するのではなく、オブジェクトにまとめることでコードをシンプルに保つ
4. **useEffect の利用**: 不要な useEffect は排除し、レンダー中に計算できるものはレンダー中に処理する
5. **カスタムフック**: コンポーネント間で共通のロジックを共有する場合は、カスタムフックを活用する
6. **クロージャ**: クロージャ内で最新の state を参照するには、依存配列を正しく設定し、必要に応じてクリーンアップ関数で処理をキャンセルする
7. **データの再取得**: useEffect でデータを取得する際は、AbortController を使用してリクエストをキャンセルし、不要な再取得を防ぐ

何か1つでも参考になりましたら幸いです！

## 参考
- [react.dev](https://ja.react.dev/)
- [All 12 useState & useEffect Mistakes Junior React Developers Still Make in 2024](https://www.youtube.com/watch?v=-yIsQPp31L0)