Suspenseとは
SuspenseはReact ver18で実装された外部APIなどからのデータ取得状態を検知するコンポーネント機能です。
従来の方法とそのデメリット
Suspenseが登場するまでは、外部APIなどからのデータ取得し描画するためには、useEffect内で非同期処理を実行し、それをuseStateで保持する方法が一般的でした。
ただし、この方法だと以下のようなつらさがあります
- 副作用(関数コンポーネントのレンダリングに関係ない処理)を起こす目的で用意されているuseEffectは極力使いたくないものだが、データ取得のためだけにわざわざ使わないといけない
- 取得したデータをJSX内で使用するという目的だけのためだけにわざわざstateを用意しないといけない
- useEffect内で非同期処理を実行し、それをstateにセットして・・・という一連の処理がどうしても命令的(手続き的)になりがちで可読性が損なわれやすい
実装例
従来の方法
todo一覧を表示するページを例に、従来までの方法とSuspenseを使った実装サンプルを紹介します。
Home.tsxからTodos.tsxコンポーネントを呼び出しTODO一覧を表示しています
index.tsx
import Todos from '../components/Todos';
export default function Home() {
return <Todos />;
}
Todos.tsx
import { useCallback, useEffect, useState } from 'react';
export type Todo = {
completed: boolean;
id: number;
title: string;
userId: number;
};
export default function Todos() {
const [todos, setTodos] = useState<Todo[]>([]);
const fetchTodos = useCallback(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
const data = await response.json();
return data;
}, []);
useEffect(() => {
// ここでローディングなど表示のための処理をする
fetchTodos().then((todos) => {
setTodos(todos);
});
}, []);
return (
<ul>
{todos.map((todo, i) => (
<li key={i}>{todo.title}</li>
))}
</ul>
);
}
Suspenseを使った方法
Suspenseを利用する場合、手順は以下の通りです。
- データ描画用のコンポーネントを使う側
- Suspenseコンポーネントで、データの取得/描画を行うコンポーネントをラップする
- Suspenseコンポーネントには、fallback属性として、データ取得が完了するまでの間に表示したい要素やコンポーネントを指定する(ローディングコンポーネントなど)
index.tsx
import { Suspense } from 'react';
import Todos from '../components/Todos';
export default function Home() {
return (
<Suspense fallback={<p>...loading</p>}>
<Todos />
</Suspense>
);
}
Todos.tsx
- サスペンドされるコンポーネント側
- コンポーネントの外側でデータ格納用の変数を定義しておき、このコンポ-ネントがレンダリングされたらデータフェッチ用の関数が実行されるようにする
- データがまだ取得されていなければ取得用fetch関数が実行される。この際、fetch関数の実行結果であるPromiseをthrowするようにする
- Promiseをthrowすると、Todo.tsxのレンダリングがサスペンド(中断)され、fallbackに指定した要素がレンダリングされる。
- throwされたPromiseがresolveされるとサスペンドが解除され、Suspenseでラップしたコンポーネントが再レンダリングされる
- Promiseがrejectされた場合はErrorBoundaryがキャッチするようにしておくこと
export type Todo = {
completed: boolean;
id: number;
title: string;
userId: number;
};
let todos: Todo[] | undefined;
export default function Todos() {
const fetchTodos = async (): Promise<Todo[]> => {
return fetch('https://jsonplaceholder.typicode.com/todos').then((res) =>
res.json()
);
};
if (!todos) {
throw fetchTodos().then((data) => (todos = data));
}
return (
<ul>
{todos.map((todo, i) => (
<li key={i}>{todo.title}</li>
))}
</ul>
);
}
従来までの命令的な記述方法と比べ、非同期処理をより宣言的に書けるようになり、宣言的UIというReactの根底にある思想とマッチするようになりました。
複数コンポーネントがある場合は、以下のようにSuspenseはネストさせることも可能です。
<Suspense fallback={<Loading />}>
<Users />
<Suspense fallback={<Loading />}>
<Todos />
</Suspense>
</Suspense>
まとめ
いかがでしたでしょうか。本記事では、React18から登場したSuspenseを使って非同期処理を実行しデータを取得/描画する方法について紹介しています。Suspenseを使うことにより、より宣言的に処理を記述できるようになったので、是非参考にしてみてください