🈲

ReactのuseEffectは極力使わないで

に公開

useEffectはあまり使わないほうがいい

まず初めに、useEffectはできるだけ使わないほうがいいです。
なぜならコードが読みやすくなり、実行速度が向上し、エラーが発生しにくくなるからです。
これはReact公式ドキュメントで明言されています。

React公式ドキュメントで明言されているエフェクトが不要なケースは2つあります。
今回はこの2つのケースをわかりやすく嚙み砕いて紹介します。

1.レンダーのためのデータ変換にエフェクトは必要ありません。

まず一つ目に書かれていたのがこのケースです。
レンダーのためのデータ変換とは、画面に表示するためにデータを整形・加工することを指します。

ではまず悪い例から見ていきます。

悪い例:useEffectでフィルター結果をstateに入れてしまう

import { useEffect, useState } from 'react';

function UserList({ users, searchQuery }) {
  const [filteredUsers, setFilteredUsers] = useState([]);

  useEffect(() => {
    const result = users.filter(user =>
      user.name.toLowerCase().includes(searchQuery.toLowerCase())
    );
    setFilteredUsers(result);
  }, [users, searchQuery]);

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

問題点

  • useEffectでfilteredUsersというstateを更新。
  • これにより、不要な再レンダリングが発生する可能性がある。
  • Reactの「レンダー → DOM更新 → エフェクト → state更新 → 再レンダー」の無駄なループを作ってしまう。

次にいい例を見ていきます。

良い例:直接レンダー中にフィルタリングする

function UserList({ users, searchQuery }) {
  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

利点

  • エフェクト不要。
  • 毎回usersかsearchQueryが変われば自動的に再計算される。
  • Reactのレンダーの流れに沿って効率的。

この「毎回usersかsearchQueryが変われば自動的に再計算される。」の部分ですが、僕自身も最近までuseEffectを使わないと更新されないと思い込んでいて、アンチパターンを採用してしまっていました。

2.ユーザイベントの処理にエフェクトは必要ありません。

2つ目に紹介されていたのはこちらのケースです。
多くの人(偏見)が「ボタンをクリックしたら useEffect で何かしよう」と考えがちですが、イベント処理は直接イベントハンドラの中で行うべきです。
まずは悪い例を見ていきます。

悪い例:useEffectでボタンクリックの結果を処理している

import { useEffect, useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [clicked, setClicked] = useState(false);

  useEffect(() => {
    if (clicked) {
      setCount(c => c + 1);
      setClicked(false);
    }
  }, [clicked]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setClicked(true)}>Increment</button>
    </div>
  );
}

問題点

  • ユーザーがボタンを押しただけで「flag(clicked)」を立てて、
  • useEffect で副作用として count を変更している。
  • 処理が回りくどいし、clicked の役割が曖昧。

こちらのコードは単純に回りくどいな、という印象です。
次にいい例を見ていきます。

良い例:イベント処理はイベントハンドラで直接書く

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

利点

  • ボタンクリック → setCount() を直接呼ぶ。
  • 状態がすぐ反映され、コードがシンプルで読みやすい。
  • useEffect は不要。

こちらは非常に見やすいですね。
onClickなどのイベントハンドラにhandleClickなどの関数を作って渡すのが一般的でしょうか。

useEffectが必要なケース

逆にuseEffectが必要なケースはどのようなときでしょうか?
React公式に以下のような記述がありました。

エフェクトは、外部システムと同期したい場合には必要です。例えば、React の state と jQuery ウィジェットを同期させるエフェクトを書くことができます。

そもそもReactでのエフェクト(effect)とは、React コンポーネントの「外側の世界(副作用)」と連携・同期する処理のことです。
具体的な例を見ていきます。

1.外部ライブラリ(jQuery ウィジェットなど)との同期

例:jQuery の datepicker を使うとき

import { useEffect, useRef } from 'react';
import $ from 'jquery';
import 'jquery-ui/ui/widgets/datepicker';

function DatePicker({ value, onChange }) {
  const inputRef = useRef();

  useEffect(() => {
    const $input = $(inputRef.current);
    $input.datepicker({
      onSelect: date => onChange(date),
    });

    return () => $input.datepicker('destroy');
  }, []);

  return <input ref={inputRef} defaultValue={value} />;
}

2.非同期でデータ取得(API との同期)

例:検索クエリが変更されるたびに API を呼ぶ

import { useState, useEffect } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

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

    const controller = new AbortController();
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => setResults(data));

    return () => controller.abort(); // 前回のリクエストをキャンセル
  }, [query]);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ul>
        {results.map(item => <li key={item.id}>{item.title}</li>)}
      </ul>
    </>
  );
}

3.DOM のイベントリスナー登録(スクロール位置の監視など)

useEffect(() => {
  const handleScroll = () => {
    console.log('スクロール位置:', window.scrollY);
  };

  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

上記の3つの例のそれぞれは、以下のように外部とやりとりをしています。

1.jQuery(React とは別の UI ライブラリ)
2.WebAPI(ネットワーク通信)
3.DOMのwindowオブジェクト

いづれも外部とやりとりしており、useEffectの本来の使い方ができています。
※ただし、データ取得については Next.jsのuseやuseSWR、react-queryなどの専用ライブラリを使う方が効率的です。

まとめ

結論、useEffectは基本使わず、どうしても必要なときにだけ使うようにしましょう。
React公式には上記以外にも様々な具体的な事例が紹介されいて面白いので、是非一度目を通してみることをお勧めします。
僕自身は、最近ではuseEffectを使うたびに、「useEffectを使わない方法はないか?」を自問自答しながら慎重に使うようにしています。
以下にuseEffectを使う際のチェックリストを残します。

1. 外部との同期が必要か?

  • 外部の状態(API、DOM、jQueryなど)と同期させる必要がある場合のみ使用します。
  • 例: fetch APIでデータを取得、windowオブジェクトのイベントリスナー、jQueryウィジェットとの統合。

2. レンダリング中にデータ変換を行っていないか?

  • レンダリング中にデータを変換する(例えば、フィルタリングやソート)場合、useEffectではなく、直接render内で計算します。
  • 例: users.filter()でフィルタリングして表示する場合はuseEffectは不要。

3. ユーザーイベントの処理は直接行うか?

  • ユーザーのアクション(クリック、入力など)の処理は、直接イベントハンドラ内で行い、useEffectを使用して間接的に状態を変更しないようにします。
  • 例: ボタンクリックでカウントアップする場合、useEffectではなく、onClickで直接setCountを呼び出す。

4. 無駄な再レンダリングを避けているか?

  • 不必要にuseEffect内で状態を変更していないか確認します。例えば、データを変換する際に状態を更新すると、無駄な再レンダリングを引き起こす可能性があります。
  • 例: 変換後の状態をuseEffectで再設定しないようにする。

5. クリーンアップ処理は必要か?

  • 外部リソース(例えば、fetchのキャンセルやイベントリスナーの削除)を使用する場合、useEffect内でクリーンアップ処理(returnで関数を返す)を行う必要があります。
  • 例: fetchリクエストのキャンセルや、addEventListenerの解除。

6. 依存配列は正しく設定されているか?

  • useEffectの依存配列が正しく設定されているかを確認します。依存する値が変更されたときだけ実行されるようにするためです。
  • 例: useEffect(() => {...}, [query]);queryが変更されるたびに再実行される。

7. useEffectが必要かどうかを常に疑問に思っているか?

  • useEffectが本当に必要かを自問自答してみましょう。できるだけ他の方法(例えば、状態の変更やuseMemouseCallback)で解決できないか検討します。

Discussion