こんにちは、電通総研の金融ソリューション事業部の上野です。
普段の業務ではアーキテクトで案件に関わっており、現在はJava、React、Next.jsを利用したソリューション開発に従事しています。
業務で利用するOSSのバージョンアップや技術選定はアーキテクトの重要な業務の一つであると考えており、今回は直近リリースされたReact19について調査した内容を一部記事にしました。
本記事は、電通総研 Advent Calendar 2024の18日目の記事となります。
はじめに
Reactは、バージョンアップのたびにレンダリング戦略や非同期処理周りの仕様が進化してきました。直近では、2022年3月に登場したReact 18では並行レンダリング(Concurrent Features)やSuspenseの実用化による非同期処理の新しい書き方が可能となりました。さらに2024年12月に登場したReact 19では、useフックをはじめとしたより自然で宣言的な非同期パターンへと進化することが期待されています。
非同期処理は、ユーザエクスペリエンス(UX)向上に直結する極めて重要な要素です。データ取得や状態の変化をブロッキングすることなく画面を表示し、スムーズな操作体験を提供するためには、React側が標準で非同期レンダリングやデータフェッチをサポートしていく必要があります。またReact本体のGitHub Issueや議論でも、非同期処理やSuspenseをめぐる活発なやり取りが続いており、コミュニティとしてもこの分野への関心は非常に高まっています。
そこで本記事では、React 17から18、そして19へと進むにつれて非同期処理の書き方がどのように変わってきたのかを解説します。
(参考)
- RFC: First class support for promises and async/await
- [Suspense - React])(https://ja.react.dev/reference/react/Suspense)
- use - React
Reactの各バージョンの非同期処理
非同期処理記載方法のまとめ
まず初めに、それぞれのReactバージョンにおける非同期処理を記載した際の課題についてまとめました。
〇は課題が解決された状態を表し、React19ではReact17の記法における課題はすべて解決しています。
課題 | React17 | React17(+useQuery) | React18(+useQuery) | React19 |
---|---|---|---|---|
(1) hooksによる状態管理が複雑 | × | 〇 | 〇 | 〇 |
(2)ifの分岐が多くなる(宣言的ではない) | × | × | 〇 | 〇 |
(3)コンポーネントの責務が多い | × | × | 〇 | 〇 |
(4)外部OSSに依存 | 〇 | × | × | 〇 |
次章から、各バージョンにおけるサンプルコードを載せます。
React17以前の非同期処理(Reactプリミティブな方法)
useEffect
およびuseState
をラップしたカスタムフックを作成し、非同期にフェッチを行う。- フェッチ後のステータスに応じて、コンポーネントのだし分けをする。
import { useState, useEffect } from 'react'; // いずれかのデータフェッチを非同期で行うカスタムフック function useFetchData(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchData() { try { const response = await fetch(url); if (!response.ok) throw new Error('Failed to fetch data'); const result = await response.json(); setData(result); } catch (err) { setError(err); } finally { setLoading(false); } } fetchData(); }, [url]); return { data, loading, error }; } // 非同期データのfetch後にレンダリングを行うコンポーネント function MyComponent() { const { data, loading, error } = useFetchData('https://api.example.com/data'); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <p>{data.title}</p> </div> ); } export default MyComponent;
このコードの課題としては、以下が挙げられます。
- (1)hooksによる状態管理が複雑。
- (2)ifの分岐が多くなる(宣言的ではない)。
- loading状態を判定する、ifによる分岐。
- error状態を判定する、ifによる分岐。
- (3)コンポーネントの責務が多い。
- loading待ち、エラーハンドルは業務処理と直接関係がない。やりたいことは
<p>data.title</p>
を表示したいだけ。
- loading待ち、エラーハンドルは業務処理と直接関係がない。やりたいことは
React17以前の非同期処理(OSSの利用)
- カスタムhook部分を
tasntack/react-query
に任せることで、fetchに応じた結果をより短いコード量で受け取ることが可能。 - フェッチ後のステータスに応じて、コンポーネントのだし分けをする。
import { useQuery } from '@tanstack/react-query'; // 非同期なデータfetch async function fetchData(url) { const response = await fetch(url); if (!response.ok) { throw new Error('Failed to fetch data'); } return response.json(); } // 非同期データのfetch後にレンダリングを行うコンポーネント // Promiseの扱いを`tanstack/react-query`に委譲 function MyComponent() { const url = 'https://api.example.com/data'; const { data, isLoading, error } = useQuery(['data'], () => fetchData(url)); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <p>{data.title}</p> </div> ); } export default MyComponent;
このコードの課題としては、以下が挙げられます。
- (2)ifの分岐が多くなる(宣言的ではない)。
- loading状態を判定する、ifによる分岐。
- error状態を判定する、ifによる分岐。
- (3)コンポーネントの責務が多い。
- loading待ち、エラーハンドルは業務処理と直接関係がない。やりたいことは
<p>data.title</p>
を表示したいだけ。
- loading待ち、エラーハンドルは業務処理と直接関係がない。やりたいことは
- (4)外部OSSに依存。
- できればReactのプリミティブな機能だけで実現したい。
React18の非同期処理
- React18で登場したSuspenseと
tanstack/react-query
が連携することで、Suspense状態を管理することが可能に。 - fetch中、fetchの成功、fetchの失敗を分岐処理を設けずに宣言的に記載することが可能に。
import React, { Suspense } from 'react'; import { useQuery } from '@tanstack/react-query'; import { ErrorBoundary } from 'react-error-boundary'; // 非同期なデータfetch async function fetchData(url) { const response = await fetch(url); if (!response.ok) { throw new Error('Failed to fetch data'); } return response.json(); } // 非同期データのfetch後にレンダリングを行うコンポーネント // Promiseの扱いを`tanstack/react-query`に委譲 function MyComponentContent() { const url = 'https://api.example.com/data'; const { data } = useQuery(['data'], () => fetchData(url), { suspense: true }); return ( <div> <p>{data.title}</p> </div> ); } // エラー時に表示するフォールバックコンポーネント function ErrorFallback({ error }) { return <p>Error: {error.message}</p>; } // fetch中、fetch成功時、fetch失敗時にそれぞれ適切なコンポーネントを表示する export default function MyComponent() { return ( <ErrorBoundary FallbackComponent={ErrorFallback}> <Suspense fallback={<p>Loading...</p>}> <MyComponentContent /> </Suspense> </ErrorBoundary> ); }
このコードの課題としては、以下が挙げられます。
- (4)外部OSSに依存。
- できればReactのプリミティブな機能だけで実現したい。
React19の非同期処理
- React19で登場した
use
とcache
の組み合わせにより、ReactプリミティブなAPIのみでSuspense状態を管理することが可能に。
import { use, Suspense, cache } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; // 非同期なデータfetch const cachedFetchData = cache(async (url) => { const response = await fetch(url); if (!response.ok) { throw new Error('Failed to fetch data'); } return response.json(); }); // 非同期データのfetch後にレンダリングを行うコンポーネント // Promiseの扱いをはReactプリミティブな`use`を利用 function MyComponentContent() { const url = 'https://api.example.com/data'; const data = use(cachedFetchData(url)); return ( <div> <p>{data.title}</p> </div> ); } // エラー時に表示するフォールバックコンポーネント function ErrorFallback({ error }) { return <p>Error: {error.message}</p>; } // fetch中、fetch成功時、fetch失敗時にそれぞれ適切なコンポーネントを表示する export default function MyComponent() { return ( <ErrorBoundary FallbackComponent={ErrorFallback}> <Suspense fallback={<p>Loading...</p>}> <MyComponentContent /> </Suspense> </ErrorBoundary> ); }
終わりに
Reactに限らず、昨今のフロントエンド界隈は群雄割拠であり、目まぐるしい進化を遂げています。その中でも今回は個人的に注目している非同期処理に特化して解説してみました。
書ききれなかった内容については、参考ブログやリンクを記載しておきます。
- use APIに関する補足
- Suspenseに関する補足
実際の案件ではNext.jsを利用することも多いため、Reactの非同期処理の進化でNext.js13以降どのような破壊的Updateが行われたのか、機会がありましたらまた筆をとろうと思います。最後まで読んでいただきありがとうございました。
執筆:@kamino.shunichiro、レビュー:@yamashita.tsuyoshi
(Shodoで執筆されました)