💡

データフェッチを useEffect から use へ移行したい

に公開1

はじめに

React v19の新機能「use」フックでは、外部ライブラリを使わずに宣言的なデータフェッチができるようになりました。

RSCが主流になりつつある中、SPAでクライアントサイドのデータフェッチを使用しているプロダクトも、依然として多いのではないでしょうか。

React v18まではクライアントサイドでデータフェッチを行う際、ReactQueryやSWRなどのデータフェッチングライブラリを使用するケースを除いて、useEffectで初回レンダリング時にデータを取得する必要がありました。

本記事ではuseEffectとuseのデータフェッチの違いと、useへ移行する際の疑問点などをまとめました。

useEffectを使用したデータフェッチ

まずはuseEffectを使用したデータフェッチについて確認していきます。

export const App = () => {
  const [users, setUsers] = useState<{ name: string; id: string }[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await fetch("/users");
        const { data } = await response.json();
        setUsers(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

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

useEffectでデータフェッチをする場合、コンポーネントの初回レンダリング後にデータを取得が開始されます。

よって、レンダリング回数は3回です。

  1. 初回レンダリング
  2. setLoadingによる再レンダリング
  3. setUsersによる再レンダリング

レンダリング回数が増えることで画面のチラつきが発生するなどのパフォーマンス面で課題があります。
また、状態管理が必要になり命令的に状態を更新する必要があります。

useを使用したデータフェッチ

それでは、useフックを使用したデータフェッチの例を見てみましょう。

const fetchUsers = async () => {
  const response = await fetch("/users");
  if (!response.ok) throw new Error("Failed to fetch users");
  const { data } = await response.json();
  return data;
};

export const App = () => {
  return (
    <ErrorBoundary
      fallbackRender={({ error }) => <p>{error.message}</p>}
      onError={(e) => alert(e)}
    >
      <Suspense fallback={<p>Loading...</p>}>
        <Users promise={fetchUsers()} />
      </Suspense>
    </ErrorBoundary>
  );
};

const Users = ({
  promise,
}: {
  promise: Promise<{ id: string; name: string }[]>;
}) => {
  const users = use<{ id: string; name: string }[]>(promise);
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

useではデータ取得後にコンポーネントがレンダリングされます。
useに渡したPromiseが解決するまでそのコンポーネントはサスペンド状態になります。
つまり、Suspenseを使用してPromsieが未解決の状態のときにfallbackを設定することができます。
これによりデータ取得完了後にコンポーネントがレンダリングされるためloadingなどの状態管理が不要になります。

useEffectからの移行Q&A

エラーが発生した時は?

ErrorBoundaryを使ってエラーハンドリングができます。
もしくはuseに渡すPromiseでPromise.catchを使用しましょう。

データを リフェッチしたい場合は?

PromiseをuseStateで管理し、更新時に新しいPromiseをセットします。
CodeSandbox

  const [messagePromise, setMessagePromise] = useState(fetchMessage());

  const refetch = () => {
    setMessagePromise(fetchMessage());
  }

Reduxにデータをセットするには?

初回レンダリング時にデータをセットしましょう。

useは他のフックとは異なり条件分岐の中で使用することができます。
よって、useRefで初回レンダリングを判別してReduxのような状態管理ライブラリにデータをセットすることができます。

    const dispatch = useDispatch();
    const isFirstRender = useRef(false);

    if (!isFirstRender.current) {
      dispatch(actions.setUsers(use(promise).data));
      isFirstRender.current = true;
    }

state更新でデータフェッチが走ってしまう

stateを持つコンポーネントとSuspenseを使用しているコンポーネントを分離してmomo化しましょう。

state 更新により再レンダリングが起きるとデータフェッチが再実行されてしまいます。
状態管理の責務を持つコンポーネントと、データフェッチや表示を担当するコンポーネントを分離し、表示用コンポーネントをmemo化して、再レンダリングを防止しましょう。

まとめ

データフェッチをuseEffectからuseへ移行することで宣言的なコンポーネントの設計が可能になります。
これによりコンポーネントの責務を明確に分離できるようになります。
レンダリング回数の抑制によるパフォーマンスの向上も見込めるため是非検討して見てください!

Discussion

ゆうたゆうた

ZennはQiitaのように編集リクエストが送信出来ないのでコメントで失礼します。
momoではなくmemoが正しいと思います。

- コンポーネントを分離してmomo化しましょう。
+ コンポーネントを分離してmemo化しましょう。