React Suspenseとは?非同期処理を簡単に扱う方法

React Suspenseの基本概念から実践的な使い方まで詳しく解説。データフェッチングやコード分割での活用方法、Error Boundaryとの組み合わせも紹介します。

Learning Next 運営
47 分で読めます

みなさん、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>
  );
};

毎回loadingerrordataの状態を管理するのって、本当に大変ですよね。

同じようなパターンを何度も書くのは、時間の無駄だし、バグの原因にもなります。

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を活用して、その便利さを体験してみてください!

関連記事