React Suspenseとは?非同期処理を簡単に扱う方法
React Suspenseの基本概念から実践的な使い方まで詳しく解説。データフェッチングやコード分割での活用方法、Error Boundaryとの組み合わせも紹介します。
みなさん、Reactで非同期処理を書いていて、こんな悩みありませんか?
「ローディング状態の管理が複雑すぎる」 「毎回同じような loading, error, data の処理を書くのが面倒」 「もっとシンプルに非同期処理を扱いたい」
そんな悩みを解決してくれるのが、React Suspenseなんです。
この記事では、Suspenseの基本概念から実践的な使い方まで、わかりやすく解説していきます。 データフェッチングやコード分割での活用方法、Error Boundaryとの組み合わせも具体例と一緒に紹介しますよ。
一緒に、もっとスマートなReact開発を身につけていきましょう!
React Suspenseって何?
まずは、Suspenseの基本的な考え方から理解していきましょう。
従来の非同期処理の問題点
これまでのReactでは、非同期処理のたびに面倒なコードを書く必要がありました。
// 従来の方法(複雑で面倒)
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
毎回loading
、error
、data
の状態を管理するのって、本当に大変ですよね。
同じようなパターンを何度も書くのは、時間の無駄だし、バグの原因にもなります。
Suspenseによる劇的な改善
Suspenseを使うと、こんなにシンプルになります!
// Suspense を使った方法(とってもシンプル!)
const UserProfile = ({ userId }) => {
const user = useUser(userId); // カスタムフックでデータを取得
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
// 親コンポーネントでSuspenseを使用
const App = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userId={1} />
</Suspense>
);
};
見てください! コンポーネントは純粋にデータの表示だけに集中できています。
ローディング状態やエラー処理は、Suspenseが勝手に面倒を見てくれるんです。
Suspenseの仕組み
「でも、これってどうやって動いてるの?」と思いますよね。
実は、Suspenseはとても面白い仕組みで動いています。
Promiseを「投げる」魔法
Suspenseは、コンポーネントがPromiseを「投げる」ことで動作します。
// Suspense対応のカスタムフック
const useUser = (userId) => {
// キャッシュをチェック
if (userCache.has(userId)) {
return userCache.get(userId);
}
// 進行中のリクエストをチェック
if (pendingRequests.has(userId)) {
throw pendingRequests.get(userId); // Promiseを投げる!
}
// 新しいリクエストを開始
const promise = fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
userCache.set(userId, user);
pendingRequests.delete(userId);
return user;
});
pendingRequests.set(userId, promise);
throw promise; // Promiseを投げる!
};
コンポーネントがPromiseを投げると、Suspenseがそれをキャッチしてfallback
を表示します。
Promiseが解決されると、コンポーネントが再レンダリングされて、データが表示されるんです。
Suspenseの境界を理解する
Suspenseは、最も近い親のSuspense境界まで「バブリング」します。
const App = () => {
return (
<div>
<h1>ユーザー管理アプリ</h1>
{/* 外側のSuspense境界 */}
<Suspense fallback={<div>アプリケーションを読み込み中...</div>}>
<Header />
{/* 内側のSuspense境界 */}
<Suspense fallback={<div>ユーザーデータを読み込み中...</div>}>
<UserProfile userId={1} />
</Suspense>
<Suspense fallback={<div>投稿データを読み込み中...</div>}>
<UserPosts userId={1} />
</Suspense>
</Suspense>
</div>
);
};
このように、適切な粒度でSuspense境界を設定することで、ユーザーにとってわかりやすいローディング表示ができます。
データフェッチングで活用してみよう
実際にSuspenseを使ったデータフェッチングの方法を学んでいきましょう。
基本的なデータフェッチング
まずは、シンプルなデータフェッチングから始めます。
データフェッチング用のユーティリティを作ろう
// データフェッチング用のユーティリティ
const createResource = (promise) => {
let status = 'pending';
let result;
const suspender = promise.then(
(res) => {
status = 'success';
result = res;
},
(err) => {
status = 'error';
result = err;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
}
};
};
このcreateResource
関数が、Suspenseとデータフェッチングを繋ぐ重要な役割を果たします。
promiseが解決されるまではsuspender
を投げて、解決されたらデータを返すシンプルな仕組みです。
APIリクエスト関数を作ろう
// APIリクエスト関数
const fetchUser = (userId) => {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
});
};
// リソースの作成
const userResource = createResource(fetchUser(1));
// コンポーネントでの使用
const UserInfo = () => {
const user = userResource.read(); // データを読み込み
return (
<div className="user-info">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>部署: {user.department}</p>
</div>
);
};
userResource.read()
を呼ぶだけで、Suspenseが自動的にローディング状態を管理してくれます。
親コンポーネントでSuspenseを使おう
// 親コンポーネント
const UserPage = () => {
return (
<div>
<h1>ユーザー情報</h1>
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo />
</Suspense>
</div>
);
};
// スケルトンローディング
const UserInfoSkeleton = () => (
<div className="user-info skeleton">
<div className="skeleton-avatar"></div>
<div className="skeleton-name"></div>
<div className="skeleton-email"></div>
<div className="skeleton-department"></div>
</div>
);
スケルトンローディングを使うことで、実際のコンテンツの構造を事前に示すことができます。
ユーザーにとっても「何が読み込まれるのか」がわかりやすくなりますね。
複数データの並列フェッチング
一つのコンポーネントで複数のデータを取得する場合の実装を見てみましょう。
// 複数のリソースを作成
const userResource = createResource(fetchUser(1));
const postsResource = createResource(fetchUserPosts(1));
const followersResource = createResource(fetchUserFollowers(1));
// 複数データを使用するコンポーネント
const UserDashboard = () => {
const user = userResource.read();
const posts = postsResource.read();
const followers = followersResource.read();
return (
<div className="user-dashboard">
<div className="user-summary">
<h2>{user.name}</h2>
<p>投稿数: {posts.length}</p>
<p>フォロワー数: {followers.length}</p>
</div>
<div className="dashboard-content">
<section className="recent-posts">
<h3>最近の投稿</h3>
{posts.slice(0, 5).map(post => (
<div key={post.id} className="post-item">
<h4>{post.title}</h4>
<p>{post.excerpt}</p>
</div>
))}
</section>
<section className="followers">
<h3>フォロワー</h3>
{followers.slice(0, 10).map(follower => (
<div key={follower.id} className="follower-item">
<img src={follower.avatar} alt={follower.name} />
<span>{follower.name}</span>
</div>
))}
</section>
</div>
</div>
);
};
このコンポーネントでは、3つのAPIを並列で呼び出しています。
Suspenseのおかげで、すべてのデータが揃ってからコンポーネントが表示されます。
カスタムフックで再利用性を高めよう
再利用可能なデータフェッチングフックを作ってみましょう。
汎用的なSuspenseフック
// キャッシュとリクエスト管理
const cache = new Map();
const pendingRequests = new Map();
// 汎用的なSuspenseフック
const useSuspenseQuery = (key, fetcher) => {
// キャッシュにデータがある場合
if (cache.has(key)) {
return cache.get(key);
}
// 進行中のリクエストがある場合
if (pendingRequests.has(key)) {
throw pendingRequests.get(key);
}
// 新しいリクエストを開始
const promise = fetcher()
.then(data => {
cache.set(key, data);
pendingRequests.delete(key);
return data;
})
.catch(error => {
pendingRequests.delete(key);
throw error;
});
pendingRequests.set(key, promise);
throw promise;
};
この汎用フックを使うことで、どんなデータフェッチングでもSuspenseが使えるようになります。
具体的なデータフェッチングフック
// 具体的なデータフェッチングフック
const useUser = (userId) => {
return useSuspenseQuery(
`user-${userId}`,
() => fetch(`/api/users/${userId}`).then(res => res.json())
);
};
const useUserPosts = (userId) => {
return useSuspenseQuery(
`user-posts-${userId}`,
() => fetch(`/api/users/${userId}/posts`).then(res => res.json())
);
};
const useComments = (postId) => {
return useSuspenseQuery(
`comments-${postId}`,
() => fetch(`/api/posts/${postId}/comments`).then(res => res.json())
);
};
これで、どのコンポーネントからでも簡単にデータフェッチングができますね。
コンポーネントでの使用例
// コンポーネントでの使用
const UserProfile = ({ userId }) => {
const user = useUser(userId);
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.bio}</p>
<img src={user.avatar} alt={user.name} />
</div>
);
};
const PostList = ({ userId }) => {
const posts = useUserPosts(userId);
return (
<div className="post-list">
{posts.map(post => (
<Suspense key={post.id} fallback={<PostSkeleton />}>
<PostWithComments post={post} />
</Suspense>
))}
</div>
);
};
const PostWithComments = ({ post }) => {
const comments = useComments(post.id);
return (
<article className="post">
<h3>{post.title}</h3>
<p>{post.content}</p>
<div className="comments">
<h4>コメント ({comments.length})</h4>
{comments.map(comment => (
<div key={comment.id} className="comment">
<strong>{comment.author}</strong>
<p>{comment.text}</p>
</div>
))}
</div>
</article>
);
};
カスタムフックのおかげで、コンポーネントがとてもシンプルになりました。
それぞれのコンポーネントは、自分の役割だけに集中できています。
エラー処理も一緒に考えよう
Suspenseと組み合わせて、エラー処理も宣言的に行いましょう。
Error Boundaryの実装
まずは、エラーをキャッチするError Boundaryを作ります。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback(this.state.error);
}
return (
<div className="error-boundary">
<h2>エラーが発生しました</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
再試行
</button>
</div>
);
}
return this.props.children;
}
}
Error Boundaryは、コンポーネントツリーの中でエラーをキャッチして、適切なフォールバックUIを表示します。
SuspenseとError Boundaryの組み合わせ
const DataSection = ({ userId }) => {
return (
<ErrorBoundary fallback={(error) => (
<div className="error-message">
<h3>データの読み込みに失敗しました</h3>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
ページを再読み込み
</button>
</div>
)}>
<Suspense fallback={<LoadingSpinner />}>
<UserProfile userId={userId} />
<UserPosts userId={userId} />
</Suspense>
</ErrorBoundary>
);
};
Error BoundaryでSuspenseを包むことで、ローディング状態とエラー状態の両方を宣言的に処理できます。
より細かい粒度でのエラー処理
const App = () => {
return (
<div className="app">
<h1>ユーザーダッシュボード</h1>
{/* ユーザー情報のエラー処理 */}
<ErrorBoundary fallback={() => <div>ユーザー情報の取得に失敗</div>}>
<Suspense fallback={<UserProfileSkeleton />}>
<UserProfile userId={1} />
</Suspense>
</ErrorBoundary>
{/* 投稿情報のエラー処理 */}
<ErrorBoundary fallback={() => <div>投稿情報の取得に失敗</div>}>
<Suspense fallback={<PostListSkeleton />}>
<PostList userId={1} />
</Suspense>
</ErrorBoundary>
</div>
);
};
このように、それぞれのセクションごとにError BoundaryとSuspenseを設定することで、一部でエラーが発生してもアプリ全体が止まることを防げます。
コード分割で更に活用しよう
Suspenseは、動的インポートとの組み合わせでコード分割にも活用できます。
React.lazyとの組み合わせ
コンポーネントを必要な時にだけ読み込む方法です。
import { lazy, Suspense } from 'react';
// 動的インポートでコンポーネントを遅延読み込み
const UserSettings = lazy(() => import('./components/UserSettings'));
const AdminPanel = lazy(() => import('./components/AdminPanel'));
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
// ルーティングでの使用例
const App = () => {
const [currentPage, setCurrentPage] = useState('home');
const renderPage = () => {
switch (currentPage) {
case 'settings':
return (
<Suspense fallback={<PageSkeleton />}>
<UserSettings />
</Suspense>
);
case 'admin':
return (
<Suspense fallback={<PageSkeleton />}>
<AdminPanel />
</Suspense>
);
case 'reports':
return (
<Suspense fallback={<PageSkeleton />}>
<ReportsPage />
</Suspense>
);
default:
return <HomePage />;
}
};
return (
<div className="app">
<Navigation onPageChange={setCurrentPage} />
<main>
{renderPage()}
</main>
</div>
);
};
ページごとにコードを分割することで、初期ロードを高速化できます。
必要な時にだけコンポーネントを読み込むので、メモリ使用量も節約できますね。
React Routerとの組み合わせ
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// 各ページコンポーネントを遅延読み込み
const HomePage = lazy(() => import('./pages/HomePage'));
const UserPage = lazy(() => import('./pages/UserPage'));
const ProductPage = lazy(() => import('./pages/ProductPage'));
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
// ページ遷移時のローディングコンポーネント
const PageLoader = () => (
<div className="page-loader">
<div className="loader-spinner"></div>
<p>ページを読み込み中...</p>
</div>
);
const App = () => {
return (
<BrowserRouter>
<div className="app">
<Header />
<main>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/users/:id" element={<UserPage />} />
<Route path="/products/:id" element={<ProductPage />} />
<Route path="/checkout" element={<CheckoutPage />} />
</Routes>
</Suspense>
</main>
<Footer />
</div>
</BrowserRouter>
);
};
React Routerと組み合わせることで、ページ遷移時のローディング状態も自然に管理できます。
条件付きコード分割
特定の条件下でのみコンポーネントを読み込む実装です。
const ConditionalComponents = () => {
const userRole = useUserRole();
const [showAdvanced, setShowAdvanced] = useState(false);
// 管理者用コンポーネントの動的読み込み
const AdminComponent = lazy(() =>
userRole === 'admin'
? import('./components/AdminDashboard')
: Promise.resolve({ default: () => <div>権限がありません</div> })
);
// 高度な機能コンポーネントの動的読み込み
const AdvancedFeatures = lazy(() => import('./components/AdvancedFeatures'));
return (
<div>
<h2>ダッシュボード</h2>
{/* 管理者用セクション */}
{userRole === 'admin' && (
<section>
<h3>管理者機能</h3>
<Suspense fallback={<div>管理者画面を読み込み中...</div>}>
<AdminComponent />
</Suspense>
</section>
)}
{/* 高度な機能(オプション) */}
<section>
<button onClick={() => setShowAdvanced(!showAdvanced)}>
{showAdvanced ? '高度な機能を非表示' : '高度な機能を表示'}
</button>
{showAdvanced && (
<Suspense fallback={<div>高度な機能を読み込み中...</div>}>
<AdvancedFeatures />
</Suspense>
)}
</section>
</div>
);
};
必要な時にのみコンポーネントを読み込むことで、パフォーマンスを最適化できます。
特に、重い機能や使用頻度の低い機能に有効ですね。
パフォーマンスを最適化しよう
Suspenseを使ったパフォーマンス最適化のテクニックを学びましょう。
効率的なローディング表示
ユーザー体験を向上させるローディング表示の実装方法です。
スケルトンスクリーンを作ろう
// 再利用可能なスケルトンコンポーネント
const Skeleton = ({ width = '100%', height = '20px', className = '' }) => (
<div
className={`skeleton ${className}`}
style={{ width, height }}
/>
);
// ユーザープロフィール用スケルトン
const UserProfileSkeleton = () => (
<div className="user-profile-skeleton">
<Skeleton width="80px" height="80px" className="avatar" />
<div className="info">
<Skeleton width="150px" height="24px" className="name" />
<Skeleton width="200px" height="16px" className="email" />
<Skeleton width="120px" height="16px" className="role" />
</div>
</div>
);
// 投稿リスト用スケルトン
const PostListSkeleton = () => (
<div className="post-list-skeleton">
{Array.from({ length: 5 }, (_, index) => (
<div key={index} className="post-skeleton">
<Skeleton width="100%" height="24px" className="title" />
<Skeleton width="100%" height="60px" className="content" />
<div className="meta">
<Skeleton width="80px" height="16px" />
<Skeleton width="100px" height="16px" />
</div>
</div>
))}
</div>
);
スケルトンスクリーンを使うことで、ユーザーに「何が読み込まれるのか」を事前に示すことができます。
通常のローディングスピナーよりも、ユーザーの待機時間を短く感じさせる効果があります。
プログレッシブローディング
段階的に情報を表示する方法です。
const ProgressiveContent = () => {
return (
<div>
{/* 最初に基本情報を表示 */}
<Suspense fallback={<UserBasicSkeleton />}>
<UserBasicInfo />
</Suspense>
{/* 次に詳細情報を表示 */}
<Suspense fallback={<UserDetailsSkeleton />}>
<UserDetails />
</Suspense>
{/* 最後に関連データを表示 */}
<Suspense fallback={<RelatedDataSkeleton />}>
<RelatedData />
</Suspense>
</div>
);
};
// 段階的に情報を表示するコンポーネント
const UserPage = ({ userId }) => {
return (
<div className="user-page">
{/* 即座に表示される基本レイアウト */}
<header className="page-header">
<h1>ユーザー詳細</h1>
<nav>
<button>編集</button>
<button>削除</button>
</nav>
</header>
<main>
{/* 段階的にコンテンツを読み込み */}
<ProgressiveContent />
</main>
</div>
);
};
重要な情報から順番に表示することで、体感パフォーマンスを向上させることができます。
キャッシュ戦略で高速化
効率的なデータキャッシュの実装方法です。
LRUキャッシュの実装
class LRUCache {
constructor(maxSize = 50) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
// アクセスされたアイテムを最新に移動
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return undefined;
}
set(key, value) {
if (this.cache.has(key)) {
// 既存のキーを削除
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
// 最も古いアイテムを削除
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
has(key) {
return this.cache.has(key);
}
clear() {
this.cache.clear();
}
}
// グローバルキャッシュインスタンス
const dataCache = new LRUCache(100);
const requestCache = new LRUCache(50);
LRUキャッシュを使うことで、メモリ使用量を抑えながら効率的にデータをキャッシュできます。
改良されたSuspenseフック
// 改良されたSuspenseフック
const useSuspenseQuery = (key, fetcher, options = {}) => {
const { ttl = 5 * 60 * 1000 } = options; // デフォルト5分のTTL
// キャッシュから取得
const cached = dataCache.get(key);
if (cached) {
const { data, timestamp } = cached;
// TTLチェック
if (Date.now() - timestamp < ttl) {
return data;
} else {
// 期限切れのデータを削除
dataCache.delete(key);
}
}
// 進行中のリクエストをチェック
if (requestCache.has(key)) {
throw requestCache.get(key);
}
// 新しいリクエストを開始
const promise = fetcher()
.then(data => {
// データをキャッシュに保存
dataCache.set(key, {
data,
timestamp: Date.now()
});
requestCache.delete(key);
return data;
})
.catch(error => {
requestCache.delete(key);
throw error;
});
requestCache.set(key, promise);
throw promise;
};
TTL(Time To Live)を設定することで、古いデータを自動的に削除できます。
データの種類に応じて、適切なキャッシュ期間を設定しましょう。
実践的なアプリケーション例
実際のアプリケーションでのSuspense活用例を見てみましょう。
ダッシュボードアプリケーション
// メインダッシュボードコンポーネント
const Dashboard = () => {
const [selectedUserId, setSelectedUserId] = useState(1);
const [activeTab, setActiveTab] = useState('overview');
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>管理ダッシュボード</h1>
<UserSelector
selectedUserId={selectedUserId}
onUserChange={setSelectedUserId}
/>
</header>
<nav className="dashboard-nav">
<button
className={activeTab === 'overview' ? 'active' : ''}
onClick={() => setActiveTab('overview')}
>
概要
</button>
<button
className={activeTab === 'analytics' ? 'active' : ''}
onClick={() => setActiveTab('analytics')}
>
分析
</button>
<button
className={activeTab === 'settings' ? 'active' : ''}
onClick={() => setActiveTab('settings')}
>
設定
</button>
</nav>
<main className="dashboard-content">
<ErrorBoundary fallback={<DashboardError />}>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent
userId={selectedUserId}
activeTab={activeTab}
/>
</Suspense>
</ErrorBoundary>
</main>
</div>
);
};
ダッシュボードの全体構造です。
Suspenseを使うことで、タブ切り替え時の状態管理がとてもシンプルになっています。
ダッシュボードコンテンツ
// ダッシュボードコンテンツ
const DashboardContent = ({ userId, activeTab }) => {
const user = useUser(userId);
const renderTabContent = () => {
switch (activeTab) {
case 'overview':
return (
<div className="overview-grid">
<Suspense fallback={<StatsSkeleton />}>
<UserStats userId={userId} />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity userId={userId} />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<ActivityChart userId={userId} />
</Suspense>
</div>
);
case 'analytics':
return (
<Suspense fallback={<AnalyticsSkeleton />}>
<AnalyticsPanel userId={userId} />
</Suspense>
);
case 'settings':
return (
<Suspense fallback={<SettingsSkeleton />}>
<UserSettings userId={userId} />
</Suspense>
);
default:
return <div>タブが見つかりません</div>;
}
};
return (
<div className="dashboard-content">
<div className="user-header">
<img src={user.avatar} alt={user.name} />
<div>
<h2>{user.name}</h2>
<p>{user.role} • {user.department}</p>
</div>
</div>
{renderTabContent()}
</div>
);
};
各セクションごとにSuspenseを設定することで、細かいローディング制御ができています。
一部のデータが遅くても、他の部分は先に表示されるんです。
統計情報コンポーネント
// 統計情報コンポーネント
const UserStats = ({ userId }) => {
const stats = useUserStats(userId);
return (
<div className="stats-grid">
<div className="stat-card">
<h3>総投稿数</h3>
<p className="stat-number">{stats.totalPosts}</p>
</div>
<div className="stat-card">
<h3>今月の投稿</h3>
<p className="stat-number">{stats.monthlyPosts}</p>
</div>
<div className="stat-card">
<h3>総いいね数</h3>
<p className="stat-number">{stats.totalLikes}</p>
</div>
<div className="stat-card">
<h3>フォロワー数</h3>
<p className="stat-number">{stats.followers}</p>
</div>
</div>
);
};
// 最近のアクティビティ
const RecentActivity = ({ userId }) => {
const activities = useUserActivities(userId);
return (
<div className="activity-feed">
<h3>最近のアクティビティ</h3>
<div className="activity-list">
{activities.map(activity => (
<div key={activity.id} className="activity-item">
<div className="activity-icon">
{getActivityIcon(activity.type)}
</div>
<div className="activity-content">
<p>{activity.description}</p>
<time>{formatRelativeTime(activity.timestamp)}</time>
</div>
</div>
))}
</div>
</div>
);
};
それぞれのコンポーネントは、純粋にデータの表示に集中できています。
Suspenseが面倒な状態管理を全部やってくれるからですね。
まとめ
React Suspenseは、非同期処理を劇的にシンプルにしてくれる素晴らしい機能です。
Suspenseの主な利点
コードの簡素化
- 複雑なローディング状態管理が不要
- 毎回同じパターンを書く必要がない
- コンポーネントが本来の役割に集中できる
ユーザー体験の向上
- 段階的ローディングとスケルトンスクリーン
- 適切な粒度でのローディング制御
- エラー処理も宣言的に管理
パフォーマンス最適化
- コード分割による初期ロードの高速化
- 効率的なキャッシュ戦略
- 必要な時だけの動的読み込み
実践的な活用ポイント
データフェッチング
- カスタムフックでの再利用性向上
- 複数データの並列取得
- キャッシュを活用した効率化
コード分割
- React.lazyとの組み合わせ
- 条件付きコンポーネント読み込み
- プリロード戦略の実装
エラー処理
- Error Boundaryとの組み合わせ
- 適切な境界設定
- ユーザーフレンドリーなエラー表示
これからの開発で
React Suspenseを使うことで、より宣言的で読みやすいコードが書けるようになります。
従来の複雑な非同期処理管理から解放されて、本当に大切なビジネスロジックに集中できるんです。
最初は慣れないかもしれませんが、一度使い始めると「もう戻れない」と感じるはずです。
ぜひ、実際のプロジェクトでSuspenseを活用して、その便利さを体験してみてください!