はじめに
データフェッチ後、レンダリングする実装って頻繁にあると思います。
ただ、データフェッチ中はデータがundefined
のになっているので、データフェッチ中なのかどうかに注目しながら実装するのって結構しんどいですよね。そんな時に、Suspense使うと便利だよって記事です。
そもそもSuspenseってなに??
Suspenseは子要素がまだレンダリング中のため、親コンポーネントを表示できないよ。という処理です。子コンポーネントでデータフェッチ中、まだ表示できないよという場合はフォールバック中のUIを表示させる事ができます。
成果物
データフェッチ前はLoadingが表示され、データ取得でき次第UIを表示する仕様です。
注目して欲しいのは、データ取得が完了できしだい、FallBackUIからSuspendUserに切り替わっていますね。
Suspenseがどちらを表示するかを決定しているので、ユーザ側でローディング中だからこっちみたいなことは考える必要がありません。
import { Suspense } from "react";
// ユーザーの型定義
type User = {
id: number;
name: string;
username: string;
email: string;
};
// キャッシュとしてグローバルにデータを保存するオブジェクト
let userData: User[] | null = null;
let userDataPromise: Promise<User[]> | null = null;
// ユーザーデータを取得する関数
const getUserData = async (): Promise<User[]> => {
// データ取得がわかりやすいように2秒待つ
await new Promise((resolve) => setTimeout(resolve, 5000));
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
};
// SuspendUserコンポーネント
const _SuspendUser = () => {
// データがまだ取得されていなければ、データ取得を開始
if (!userDataPromise) {
userDataPromise = getUserData().then((data) => {
userData = data; // データをキャッシュ
return data;
});
}
// データが取得できるまでPromiseを投げる
if (!userData) {
throw userDataPromise; // 取得が完了していない場合はPromiseを投げる
}
// データが取得されたら表示
return (
<div>
<p>{userData[0].name}</p> {/* 最初のユーザーの名前を表示 */}
</div>
);
};
// メインのAppコンポーネント
export const SuspendUser = () => {
return (
<Suspense fallback={<FallbackUI />}>
<_SuspendUser />
</Suspense>
);
};
export const FallbackUI = () => {
return (
<div>
<p>Loading...</p>
</div>
);
};
Suspenseを使わない実装例
import React, { useState, useEffect } from "react";
// ユーザーの型定義
type User = {
id: number;
name: string;
username: string;
email: string;
};
// データを取得する関数
const getUserData = async (): Promise<User[]> => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
};
// Suspendなしのユーザーデータ表示コンポーネント
export const NotSuspendUser = () => {
const [userData, setUserData] = useState<User[] | null>(null); // データの状態
const [loading, setLoading] = useState(true); // ローディング状態
const [error, setError] = useState<string | null>(null); // エラー状態
useEffect(() => {
// データを取得する非同期関数を定義
const fetchData = async () => {
try {
setLoading(true); // ローディング開始
const data = await getUserData(); // データ取得
setUserData(data); // データを状態に保存
} catch (err: any) {
setError(err.message); // エラーメッセージを保存
} finally {
setLoading(false); // ローディング終了
}
};
fetchData(); // データ取得を実行
}, []); // コンポーネントの初回マウント時に実行
// ローディング中のUI
if (loading) {
return <p>Loading...</p>;
}
// エラーが発生した場合のUI
if (error) {
return <p>Error: {error}</p>;
}
// データが取得できた場合のUI
return (
<div>
{userData && (
<div>
<p>{userData[0].name}</p> {/* 最初のユーザーの名前を表示 */}
</div>
)}
</div>
);
};
→開発者ツールからもわかる通り、レンダリングが2回されています。
1回目はLoading、2回目はデータフェッチ後の画面。データが変更された事で2回目は再レンダリングを実施させているので、ユーザには同じように見えるのですが、パフォーマンス的には1回の表示の方が良さそうですね!
最後に
ReactでSuspenseを使うとデータフェッチした値がundeifinedなのかどうかを検討する必要がなくなるので、良いですね!!
また、パフォーマンス的にも1回のレンダリングで済むのでよいと思います!
次回はまだReact18にversionアップできていない人向けにどうやって表示するのかの記事を作ろうと思います!!!