原文地址:A Fundamental Guide To React Suspense
もう一つ、React 18 でリリースされる大きな機能は Suspense です。React 開発の分野に長くいるなら、Suspense 機能は特に新しいものではないことをご存知でしょう。2018 年には、Suspense は React 16.6 の一部として実験的機能としてリリースされました。その時、主に React.lazy と組み合わせたコード分割の処理に焦点を当てていました。
しかし今、React 18 のリリースに伴い、Suspense の正式なリリースが私たちの前にあります。並行レンダリングのリリースと共に、Suspense の真の力がついに解放されました。Suspense と並行レンダリングの相互作用は、ユーザー体験を改善するための巨大な機会の世界を開きます。
しかし、すべての機能と同様に、並行レンダリングのように、基本原理から始めることが重要です。そもそも Suspense とは何でしょうか?なぜ私たちは最初に Suspense を必要とするのでしょうか?Suspense はどのようにこの問題を解決するのでしょうか?どんな利点があるのでしょうか?これらの基本原理を理解するために、この記事ではこれらの質問を正確に議論し、Suspense に関する堅実な知識基盤を提供します。
Suspense とは何か#
本質的に、Suspense は React 開発者が React に対して、あるコンポーネントがデータの準備が整うのを待っていることを示すメカニズムです。すると React は、そのデータが取得されるのを待つべきであることを理解します。同時に、ユーザーにはフォールバックが表示され、React はアプリケーションの残りの部分をレンダリングし続けます。データが準備できたら、React はその特定のユーザーインターフェースに戻り、それに応じて更新します。
根本的に言えば、これは現在の React 開発者がデータ取得プロセスを実装する方法と大きな違いはありません:コンポーネントがまだデータを待っているかどうかを示すために何らかの状態を使用し、データを取得し始める useEffect、データの状態に応じてローディング状態を表示し、データが準備できたら UI を更新するというものです。
しかし実際には、Suspense は技術的にこの状況を実現し、全く異なります。前述のデータ取得プロセスとは対照的に、Suspense は React と深く統合されており、開発者がローディング状態をより直感的に調整できるようにし、競合条件を回避します。これらの詳細をよりよく理解するためには、なぜ私たちが Suspense を必要とするのかを知ることが重要です。
なぜ私たちは Suspense を必要とするのか#
Suspense がない場合、データ取得フローを実現するための主な方法は 2 つあります:レンダリング時に取得する方法と、フェッチしてからレンダリングする方法です。しかし、これらの従来のデータ取得プロセスにはいくつかの問題があります。Suspense を理解するためには、これらのプロセスの問題と制限を深く掘り下げる必要があります。
レンダリング時に取得#
ほとんどの人は、前述のように useEffect と状態変数を使用してデータ取得プロセスを実現します。これは、コンポーネントがレンダリングされるときにのみデータの取得を開始することを意味します。すべてのデータ取得は、コンポーネントの効果とライフサイクルメソッド内で発生します。
このアプローチの主な問題は、コンポーネントがレンダリング時にのみデータ取得をトリガーし、その非同期的な性質が他のコンポーネントのデータリクエストを待たなければならないことです。
例えば、ComponentA というコンポーネントがあり、いくつかのデータを取得し、ローディング状態を持っているとします。内部で、ComponentA は別のコンポーネント ComponentB をレンダリングし、ComponentB も自分自身でデータ取得を行います。しかし、データ取得の実装方法のために、ComponentB はレンダリングされた後にのみデータを取得し始めます。これは、ComponentA がデータの取得を完了するまで待たなければならないことを意味します。
これにより、コンポーネント間のデータ取得が順次行われる滝のようなアプローチが生じ、基本的にそれらが互いにブロックされることを意味します。
function ComponentA() {
const [data, setData] = useState(null)
useEffect(() => {
fetchAwesomeData().then((data) => setData(data))
}, [])
if (user === null) {
return <p>データを読み込んでいます...</p>
}
return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
)
}
function ComponentB() {
const [data, setData] = useState(null)
useEffect(() => {
fetchGreatData().then((data) => setData(data))
}, [])
return data === null ? <h2>データを読み込んでいます...</h2> : <SomeComponent data={data} />
}
フェッチしてからレンダリング#
コンポーネント間のデータ取得の順序ブロッキングを防ぐための代替案は、できるだけ早くすべてのデータ取得作業を開始することです。したがって、コンポーネントがレンダリング時にデータ取得とデータリクエストを処理する責任を持つのではなく、ツリーがレンダリングを開始する前にすべてのリクエストを開始する方が良いです。
このアプローチの利点は、すべてのデータリクエストが一緒に開始されるため、コンポーネント B はコンポーネント A が完了するのを待つ必要がないことです。これにより、コンポーネントが互いにデータフローをブロックする問題が解決されます。しかし、これにはもう一つの問題があり、すべてのデータリクエストが完了するまでユーザーに何も表示できないということです。これは最適な体験ではないことは想像に難くありません。
// ツリー全体をレンダリングする前にデータを取得し始める
function fetchAllData() {
return Promise.all([fetchAwesomeData(), fetchGreatData()]).then(([awesomeData, greatData]) => ({
awesomeData,
greatData,
}))
}
const promise = fetchAllData()
function ComponentA() {
const [awesomeData, setAwesomeData] = useState(null)
const [greatData, setGreatData] = useState(null)
useEffect(() => {
promise.then(({ awesomeData, greatData }) => {
setAwesomeData(awesomeData)
setGreatData(greatData)
})
}, [])
if (user === null) {
return <p>データを読み込んでいます...</p>
}
return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
)
}
function ComponentB({ data }) {
return data === null ? <h2>データを読み込んでいます...</h2> : <SomeComponent data={data} />
}
Suspense はこの問題をどのように解決するのか#
本質的に、fetch-on-render と fetch-then-render の主な問題は、データ取得プロセスと React ライフサイクルという 2 つの異なるプロセスを強制的に同期させようとする事実に帰着します。Suspense を使用することで、私たちは「フェッチしながらレンダリングする」方法という異なるデータ取得方法を得ることができます。
const specialSuspenseResource = fetchAllDataSuspense()
function App() {
return (
<Suspense fallback={<h1>データを読み込んでいます...</h1>}>
<ComponentA />
<Suspense fallback={<h2>データを読み込んでいます...</h2>}>
<ComponentB />
</Suspense>
</Suspense>
)
}
function ComponentA() {
const data = specialSuspenseResource.awesomeData.read()
return <h1>{data.title}</h1>
}
function ComponentB() {
const data = specialSuspenseResource.greatData.read()
return <SomeComponent data={data} />
}
以前の実装とは異なり、これによりコンポーネントは React が到達する瞬間にデータ取得を開始できます。これはコンポーネントがレンダリングされる前に発生し、React はそれによって停止することはありません。その後、React はコンポーネントの子ツリーを評価し続け、データ取得の完了を待ちながらそれをレンダリングしようとします。
これは、Suspense がレンダリングをブロックしないことを意味し、子コンポーネントは親コンポーネントが完了するのを待たずにデータ取得リクエストを開始できます。React は可能な限りレンダリングを試みながら、適切なデータ取得リクエストを開始します。リクエストが完了すると、React は対応するコンポーネントを再訪し、新しく受け取ったデータを使用してユーザーインターフェースを更新します。
Suspense の利点は何か#
Suspense には多くの利点があり、特にユーザー体験において。しかし、その中のいくつかの利点は開発者の体験にも関連しています。
-
早期の取得開始。Suspense が導入する「フェッチしながらレンダリング」方法の最大かつ最も直接的な利点は、データ取得が可能な限り早く開始されることです。これにより、ユーザーが待つ時間が短くなり、アプリケーションの速度が向上し、これはすべてのフロントエンドアプリケーションにとって普遍的に有益です。
-
より直感的なローディング状態。Suspense を使用すると、コンポーネントは混乱した if 文の山を含む必要がなく、ローディング状態を実現するために状態を個別に追跡する必要もありません。代わりに、ローディング状態はそれが属するコンポーネント自体に統合されます。これにより、コンポーネントはより直感的になり、ローディングコードと関連コードを一緒に保持し、ローディング状態がコンポーネントに含まれるため、再利用が容易になります。
-
競合条件の回避。既存のデータ取得実装の一つの問題は、この記事では深く掘り下げていない競合条件です。場合によっては、従来の「レンダリング時に取得」と「フェッチしてからレンダリング」の実装が、時間、ユーザー入力、パラメータ化されたデータリクエストなどの異なる要因に依存して競合条件を引き起こす可能性があります。主な潜在的な問題は、React のプロセスとデータ取得のプロセスという 2 つの異なるプロセスを強制的に同期させようとすることです。しかし、Suspense を使用することで、これをより優雅に統合し、上記の問題を回避できます。
-
より包括的なエラーハンドリング。Suspense を使用することで、私たちは基本的にデータリクエストフローの境界を作成しました。それに加えて、Suspense がコンポーネントのコードとより直感的に統合されているため、React 開発者は React コードとデータリクエストのためにより多くの統合エラーハンドリングを実現できます。
最後のまとめ#
React Suspense は 3 年以上注目されてきました。しかし、React 18 のリリースに伴い、正式なリリースの時期が近づいています。並行レンダリングに続いて、これはこの React バージョンの一部としてリリースされる最大の機能の一つです。それ自体として、データ取得とローディング状態の実装を新しい直感性と優雅さのレベルに引き上げることができます。
Suspense の基本原理を理解するために、この記事ではそれにとって重要な幾つかの質問と側面をカバーしました。これには、Suspense とは何か、なぜ私たちが最初に Suspense のようなものを必要とするのか、どのように特定のデータ取得の問題を解決するのか、そして Suspense がもたらすすべての利点が含まれています。