Reactにおけるデータ取得の変遷
本題に入る前に対象範囲を限定します。今回はブラウザがAPIサーバーにGETリクエストを投げてデータ取得をすることのみに焦点を当てています。いわゆるクライアントサイドレンダリング(CSR)におけるデータフェッチです。
React Server Componentを利用しているNext.jsやReact Router v7(旧Remix)といったサーバーサイドでデータ取得が可能なフレームワークまで範囲を広げると話が複雑になるためここでは割愛します。GraphQLも本記事では対象外とします。
useEffectを使ったデータ取得
useEffectはReact Hooksの一種で副作用を扱うものです。以下はデータ取得の実例です。
import { useEffect, useState } from 'react'; const POST_URL = 'https://jsonplaceholder.typicode.com/posts/1'; type ArticleType = { title: string; body: string; }; function Article() { // 1. 状態を初期化する // 返り値は [状態(初期値はnull), 状態を更新する関数] の配列 const [article, setArticle] = useState<ArticleType | null>(null); // 2. 副作用を扱う useEffect(() => { // 3. APIをコールする非同期処理 async function fetchData() { const response = await fetch(POST_URL); const data = await response.json() as ArticleType; setArticle(data); } fetchData(); }, []); // 4. loading...を描画する if (article === null) { return <div>loading...</div>; } // 5. APIから得た値とともにarticleを描画する return ( <article> <header> <h1>{article.title}</h1> </header> <p>{article.body}</p> </article> ); }
このコンポーネントは、記事を返すAPIにGETリクエストを送り、レスポンスを受け取ったら記事のタイトルと本文を表示するものです。レスポンスを受け取るまでは「loading...」という文字が画面に表示されます。
コードを解説します。まず、状態管理のReact HooksであるuseState
を使って状態を初期化します。articleという値が管理したい状態であり、setArticleはこの状態を更新する関数です。
なお、変数articleに値を再代入して直接更新することはできません。状態を更新する場合、必ずsetArticle(新しい値)
というように更新関数を使う必要があります。
useEffectの実行タイミングはコンポーネントの描画後です。Reactは状態の初期値でコンポーネントを一度描画した後、useEffectに渡された副作用を持つコールバック関数を処理します。
このため、「1→4→初期描画(loading...)→2→3→5→再描画(記事のタイトルと本文)」の順番で処理されます。コードは上から書いた順番に実行されるわけではないのです。
Reactコンポーネントは状態が更新されると再描画されます。3の中でsetArticle
を実行しており、状態がnull
からArticleType
型の値に更新されました。Reactはこの新しい値を使ってコンポーネントを再実行します。よって、4の条件式がfalseになり、5の箇所が実行された結果が再描画されるのです。
副作用を扱うuseEffectの挙動は一目で理解しづらく、React開発者にとって混乱の元でした。Reduxの開発者でありReactコアチームに在籍していたDan Abramov氏が、2019年に「useEffect完全ガイド」という長文ブログを公開して幅広く説明をしたものの、誤用はなくなりませんでした。
現在ではuseEffectは本当に必要な場面でだけ使おう、無闇に使わないでおこうという考えが主流です。Reactの公式ドキュメントが「そのエフェクトは不要かも」という記事を公開しているほどです。
それでも以下のデータフェッチ用のライブラリが登場するまでは、useEffectの中でデータを取得することが一般的でした。
ライブラリを使ったデータ取得
useEffectを使ったデータ取得の問題点は、useEffect自体のややこしさだけではありません。ユーザー体験の面でも物足りないところがありました。
例えば、ページAでAPI経由のデータ取得をした後、ページBに遷移します。ページBに遷移したときにページAのコンポーネントは破棄されるため、ページBからAに戻った時にはまたページAで再度APIをコールする必要があるのです。これでは一度遷移したページに戻るときもloading...
という表示を挟まなければならならず、サクサクとページ遷移をするSPAらしい体験が損なわれてしまいます。
この問題を解決するために、Vercel社が開発しているSWRやTanStack Query(旧React Query)というライブラリが登場しました。以下ではTanStack Queryの例を紹介します。
import { useQuery } from '@tanstack/react-query'; const POST_URL = 'https://jsonplaceholder.typicode.com/posts/1'; type ArticleType = { title: string; body: string; }; async function fetchArticle(): Promise<ArticleType> { const response = await fetch(POST_URL); if (!response.ok) { throw new Error('Failed to fetch article'); } return response.json(); } function Article() { // useQueryはデータ取得と状態管理が一体になっているHooks const { data: article, error, isLoading } = useQuery({ queryKey: ['article'], queryFn: fetchArticle, }); if (isLoading) return <div>loading...</div>; if (error) return <div>エラーが発生しました: {error.message}</div>; return ( <article> <header> <h1>{article.title}</h1> <span>投稿日: 2025/1/1</span> </header> <p>{article.body}</p> </article> ); }
useEffectの例と大きく異なるのはuseState, useEffectが消え、代わりにuseQueryというライブラリ由来のHooksを使っている点です。
useQueryはデータ取得と状態管理をセットで実施します。引数のオブジェクトにqueryFnというキーでAPIコールをする関数を渡し、queryKeyというキーにその関数とセットのキーを渡します(SWRも同じようなインターフェースを備えています)。TanStack Queryは中でJavaScriptのMapオブジェクトを使ってkey/value形式でクエリに関するデータをメモリに保持し、キャッシュとして扱います。
この仕組みによって先ほどの課題は解決されました。TanStack Queryを使うと、ページAのHTTPレスポンスをメモリに保持します。ページBに遷移してからまたページAに戻ったとしても、先ほどのレスポンスのキャッシュデータを使ってページを表示します。
このため、ローディング画面は表示されません。Reactは以前の状態を使ってコンポーネントを描画するため、if (isLoading) {/* ... */}
のブロックを通らないのです。
なお、ブラウザで実行されるJavaScriptのメモリにデータを保持しているだけなので、画面をリロードするとキャッシュは破棄され、再度画面ローディングが始まります。
ただし、ここにはトレードオフがあります。先ほどの問題は解決しましたが、他にリトライや再検証(revalidate。最新のレスポンスを取得するために再度リクエストを送ること)、キャッシュの更新ロジックの管理など開発者がケアするべき他の点も生まれました。
それでも、ユーザー側の利便性は向上した一方で開発者に求められることが増えただけと考えれば、ユーザーファーストなトレードオフであると言えるでしょう。
useを使ったデータ取得
最後にuseを使ったデータ取得について触れておきます。useは2024年12月にリリースされたReact v19で導入された関数です。useにはJavaScriptの非同期関数の返り値であるPromiseを渡せるので、データ取得に使うことができます。記事執筆時点(2025年4月)でuseEffectを使わない最新のデータ取得方法は以下です。
import React, { use, Suspense } from 'react'; type Todo = { userId: number; id: number; title: string; completed: boolean; }; async function fetchData(): Promise<Todo> { const res = await fetch('https://jsonplaceholder.typicode.com/todos/1'); if (!res.ok) throw new Error('Failed to fetch'); return res.json(); } type Props = { data: Promise<Todo>; }; function MyTodo({ data }: Props) { const fetchedData = use(data); return <div>{fetchedData.title}</div>; } export default function App() { const data = fetchData(); return ( <Suspense fallback={<div>Loading...</div>}> <MyTodo data={data} /> </Suspense> ); }
useはPromiseの状態に応じてその処理を変えます。
Promiseがpendingの場合、useはpromiseをthrowするため、親コンポーネントであるSuspenseのfallbackコンポーネント(<div>Loading...</div>
)を表示します(Suspenseの詳しい説明は省略します)。
Promiseがresolvedの時は、解決した値(ここではTODO型の値)を返すことでMyTodo
コンポーネントが描画されます。反対にrejectedの場合は、エラーオブジェクトをthrowするため、ErrorBoundary(コード上では省略)でキャッチできるように準備します。
データ取得のように、ReactにおいてuseEffectを使って非同期関数を実行する処理は、副作用と見なされると先ほど述べました。一方useを使うことで、純粋関数を志向するReactコンポーネントの中に副作用を扱う処理を自然に組み込めるようになったことがコードを見ても分かります。useEffectで行っていたように非同期処理を明示的に分離する必要がなくなったのです。これはReactの進化と呼べるでしょう。ただ、現場ではAPIでのデータ取得にTanStack QueryやSWRといったライブラリを使うことが主流であるため、開発者がuseを新たに使う機会は限定的かもしれません[3]。
まとめ
本記事ではReactにおけるデータ取得について紹介しました。同期処理と非同期処理の考え方の違い、またデータ取得の変遷を追うとReactとフロントエンドの関心ごとが分かるのではないかと思います。次の最終回ではフロントエンドのディレクトリ構成とコンポーネント開発を支えるツールについて紹介します。