React Server Componentsとは?次世代のレンダリング手法を解説

React Server Componentsの基本概念から実装方法まで詳しく解説。従来のSSRとの違い、メリット・デメリット、Next.js App Routerでの活用方法を紹介します。

Learning Next 運営
68 分で読めます

みなさん、Reactアプリのページ表示が遅いなと感じたことはありませんか?

「もっと速くページを表示できないかな?」 「サーバーの力をもっと活用したい」 「でもSSRって複雑そう...」

こんな悩みを持ったことがある方も多いでしょう。

実は、React Server Componentsという新しい技術が、これらの問題を解決してくれるんです。 従来のレンダリング方法とは全く違うアプローチで、高速でユーザーフレンドリーなアプリを作ることができます。

この記事では、React Server Componentsの基本から実践的な活用方法まで、実際のコード例とともに詳しく解説します。 きっと、あなたのReact開発に新しい可能性をもたらしてくれるはずです!

React Server Componentsって何?

React Server Componentsは、サーバー側で実行されるReactコンポーネントのことです。

従来のReactとはちょっと違うアプローチですね。 でも大丈夫です!順番に理解していきましょう。

従来のReactとの違いを見てみよう

まずは、従来のやり方から見てみましょう。

// 従来のクライアントコンポーネント
const BlogPost = ({ postId }) => {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch(`/api/posts/${postId}`)
      .then(res => res.json())
      .then(data => {
        setPost(data);
        setLoading(false);
      });
  }, [postId]);
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
};

この例では、クライアント側(ブラウザ)でデータを取得しています。 ユーザーはページを開いてから、データが読み込まれるまで待つ必要がありますね。

では、Server Componentsではどうなるでしょうか?

// React Server Component
const BlogPost = async ({ postId }) => {
  // サーバー側で直接データベースアクセス
  const post = await db.posts.findById(postId);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
};

Server Componentsでは、サーバー側で直接データを取得してからHTMLを生成します。 ユーザーが受け取る時には、すでにコンテンツが表示されているんです。

レンダリングの流れを理解しよう

Server Componentsがどのように動作するか、流れを見てみましょう。

  1. ユーザーがページをリクエスト
  2. サーバーでServer Componentが実行される
  3. データベースやAPIからデータを取得
  4. HTMLが生成される
  5. 完成したHTMLがクライアントに送信される
  6. ブラウザに表示される
  7. 必要に応じてClient Componentが動作開始

このように、サーバー側で必要な処理を済ませてから、ユーザーに届けることができるんです。

Server ComponentsとClient Componentsの使い分け

React Server Componentsでは、コンポーネントが2種類に分かれます。

Server Component(サーバーコンポーネント)

// app/blog/[id]/page.js (Server Component)
const BlogPostPage = async ({ params }) => {
  // サーバー側でのみ実行される
  const post = await fetch(`${process.env.API_URL}/posts/${params.id}`)
    .then(res => res.json());
  
  const comments = await fetch(`${process.env.API_URL}/comments?postId=${params.id}`)
    .then(res => res.json());
  
  return (
    <div>
      <h1>{post.title}</h1>
      <p>作成者: {post.author}</p>
      <div>{post.content}</div>
      
      <CommentSection comments={comments} postId={params.id} />
    </div>
  );
};

export default BlogPostPage;

このコンポーネントは、サーバー側で実行されます。 async/awaitを使って、データベースやAPIに直接アクセスできるのが特徴です。

環境変数なども安全に使えるので、APIキーなどの機密情報を扱うのにも向いています。

Client Component(クライアントコンポーネント)

// components/CommentSection.js (Client Component)
'use client'; // この指定が重要!

import { useState } from 'react';

const CommentSection = ({ comments: initialComments, postId }) => {
  const [comments, setComments] = useState(initialComments);
  const [newComment, setNewComment] = useState('');
  const [loading, setLoading] = useState(false);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    
    try {
      const response = await fetch('/api/comments', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId, content: newComment })
      });
      
      const newCommentData = await response.json();
      setComments([...comments, newCommentData]);
      setNewComment('');
    } catch (error) {
      console.error('コメント投稿エラー:', error);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div>
      <h3>コメント ({comments.length})</h3>
      
      {comments.map(comment => (
        <div key={comment.id} style={{ 
          border: '1px solid #eee', 
          padding: '10px', 
          margin: '10px 0' 
        }}>
          <p>{comment.content}</p>
          <small>投稿者: {comment.author}</small>
        </div>
      ))}
      
      <form onSubmit={handleSubmit}>
        <textarea
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
          placeholder="コメントを入力..."
          rows={3}
          style={{ width: '100%', margin: '10px 0' }}
        />
        <button type="submit" disabled={loading}>
          {loading ? '投稿中...' : 'コメント投稿'}
        </button>
      </form>
    </div>
  );
};

export default CommentSection;

Client Componentは、ブラウザ上で動作します。 'use client'という指定をファイルの先頭に書くのがポイントです。

useStateuseEffect、イベントハンドラーなど、インタラクティブな機能はClient Componentで実装します。

従来のSSRとの違いは?

「SSRと何が違うの?」と思った方もいるでしょう。 実際に比較してみましょう。

従来のSSR(Server-Side Rendering)

// 従来のSSR
const BlogPage = ({ post, comments }) => {
  const [newComment, setNewComment] = useState('');
  
  // サーバー側でHTMLを生成
  // クライアント側でハイドレーション
  
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      {comments.map(comment => (
        <div key={comment.id}>{comment.content}</div>
      ))}
      
      <form onSubmit={handleSubmit}>
        <input 
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
        />
        <button type="submit">投稿</button>
      </form>
    </div>
  );
};

// getServerSideProps でデータ取得
export async function getServerSideProps({ params }) {
  const post = await fetch(`/api/posts/${params.id}`);
  const comments = await fetch(`/api/comments?postId=${params.id}`);
  
  return {
    props: { post, comments }
  };
}

SSRでは、サーバー側でHTMLを生成してから、クライアント側で全体をハイドレーションします。 つまり、全部がクライアントコンポーネントとして動作することになります。

React Server Components

// Server Components
const BlogPage = async ({ params }) => {
  // サーバー側で実行、クライアントに送信されない
  const post = await db.posts.findById(params.id);
  const comments = await db.comments.findByPostId(params.id);
  
  return (
    <div>
      {/* Server Component部分 */}
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      
      {/* Client Component部分 */}
      <InteractiveComments initialComments={comments} postId={params.id} />
    </div>
  );
};

Server Componentsでは、必要な部分だけがクライアント側で動作します。 Server Component部分は、HTMLとして送信されるだけで、JavaScriptは含まれません。

これにより、バンドルサイズの削減と高速化が実現できるんです。

Server Componentsの仕組みを詳しく見てみよう

Server Componentsがどのように動作するか、もう少し詳しく理解していきましょう。

サーバー側での実行プロセス

// app/dashboard/page.js
const DashboardPage = async () => {
  // データベースに直接アクセス
  const user = await getUserFromDatabase();
  const analytics = await getAnalyticsData(user.id);
  const notifications = await getNotifications(user.id);
  
  return (
    <div>
      <header>
        <h1>ダッシュボード</h1>
        <UserInfo user={user} />
      </header>
      
      <main>
        <AnalyticsChart data={analytics} />
        <NotificationList notifications={notifications} />
        <QuickActions userId={user.id} />
      </main>
    </div>
  );
};

// サーバー側でのみ実行される関数
async function getUserFromDatabase() {
  // 環境変数やサーバー側リソースにアクセス可能
  const db = await connectToDatabase(process.env.DATABASE_URL);
  return await db.users.findById(getCurrentUserId());
}

async function getAnalyticsData(userId) {
  // サードパーティAPIに直接アクセス
  const response = await fetch(`${process.env.ANALYTICS_API_URL}/users/${userId}`, {
    headers: {
      'Authorization': `Bearer ${process.env.ANALYTICS_API_KEY}`
    }
  });
  return response.json();
}

このコードでは、サーバー側で直接データベースやAPIにアクセスしています。 環境変数のprocess.env.DATABASE_URLprocess.env.ANALYTICS_API_KEYは、クライアント側には送信されません。

セキュリティ的にも安全ですし、データ取得も高速になります。

コンポーネント階層での処理

Server ComponentとClient Componentを組み合わせる方法も見てみましょう。

// UserInfo.js (Server Component)
const UserInfo = async ({ user }) => {
  // 追加のデータ取得
  const profile = await getUserProfile(user.id);
  
  return (
    <div>
      <img src={profile.avatar} alt={user.name} />
      <div>
        <h2>{user.name}</h2>
        <p>{user.email}</p>
        <p>最終ログイン: {profile.lastLogin}</p>
      </div>
      
      {/* Client Componentを組み込み */}
      <ProfileEditButton userId={user.id} />
    </div>
  );
};

// ProfileEditButton.js (Client Component)
'use client';

const ProfileEditButton = ({ userId }) => {
  const [isEditing, setIsEditing] = useState(false);
  
  return (
    <div>
      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? 'キャンセル' : 'プロフィール編集'}
      </button>
      
      {isEditing && <ProfileEditForm userId={userId} />}
    </div>
  );
};

この例では、UserInfo(Server Component)の中にProfileEditButton(Client Component)を組み込んでいます。 Server Componentでデータを取得し、Client Componentでインタラクティブな機能を提供する、という使い分けですね。

効率的なデータフェッチングのパターン

Server Componentsの力を最大限活用するデータ取得パターンを学びましょう。

並列データフェッチング

// 効率的な並列データ取得
const ProductPage = async ({ params }) => {
  // 複数のデータを並列で取得
  const [product, reviews, recommendations] = await Promise.all([
    getProduct(params.id),
    getProductReviews(params.id),
    getRecommendations(params.id)
  ]);
  
  return (
    <div>
      <ProductDetails product={product} />
      <ReviewsSection reviews={reviews} productId={params.id} />
      <RecommendationsSection recommendations={recommendations} />
    </div>
  );
};

// 個別のデータ取得関数
async function getProduct(id) {
  const response = await fetch(`${process.env.API_URL}/products/${id}`, {
    // Next.jsでのキャッシュ設定
    next: { revalidate: 3600 } // 1時間キャッシュ
  });
  return response.json();
}

async function getProductReviews(productId) {
  const response = await fetch(`${process.env.API_URL}/reviews?productId=${productId}`, {
    next: { revalidate: 600 } // 10分キャッシュ
  });
  return response.json();
}

Promise.allを使うことで、複数のデータを並列で取得できます。 順番に取得するよりも、大幅に時間を短縮できますね。

キャッシュ設定も適切に行うことで、さらなる高速化が期待できます。

段階的データロード

// 段階的なデータロード
const BlogPage = async ({ params }) => {
  // 基本データを先に取得
  const post = await getPost(params.slug);
  
  return (
    <div>
      <article>
        <h1>{post.title}</h1>
        <p>投稿日: {post.publishedAt}</p>
        <div>{post.content}</div>
      </article>
      
      {/* 関連データは Suspense で遅延ロード */}
      <Suspense fallback={<CommentsLoading />}>
        <CommentsSection postId={post.id} />
      </Suspense>
      
      <Suspense fallback={<RecommendationsLoading />}>
        <RelatedPosts postId={post.id} />
      </Suspense>
    </div>
  );
};

// 遅延ロードされるコンポーネント
const CommentsSection = async ({ postId }) => {
  // 少し時間のかかるデータ取得
  const comments = await getComments(postId);
  
  return (
    <div>
      <h3>コメント</h3>
      {comments.map(comment => (
        <CommentCard key={comment.id} comment={comment} />
      ))}
    </div>
  );
};

const RelatedPosts = async ({ postId }) => {
  // 関連記事の計算と取得
  const relatedPosts = await getRelatedPosts(postId);
  
  return (
    <div>
      <h3>関連記事</h3>
      {relatedPosts.map(post => (
        <RelatedPostCard key={post.id} post={post} />
      ))}
    </div>
  );
};

// ローディングコンポーネント
const CommentsLoading = () => (
  <div>
    <h3>コメント</h3>
    <div>コメントを読み込み中...</div>
  </div>
);

const RecommendationsLoading = () => (
  <div>
    <h3>関連記事</h3>
    <div>関連記事を読み込み中...</div>
  </div>
);

Suspenseを使うことで、段階的にコンテンツを表示できます。 重要なコンテンツを先に表示して、関連情報は後から読み込むことで、ユーザー体験が向上しますね。

Server Componentsのメリットとデメリット

Server Componentsが実際にどんな効果をもたらすのか、具体例とともに見てみましょう。

こんなに改善される!パフォーマンスの向上

従来のクライアントサイドでの問題

// 従来のクライアントサイドデータフェッチング
const UserDashboard = () => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [analytics, setAnalytics] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchData = async () => {
      // 複数のAPIコール(ウォーターフォール)
      const userResponse = await fetch('/api/user');
      const user = await userResponse.json();
      
      const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
      const posts = await postsResponse.json();
      
      const analyticsResponse = await fetch(`/api/analytics?userId=${user.id}`);
      const analytics = await analyticsResponse.json();
      
      setUser(user);
      setPosts(posts);
      setAnalytics(analytics);
      setLoading(false);
    };
    
    fetchData();
  }, []);
  
  if (loading) return <Loading />;
  
  return (
    <div>
      {/* レンダリング */}
    </div>
  );
};

従来の方法では、以下の問題がありました。

  • ウォーターフォール問題: APIコールが順番に実行される
  • ローディング時間: ユーザーは白い画面を見て待つ必要がある
  • クライアントリソース: ブラウザのリソースを大量消費

Server Components での改善

// Server Components での改善
const UserDashboard = async () => {
  // サーバー側で並列処理
  const [user, posts, analytics] = await Promise.all([
    getUserFromDB(),
    getPostsFromDB(),
    getAnalyticsFromDB()
  ]);
  
  return (
    <div>
      <UserProfile user={user} />
      <PostsList posts={posts} />
      <AnalyticsChart data={analytics} />
    </div>
  );
};

Server Componentsでは、こんな改善が実現できます。

  • 並列処理: 複数のデータを同時に取得
  • 即座の表示: HTMLが生成された状態で届く
  • サーバーパワー: 高性能なサーバーリソースを活用

バンドルサイズの大幅削減

// 従来のクライアントコンポーネント(全てがバンドルに含まれる)
import { format } from 'date-fns'; // バンドルに含まれる
import { marked } from 'marked'; // バンドルに含まれる
import hljs from 'highlight.js'; // バンドルに含まれる

const BlogPost = ({ post }) => {
  const formattedDate = format(new Date(post.createdAt), 'yyyy年MM月dd日');
  const htmlContent = marked(post.content);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{formattedDate}</time>
      <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
    </article>
  );
};

このコードでは、date-fnsmarkedhighlight.jsなどのライブラリがすべてクライアントのバンドルに含まれてしまいます。

// Server Component(サーバー側でのみ実行)
import { format } from 'date-fns'; // バンドルに含まれない
import { marked } from 'marked'; // バンドルに含まれない
import hljs from 'highlight.js'; // バンドルに含まれない

const BlogPost = async ({ postId }) => {
  const post = await getPost(postId);
  
  // サーバー側で処理
  const formattedDate = format(new Date(post.createdAt), 'yyyy年MM月dd日');
  const htmlContent = marked(post.content);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{formattedDate}</time>
      <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
    </article>
  );
};

Server Componentでは、これらのライブラリがクライアントバンドルに含まれません。 結果として、大幅なバンドルサイズの削減が実現できるんです。

セキュリティの大幅向上

// Server Component でのセキュアな処理
const UserProfile = async ({ userId }) => {
  // APIキーや秘密情報をサーバー側でのみ使用
  const userDetails = await fetch(`${process.env.INTERNAL_API_URL}/users/${userId}`, {
    headers: {
      'Authorization': `Bearer ${process.env.INTERNAL_API_KEY}`, // クライアントに露出しない
      'X-Internal-Request': 'true'
    }
  }).then(res => res.json());
  
  // 機密情報をフィルタリング
  const publicUserData = {
    name: userDetails.name,
    avatar: userDetails.avatar,
    publicProfile: userDetails.publicProfile
    // privateInfo は含めない
  };
  
  return (
    <div>
      <img src={publicUserData.avatar} alt={publicUserData.name} />
      <h2>{publicUserData.name}</h2>
      <p>{publicUserData.publicProfile}</p>
    </div>
  );
};

Server Componentでは、以下のようなセキュリティメリットがあります。

  • 機密情報の保護: APIキーなどがクライアントに送信されない
  • データフィルタリング: 必要な情報のみをクライアントに送信
  • 安全な処理: サーバー側でのみセキュアな処理を実行

知っておくべき制約事項

もちろん、Server Componentsにも制約があります。 これらを理解して、適切に使い分けることが重要です。

インタラクティブ機能の制限

// ❌ Server Component では使用できない
const InteractiveComponent = () => {
  const [count, setCount] = useState(0); // エラー: useState は使用不可
  
  const handleClick = () => { // エラー: イベントハンドラー不可
    setCount(count + 1);
  };
  
  useEffect(() => { // エラー: useEffect は使用不可
    console.log('マウントされました');
  }, []);
  
  return (
    <button onClick={handleClick}> 
      カウント: {count}
    </button>
  );
};

// ✅ Client Component で解決
'use client';

const InteractiveComponent = () => {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <button onClick={handleClick}>
      カウント: {count}
    </button>
  );
};

インタラクティブな機能を実装する場合は、Client Componentを使う必要があります。

ブラウザAPIの制限

// ❌ Server Component では使用できない
const LocationComponent = () => {
  const location = window.location; // エラー: window オブジェクト不可
  const storage = localStorage.getItem('data'); // エラー: localStorage 不可
  
  return <div>現在のURL: {location.href}</div>;
};

// ✅ Client Component で解決
'use client';

const LocationComponent = () => {
  const [location, setLocation] = useState('');
  
  useEffect(() => {
    setLocation(window.location.href);
  }, []);
  
  return <div>現在のURL: {location}</div>;
};

ブラウザ固有のAPIを使用する場合も、Client Componentが必要になります。

実際のパフォーマンス改善例

具体的な数値で見ると、どれほどの改善が期待できるでしょうか。

// パフォーマンス測定の例
const PerformanceComparison = {
  // 従来のクライアントサイドレンダリング
  clientSideRendering: {
    firstContentfulPaint: '1.8s',
    timeToInteractive: '3.2s',
    bundleSize: '450KB',
    apiRequests: 5,
    renderBlocking: 'JavaScript実行待ち'
  },
  
  // Server Components使用
  serverComponents: {
    firstContentfulPaint: '0.8s', // 55% 改善
    timeToInteractive: '1.5s', // 53% 改善  
    bundleSize: '180KB', // 60% 削減
    apiRequests: 0, // サーバー側で処理
    renderBlocking: 'なし'
  }
};

このように、適切にServer Componentsを活用することで、大幅なパフォーマンス向上が期待できます。

特に、初期表示速度バンドルサイズの改善効果は顕著ですね。

Next.js App Routerで実際に使ってみよう

ここからは、実際にNext.js App Routerを使ってServer Componentsを実装してみましょう。 コードを見ながら、一歩ずつ理解していきますね。

基本的なプロジェクト構成

まずは、ディレクトリ構造から見てみましょう。

app/
├── layout.js          # ルートレイアウト
├── page.js            # ホームページ
├── globals.css        # グローバルスタイル
├── products/
│   ├── page.js        # 商品一覧ページ
│   ├── [id]/
│   │   ├── page.js    # 商品詳細ページ
│   │   └── loading.js # ローディングUI
│   └── loading.js
├── dashboard/
│   ├── layout.js      # ダッシュボード専用レイアウト
│   ├── page.js        # ダッシュボードトップ
│   ├── analytics/
│   │   └── page.js    # アナリティクス
│   └── settings/
│       └── page.js    # 設定
└── components/
    ├── ClientButton.js
    └── ServerCard.js

この構造で、どのファイルがServer Componentで、どのファイルがClient Componentなのかを理解していきましょう。

ルートレイアウトの実装

// app/layout.js (Server Component)
import './globals.css';

const RootLayout = ({ children }) => {
  return (
    <html lang="ja">
      <head>
        <title>ECサイト</title>
        <meta name="description" content="高品質な商品を提供" />
      </head>
      <body>
        <nav>
          <div>
            <h1>ECサイト</h1>
            <ul>
              <li><a href="/">ホーム</a></li>
              <li><a href="/products">商品一覧</a></li>
              <li><a href="/dashboard">ダッシュボード</a></li>
            </ul>
          </div>
        </nav>
        
        <main>
          {children}
        </main>
        
        <footer>
          <p>&copy; 2025 ECサイト. All rights reserved.</p>
        </footer>
      </body>
    </html>
  );
};

export default RootLayout;

ルートレイアウトは、アプリ全体の基本構造を定義します。 特に'use client'の指定がないので、Server Componentとして動作します。

ナビゲーションやフッターなど、基本的な構造はServer Componentで十分ですね。

商品一覧ページの実装

// app/products/page.js (Server Component)
import Link from 'next/link';
import ProductCard from '@/components/ProductCard';
import SearchFilter from '@/components/SearchFilter';

const ProductsPage = async ({ searchParams }) => {
  // サーバー側でデータ取得
  const { category, search, page = 1 } = searchParams;
  
  const products = await getProducts({
    category,
    search,
    page: parseInt(page),
    limit: 12
  });
  
  const categories = await getCategories();
  
  return (
    <div>
      <h1>商品一覧</h1>
      
      {/* Client Component(検索とフィルタリング) */}
      <SearchFilter categories={categories} />
      
      <div style={{ 
        display: 'grid', 
        gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', 
        gap: '20px' 
      }}>
        {products.items.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      
      {/* ページネーション */}
      <Pagination 
        currentPage={products.currentPage}
        totalPages={products.totalPages}
        baseUrl="/products"
      />
    </div>
  );
};

// データ取得関数
async function getProducts({ category, search, page, limit }) {
  const params = new URLSearchParams();
  if (category) params.append('category', category);
  if (search) params.append('search', search);
  params.append('page', page);
  params.append('limit', limit);
  
  const response = await fetch(`${process.env.API_URL}/products?${params}`, {
    next: { revalidate: 300 } // 5分間キャッシュ
  });
  
  return response.json();
}

async function getCategories() {
  const response = await fetch(`${process.env.API_URL}/categories`, {
    next: { revalidate: 3600 } // 1時間キャッシュ
  });
  
  return response.json();
}

export default ProductsPage;

このページでは、以下のことを行っています。

  1. サーバー側でデータ取得: getProductsgetCategoriesを並列実行
  2. キャッシュ設定: next: { revalidate: 300 }でキャッシュ時間を指定
  3. 検索パラメータの処理: URLパラメータに基づいてフィルタリング

searchParamsを使うことで、URLのクエリパラメータを自動的に受け取れるのも便利ですね。

商品詳細ページの実装

// app/products/[id]/page.js (Server Component)
import { notFound } from 'next/navigation';
import ProductImage from '@/components/ProductImage';
import AddToCartButton from '@/components/AddToCartButton';
import ProductReviews from '@/components/ProductReviews';
import RelatedProducts from '@/components/RelatedProducts';

const ProductDetailPage = async ({ params }) => {
  const product = await getProduct(params.id);
  
  if (!product) {
    notFound(); // 404ページを表示
  }
  
  // 関連データを並列取得
  const [reviews, relatedProducts, inventory] = await Promise.all([
    getProductReviews(params.id),
    getRelatedProducts(product.categoryId, params.id),
    getProductInventory(params.id)
  ]);
  
  return (
    <div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '40px' }}>
        {/* 商品画像(Client Component) */}
        <ProductImage 
          images={product.images}
          alt={product.name}
        />
        
        {/* 商品情報 */}
        <div>
          <h1>{product.name}</h1>
          <p style={{ fontSize: '24px', color: '#e74c3c' }}>
            ¥{product.price.toLocaleString()}
          </p>
          
          <p>{product.description}</p>
          
          <div style={{ margin: '20px 0' }}>
            <strong>在庫状況: </strong>
            <span style={{ 
              color: inventory.available > 0 ? '#27ae60' : '#e74c3c' 
            }}>
              {inventory.available > 0 ? `在庫あり (${inventory.available}個)` : '在庫切れ'}
            </span>
          </div>
          
          {/* カートに追加ボタン(Client Component) */}
          <AddToCartButton 
            productId={product.id}
            available={inventory.available}
          />
        </div>
      </div>
      
      {/* 商品レビュー(Client Component) */}
      <ProductReviews 
        reviews={reviews}
        productId={product.id}
      />
      
      {/* 関連商品(Server Component) */}
      <RelatedProducts products={relatedProducts} />
    </div>
  );
};

// メタデータの生成
export async function generateMetadata({ params }) {
  const product = await getProduct(params.id);
  
  return {
    title: `${product.name} | ECサイト`,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [product.images[0]],
    },
  };
}

// 静的パラメータの生成(ISG)
export async function generateStaticParams() {
  const products = await fetch(`${process.env.API_URL}/products/popular`)
    .then(res => res.json());
  
  return products.map(product => ({
    id: product.id.toString()
  }));
}

export default ProductDetailPage;

この商品詳細ページでは、いくつかの重要な機能を実装しています。

  • 動的ルーティング: [id]フォルダでIDを動的に受け取り
  • 404処理: 商品が存在しない場合の適切な処理
  • メタデータ生成: SEO対応のためのmeta情報自動生成
  • 静的生成: 人気商品を事前に静的生成

Suspenseとストリーミングの活用

次は、段階的なコンテンツ表示を実現してみましょう。

// app/dashboard/page.js
import { Suspense } from 'react';
import UserStats from '@/components/UserStats';
import RecentOrders from '@/components/RecentOrders';
import AnalyticsChart from '@/components/AnalyticsChart';
import ActivityFeed from '@/components/ActivityFeed';

const DashboardPage = () => {
  return (
    <div>
      <h1>ダッシュボード</h1>
      
      <div style={{ 
        display: 'grid', 
        gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', 
        gap: '20px' 
      }}>
        {/* 高速で表示される部分 */}
        <Suspense fallback={<StatsLoading />}>
          <UserStats />
        </Suspense>
        
        {/* 中程度の速度で表示 */}
        <Suspense fallback={<OrdersLoading />}>
          <RecentOrders />
        </Suspense>
        
        {/* 複雑な計算が必要で時間がかかる部分 */}
        <Suspense fallback={<ChartLoading />}>
          <AnalyticsChart />
        </Suspense>
        
        {/* リアルタイムデータで最も時間がかかる */}
        <Suspense fallback={<ActivityLoading />}>
          <ActivityFeed />
        </Suspense>
      </div>
    </div>
  );
};

この実装では、各セクションが異なるタイミングで段階的に表示されます。

重要なコンテンツから順番に表示されるので、ユーザーは待ち時間を感じにくくなります。

// 各コンポーネントは異なる速度でデータを取得
const UserStats = async () => {
  // 高速(キャッシュされたデータ)
  const stats = await getUserStats();
  
  return (
    <div>
      <h3>統計情報</h3>
      <p>総注文数: {stats.totalOrders}</p>
      <p>総支払額: ¥{stats.totalSpent.toLocaleString()}</p>
    </div>
  );
};

const RecentOrders = async () => {
  // 中速(データベースクエリ)
  await new Promise(resolve => setTimeout(resolve, 1000));
  const orders = await getRecentOrders();
  
  return (
    <div>
      <h3>最近の注文</h3>
      {orders.map(order => (
        <div key={order.id}>
          <p>{order.productName} - ¥{order.amount}</p>
        </div>
      ))}
    </div>
  );
};

const AnalyticsChart = async () => {
  // 低速(複雑な分析処理)
  await new Promise(resolve => setTimeout(resolve, 2000));
  const chartData = await getAnalyticsData();
  
  return (
    <div>
      <h3>売上推移</h3>
      <div>チャートコンテンツ: {JSON.stringify(chartData)}</div>
    </div>
  );
};

各コンポーネントで処理時間が違うことで、段階的な表示が実現されます。 ユーザーは、「何も表示されない空白時間」を最小限に抑えることができるんです。

実践的な活用例を見てみよう

ここからは、実際のプロジェクトで活用できる具体例を紹介します。 ECサイトと管理ダッシュボードの実装を通して、Server Componentsの実力を確認してみましょう。

ECサイトの商品カタログシステム

まずは、ECサイトの商品カタログから始めましょう。

// app/catalog/page.js
import { Suspense } from 'react';
import FilterSidebar from '@/components/FilterSidebar';
import ProductGrid from '@/components/ProductGrid';
import FeaturedProducts from '@/components/FeaturedProducts';

const CatalogPage = async ({ searchParams }) => {
  // 基本データを並列取得
  const [categories, brands, featuredProducts] = await Promise.all([
    getCategories(),
    getBrands(),
    getFeaturedProducts()
  ]);
  
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '250px 1fr', gap: '20px' }}>
      {/* サイドバーフィルター(Client Component) */}
      <FilterSidebar 
        categories={categories}
        brands={brands}
        initialFilters={searchParams}
      />
      
      <div>
        {/* 特集商品(Server Component) */}
        <FeaturedProducts products={featuredProducts} />
        
        {/* 商品グリッド(段階的ロード) */}
        <Suspense fallback={<ProductGridSkeleton />}>
          <ProductGrid searchParams={searchParams} />
        </Suspense>
      </div>
    </div>
  );
};

このページでは、以下のような設計になっています。

  • Server Component: 基本データの取得と特集商品の表示
  • Client Component: インタラクティブなフィルター機能
  • Suspense: 商品一覧の段階的ロード
// 商品グリッドコンポーネント
const ProductGrid = async ({ searchParams }) => {
  // フィルター条件に基づいて商品を取得
  const products = await getFilteredProducts(searchParams);
  
  return (
    <div>
      <div style={{ marginBottom: '20px' }}>
        <h2>商品一覧 ({products.total}件)</h2>
        <SortOptions />
      </div>
      
      <div style={{ 
        display: 'grid', 
        gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', 
        gap: '20px' 
      }}>
        {products.items.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      
      {products.hasMore && (
        <LoadMoreButton currentPage={products.currentPage} />
      )}
    </div>
  );
};

getFilteredProducts関数では、検索条件に基づいて適切な商品を取得します。 URLパラメータが変わると、自動的に再レンダリングされるのも便利ですね。

フィルターサイドバー(Client Component)

// components/FilterSidebar.js (Client Component)
'use client';
import { useState, useRouter } from 'next/navigation';

const FilterSidebar = ({ categories, brands, initialFilters }) => {
  const [filters, setFilters] = useState(initialFilters);
  const router = useRouter();
  
  const updateFilters = (newFilters) => {
    setFilters({ ...filters, ...newFilters });
    
    const params = new URLSearchParams();
    Object.entries({ ...filters, ...newFilters }).forEach(([key, value]) => {
      if (value) params.append(key, value);
    });
    
    router.push(`/catalog?${params.toString()}`);
  };
  
  return (
    <div>
      <h3>絞り込み</h3>
      
      <div>
        <h4>カテゴリ</h4>
        {categories.map(category => (
          <label key={category.id}>
            <input
              type="checkbox"
              checked={filters.categories?.includes(category.id)}
              onChange={(e) => {
                const newCategories = e.target.checked
                  ? [...(filters.categories || []), category.id]
                  : (filters.categories || []).filter(id => id !== category.id);
                updateFilters({ categories: newCategories });
              }}
            />
            {category.name}
          </label>
        ))}
      </div>
      
      <div>
        <h4>価格帯</h4>
        <input
          type="range"
          min="0"
          max="100000"
          value={filters.maxPrice || 100000}
          onChange={(e) => updateFilters({ maxPrice: e.target.value })}
        />
        <p>最大: ¥{(filters.maxPrice || 100000).toLocaleString()}</p>
      </div>
      
      <div>
        <h4>ブランド</h4>
        <select 
          value={filters.brand || ''}
          onChange={(e) => updateFilters({ brand: e.target.value })}
        >
          <option value="">すべて</option>
          {brands.map(brand => (
            <option key={brand.id} value={brand.id}>
              {brand.name}
            </option>
          ))}
        </select>
      </div>
    </div>
  );
};

フィルターサイドバーでは、以下の機能を実装しています。

  • インタラクティブな操作: チェックボックス、スライダー、セレクトボックス
  • URLの更新: フィルター条件をURLパラメータに反映
  • 状態管理: ローカルstateでフィルター状態を管理

管理者向けダッシュボード

次に、管理者向けのリアルタイムダッシュボードを見てみましょう。

// app/admin/dashboard/page.js
import { Suspense } from 'react';
import SalesOverview from '@/components/dashboard/SalesOverview';
import OrdersTable from '@/components/dashboard/OrdersTable';
import InventoryAlerts from '@/components/dashboard/InventoryAlerts';
import CustomerMetrics from '@/components/dashboard/CustomerMetrics';

const AdminDashboard = () => {
  return (
    <div>
      <h1>管理ダッシュボード</h1>
      
      {/* KPI概要(高優先度) */}
      <div style={{ 
        display: 'grid', 
        gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', 
        gap: '20px',
        marginBottom: '30px'
      }}>
        <Suspense fallback={<MetricCardSkeleton />}>
          <SalesMetricCard />
        </Suspense>
        <Suspense fallback={<MetricCardSkeleton />}>
          <OrdersMetricCard />
        </Suspense>
        <Suspense fallback={<MetricCardSkeleton />}>
          <RevenueMetricCard />
        </Suspense>
        <Suspense fallback={<MetricCardSkeleton />}>
          <CustomerMetricCard />
        </Suspense>
      </div>
      
      {/* 詳細セクション */}
      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '20px' }}>
        <div>
          <Suspense fallback={<ChartSkeleton />}>
            <SalesOverview />
          </Suspense>
          
          <Suspense fallback={<TableSkeleton />}>
            <OrdersTable />
          </Suspense>
        </div>
        
        <div>
          <Suspense fallback={<AlertsSkeleton />}>
            <InventoryAlerts />
          </Suspense>
          
          <Suspense fallback={<CustomersSkeleton />}>
            <CustomerMetrics />
          </Suspense>
        </div>
      </div>
    </div>
  );
};

ダッシュボードでは、複数のデータソースから情報を取得する必要があります。 Suspenseを使うことで、それぞれのデータが取得でき次第、順次表示していくことができます。

KPIメトリックカードの実装

// KPIメトリックカード
const SalesMetricCard = async () => {
  const salesData = await getDailySales();
  const previousSales = await getPreviousDaySales();
  const change = ((salesData.total - previousSales.total) / previousSales.total) * 100;
  
  return (
    <div style={{ 
      padding: '20px', 
      border: '1px solid #ddd', 
      borderRadius: '8px',
      backgroundColor: '#fff'
    }}>
      <h3>本日の売上</h3>
      <p style={{ fontSize: '28px', color: '#2ecc71', margin: '10px 0' }}>
        ¥{salesData.total.toLocaleString()}
      </p>
      <p style={{ 
        color: change >= 0 ? '#27ae60' : '#e74c3c',
        fontSize: '14px'
      }}>
        前日比 {change >= 0 ? '+' : ''}{change.toFixed(1)}%
      </p>
    </div>
  );
};

const OrdersMetricCard = async () => {
  const ordersData = await getTodayOrders();
  
  return (
    <div style={{ 
      padding: '20px', 
      border: '1px solid #ddd', 
      borderRadius: '8px',
      backgroundColor: '#fff'
    }}>
      <h3>受注件数</h3>
      <p style={{ fontSize: '28px', color: '#3498db', margin: '10px 0' }}>
        {ordersData.count}件
      </p>
      <p style={{ fontSize: '14px', color: '#666' }}>
        処理中: {ordersData.pending}件
      </p>
    </div>
  );
};

各メトリックカードでは、リアルタイムのデータを表示しています。 前日比較や処理状況なども含めて、管理者が必要な情報を瞬時に把握できるようになっています。

インタラクティブチャート(Client Component)

// インタラクティブチャート(Client Component)
'use client';
import { useState } from 'react';
import { Line } from 'react-chartjs-2';

const InteractiveChart = ({ data }) => {
  const [selectedPeriod, setSelectedPeriod] = useState('30days');
  
  return (
    <div>
      <div style={{ marginBottom: '20px' }}>
        <select 
          value={selectedPeriod}
          onChange={(e) => setSelectedPeriod(e.target.value)}
        >
          <option value="7days">過去7日</option>
          <option value="30days">過去30日</option>
          <option value="90days">過去90日</option>
        </select>
      </div>
      
      <Line data={data} options={{
        responsive: true,
        plugins: {
          legend: {
            display: true
          }
        },
        scales: {
          y: {
            beginAtZero: true,
            ticks: {
              callback: function(value) {
                return '¥' + value.toLocaleString();
              }
            }
          }
        }
      }} />
    </div>
  );
};

チャートコンポーネントでは、ユーザーが期間を選択できるインタラクティブな機能を提供しています。 このような操作が必要な部分は、Client Componentとして実装することが重要ですね。

まとめ

React Server Componentsについて、基本概念から実践的な活用方法まで詳しく解説してきました。

Server Componentsの主な特徴

  • サーバー側実行: データベースアクセスやAPI呼び出しを直接実行
  • ゼロバンドル: サーバー側ライブラリがクライアントに含まれない
  • 高速表示: HTMLが事前生成されるため初期表示が高速
  • セキュリティ: 機密情報をサーバー側でのみ処理

従来技術との違い

  • CSR(クライアントサイドレンダリング): より高速な初期表示を実現
  • SSR(サーバーサイドレンダリング): より効率的なハイドレーション
  • ISG(インクリメンタル静的生成): リアルタイムデータとの組み合わせが可能

実装時のポイント

  • 適切な分離: Server ComponentとClient Componentの使い分けが重要
  • 段階的ロード: Suspenseを活用したストリーミング表示
  • 効率的なデータ取得: 並列処理とキャッシュの適切な設定
  • エラーハンドリング: 適切な境界設定とフォールバック

活用が効果的な場面

  • ECサイト: 商品カタログと動的フィルタリング
  • ダッシュボード: リアルタイム分析とKPI表示
  • ブログ・CMS: SEO最適化とコンテンツ管理
  • 管理システム: 大量データの効率的な表示

開発のベストプラクティス

  • 段階的導入: 既存プロジェクトでの部分的な採用から始める
  • パフォーマンス測定: Core Web Vitalsなどの指標で効果を確認
  • ユーザビリティ: ローディング状態を適切に表示
  • 保守性: コンポーネント設計の一貫性を保つ

React Server Componentsは、現代のWeb開発における重要な技術革新です。 従来のアプローチでは実現が困難だった、高性能かつユーザーフレンドリーなアプリケーションを構築できます。

適切に活用することで、ユーザー体験の大幅な向上開発効率の改善を同時に実現できるでしょう。

ぜひ、あなたの次のプロジェクトでServer Componentsを試してみてください。 きっと、その効果を実感していただけるはずです!

関連記事