⚠️

useEffectでデータフェッチは欠点だらけ?じゃあどうしたらいいの?

に公開3

はじめに

突然ですが、useEffect内でfetch関数を使用してデータフェッチを行っている方、たくさんいらっしゃいますよね。
実はこのアプローチ、公式ドキュメントに「大きな欠点がある」と記載されているんです。

特に完全にクライアントサイドのアプリにおいては、エフェクトの中で fetch コールを書くことはデータフェッチの一般的な方法です。しかし、これは非常に手作業頼りのアプローチであり、大きな欠点があります。

エフェクトでのデータ取得に代わる良い方法は?

そんな事実に気がついた私が、「じゃあどうしたらいいのか」を考えてまとめてみました。
クライアントサイドでのデータフェッチについて、検討材料になれば幸いです。

欠点とは一体?

公式ドキュメントには「大きな欠点がある」と書かれていますが、具体的には以下の3つです。

1. ローディング・エラー状態の管理が複雑

useEffectでデータフェッチを行う場合、「データ」「ローディング」「エラー」の3つの状態を手動で管理する必要があります。

ECサイトの商品一覧ページの実装で考えてみましょう。

const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
  const fetchProducts = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch('/api/products');
      if (!response.ok) {
        throw new Error('商品データの取得に失敗');
      }
      const data = await response.json();
      setProducts(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };
  fetchProducts();
}, []);

if (loading) {
  return (
    <div>読み込み中...</div>
  )
}

if (error) {
  return (
    <div>エラーが発生しました</div>
  )
}

return (
  <ProductList data={products} />
)

このように、実際のビジネスロジックとは関係のない「状態管理のためのコード」が大量に必要になります。
さらに、この状態管理を各コンポーネントで毎回書く必要があり、コードの重複と状態管理が複雑になり、可読性、保守性共に低い実装となってしまいます。

2. ネットワークウォーターフォールが発生

親コンポーネント -> 子コンポーネントの順番でデータ取得処理が走り、本来は並列で処理できるリクエストが直列実行されます。

ブログの記事ページで考えてみましょう。
親コンポーネントに「記事データを取得するフェッチ関数」、子コンポーネントに「著者情報、サムネイル画像を取得するフェッチ関数」があるとします。
すると、実際のデータ取得は下記の流れになります。

  1. 記事データの取得処理( 1秒 )
  2. 記事データが取得されて、初めて子コンポーネントが描画
  3. 著者情報の取得( 1秒 )
  4. サムネイル画像の取得 ( 1秒 )

合計で、3秒のデータ取得が完了してから画面が表示されます。
こちらも1つ目の欠点と似通っていますが、ユーザーがページを開いてから実際にデータが表示されるまでに3秒もかかってしまうということになります。
3つのデータ取得を並列で行えば、1秒で済むところが3倍の時間がかかることになります。
これはユーザーによっては離脱の原因になってしまうため、重要な問題です。

3. データのプリロード・キャッシュができない

ユーザがリンクをクリックする前にデータを準備したり ( プリロード )、一度取得したデータを再利用する ( キャッシュ )仕組みが作成しにくくなります。

ユーザー一覧からユーザー詳細ページに遷移するケースで考えてみましょう。
ユーザー詳細画面に遷移すると、ユーザーの詳細情報を取得します。
ユーザーが一覧画面に戻り、再度ユーザー詳細画面に遷移した場合、ユーザーの詳細情報取得処理が再実行されます。
ユーザーの情報というのは頻繁に変わることはないため、無駄なデータフェッチが行われることになります。
このようなデータはキャッシュをしておくことで不要なデータフェッチを防ぎ、画面の描画速度も向上します。
画面の描画速度はユーザー体験(UX)に直結するため、こちらも重要な問題となります。

これらの欠点から見えるのは、「パフォーマンス面」「ユーザー体験(UX)面」で大きなデメリットになるということです。

じゃあどうしたらいいの?

欠点を3つほど紹介させていただきましたが、記事のタイトルにもあるとおり今回はuseEffectでのデータフェッチに観点を絞ります。
そのため、欠点の1つ目の解決を図っていきます。

React 19 で追加された use フックを使う!

React 19 にて追加された新しいフックである useフックを使用することで、この問題を解消できます!

use フックって何?

簡単にいうと、Promise が直接扱えるフックです!
「Promise?なんだそれ?」という方に簡単に説明をします。

Promiseとは

Promise は「いつか結果が返ってくる処理」を指す JavaScript の仕組みのことです。
例えば、

  • APIからデータを取得する
  • ファイルの内容を読み込む
  • 時間のかかる計算処理を実行する

こういった「時間のかかる処理」を扱う場合に使用します。

useフックの基本的な使い方

今まではuseState, useEffectを用いてこんな感じに実装していたと思います。

const [data, setData] = useState();

useEffect(() => {
  fetch('/api/v1/data')
    .then(response => response.json())
    .then(data => setData(data));
}, [])

でも、useフックを使うと

const data = use(fetch('/api/v1/data').then(response => response.json()));

すごくシンプルになりました!!
6行ほど必要だった処理が、なんと1行にまとめられました!

Before/After比較

Before:useEffect + useState(約30行)

const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
  const fetchProducts = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch('/api/products');
      if (!response.ok) {
        throw new Error('商品データの取得に失敗');
      }
      const data = await response.json();
      setProducts(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };
  fetchProducts();
}, []);

if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラーが発生しました</div>;
return <ProductList data={products} />;

After:useフック(約15行)

// データフェッチ関数(ビジネスロジックのみ)
const fetchProducts = async (): Promise<Product[]> => {
  const response = await fetch('/api/products');
  if (!response.ok) {
    throw new Error('商品データの取得に失敗');
  }
  return response.json();
};

// コンポーネント(状態管理不要!)
const ProductComponent = () => {
  const products = use(fetchProducts());
  return <ProductList data={products} />;
};

// アプリケーション全体
return (
  <ErrorBoundary fallback={<div>エラーが発生しました</div>}>
    <Suspense fallback={<div>読み込み中...</div>}>
      <ProductComponent />
    </Suspense>
  </ErrorBoundary>
);

比較表

項目 Before(useEffect) After(useフック)
状態管理 手動(3つの状態変数) 自動(Suspense/ErrorBoundary)
エラーハンドリング try-catch + setState 宣言的(ErrorBoundary)
ローディング表示 条件分岐 + setState 宣言的(Suspense)
コード行数 約30行 約15行
可読性 状態管理が混在 ビジネスロジックのみ
保守性 状態管理の重複 宣言的で再利用しやすい

useフックの動作の仕組み

  1. データ取得開始 → Suspenseが<div>読み込み中...</div>を表示
  2. データ取得成功 → 通常のコンポーネントが表示
  3. エラー発生 → ErrorBoundaryが<div>エラーが発生しました</div>を表示

つまり、コンポーネントは 「データがある状態」だけ を考えればよくなります!

こうすることで、状態管理はReactに任せつつ、開発者はビジネスロジックの実装に集中することができます。

さいごに

今回は React 19 の useフックが useEffectでのデータフェッチの欠点を解決できることを書かせていただきました。
案件で実際に使用してみるとよりありがたみがわかると感じるので、機会があれば積極的に使っていきたいです!

今回紹介させていただいた useフック以外にも、React 19 では様々なフックが追加されました。どれも開発者にとってとても便利だなぁと感じるものなので、気になる方はぜひ調べてみてくださいね!

では、またね〜👋

Discussion

あすぱるあすぱる

シンプルにデータフェッチが必要な場面ならもちろんuseフックのみでいいと思うのですが、
一般的なデータフェッチとかはSWR (https://swr.vercel.app/ja) というライブラリがより洗練されてていい感じです