0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js SC/CC移行でハマる!状態管理とデータ取得の解決策

0
Posted at

多くのNext.js開発者がApp Routerへの移行時に直面するのが、Server ComponentsとClient Componentsの適切な使い分けです。特に「Server ComponentでuseStateが使えない!」「Client ComponentでデータフェッチしたらAPIキーが漏れた!」「どこまで"use client"を書けばいいの?」といった悩みにぶつかることは少なくありません。これを知らないと、アプリケーションのパフォーマンス低下やセキュリティリスク、そして何よりも開発効率の悪化を招きかねません。

この記事では、Next.jsのServer ComponentsとClient Componentsが混在する環境において、状態管理とデータ取得のベストプラクティスを、具体的なコード例とよくあるエラー事例を交えて徹底解説します。この記事を読めば、Next.jsの新しいコンポーネントモデルを理解し、効率的で安全なアプリケーション開発の道筋が見えるでしょう。

Next.jsのServer/Client Componentsとは?基本概念の理解

Next.js v13以降のApp Routerでは、デフォルトでReact Server Components (RSC)が採用されています。このセクションでは、まずServer ComponentsとClient Componentsの基本的な役割と違いを理解します。

Server Components (SC) の役割と特徴

Server Componentsは、その名の通りサーバーサイドでレンダリングされるコンポーネントです。クライアントに送信されるJavaScriptバンドルに含まれないため、以下の特徴を持ちます。

  • 初期ロードパフォーマンスの向上: クライアントに送るJavaScriptの量を減らし、初期ロードを高速化します。
  • データ取得の最適化: データベースやバックエンドAPIに直接アクセスでき、機密情報(APIキーなど)をクライアントに露出させることなくデータをフェッチできます。
  • セキュリティの向上: サーバーサイドでのみ実行されるため、機密性の高い処理を安全に行えます。
  • SEOの改善: サーバーでHTMLが生成されるため、検索エンジンによるクローリングが容易になります。
  • React Hooksの制限: useStateuseEffectなどのReact Hooksは使用できません。ブラウザAPI(window, localStorageなど)も使用できません。

Next.jsのApp Routerでは、すべてのコンポーネントがデフォルトでServer Componentsとして扱われます。

Client Components (CC) の役割と特徴

Client Componentsは、クライアントサイドでレンダリングされ、インタラクティブな機能を提供するコンポーネントです。ファイルの先頭に"use client"ディレクティブを記述することで宣言します。

  • インタラクティブなUI: useState, useEffectなどのReact Hooksを使用でき、ユーザーの操作に応じて状態を更新したり、副作用を実行したりできます。
  • ブラウザAPIへのアクセス: window, localStorageなどのブラウザAPIに直接アクセスできます。
  • バンドルサイズへの影響: クライアントに送信されるJavaScriptバンドルに含まれるため、多用しすぎるとバンドルサイズが増加し、初期ロードが遅くなる可能性があります。

基本的には、ユーザー操作が必要な部分やブラウザAPIに依存する部分にのみClient Componentsを使用し、それ以外の部分はServer Componentsとして残すのがベストプラクティスです。

Server Componentsでのデータ取得とClient Componentsへの連携

データ取得は、Next.js App Routerにおける重要な設計判断の一つです。ここでは、Server ComponentsとClient Componentsそれぞれでのデータ取得方法とその連携を見ていきます。

Server Componentsでのデータ取得

Server Componentsでは、サーバーサイドで直接データをフェッチできます。これにより、クライアントへのネットワーク往復を減らし、パフォーマンスを向上させることができます。

Server Componentsはasync関数として定義でき、その中でawaitを使ってデータフェッチを待機できます。

// app/blog/page.tsx
import type { Post } from '@/lib/posts';

export default async function Page() {
  // Server Componentで直接データをフェッチ
  // 'https://api.vercel.app/blog' はダミーURLとしています
  const res = await fetch('https://api.vercel.app/blog', { cache: 'no-store' }); // 動的なデータ取得のためキャッシュ無効化
  if (!res.ok) {
    // エラーハンドリングの例
    throw new Error('Failed to fetch posts');
  }
  const posts: Post[] = await res.json();

  return (
    <section>
      <h1>ブログ記事一覧 (Server Component)</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </section>
  );
}

fetchオプションの{ cache: 'no-store' }は、データが頻繁に更新される場合にキャッシュを無効化するためのものです。静的なデータや頻繁に変わらないデータであれば、デフォルトのキャッシュ戦略を利用することで、再ビルド時のデータ取得を最適化できます。

ポイント:

  • データベースクライアントやORMを直接使用することも可能です。
  • cookies()headers()といったNext.js独自のAPIもServer Components内で利用できます。
  • loading.js<Suspense>を使って、データフェッチ中のローディングUIを表示し、ストリーミングレンダリングを実現できます。
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="flex justify-center items-center h-screen">
      <p className="text-xl animate-pulse">Loading blog posts...</p>
    </div>
  );
}

このloading.tsxファイルがapp/blogディレクトリに存在する場合、app/blog/page.tsxのデータフェッチ中に自動的に表示されます。

Server ComponentsとClient Componentsの組み合わせ

Server Componentsで取得したデータを、インタラクティブなClient Componentsに渡すことで、両者の利点を最大限に引き出せます。このとき、データをpropsとして渡すのが一般的なパターンです。

// lib/posts.ts (データ取得のモックアップ)
export type Post = {
  id: number;
  title: string;
  content: string;
};

export async function getPosts(): Promise<Post[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, title: 'Next.js Server Components入門', content: '...' },
        { id: 2, title: '状態管理の新しいアプローチ', content: '...' },
        { id: 3, title: 'データフェッチの最適化', content: '...' },
      ]);
    }, 500);
  });
}

// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/posts';
import { PostSearch } from './post-search'; // Client Componentをインポート

export default async function PostsPage() {
  const posts = await getPosts(); // サーバーでデータを取得

  return (
    <section>
      <h1>記事一覧 (Server Component)</h1>
      {/* 取得結果をpropsでClient Componentに渡す */}
      <PostSearch posts={posts} />
    </section>
  );
}

// app/posts/post-search.tsx (Client Component)
'use client';

import { useState } from 'react';
import type { Post } from '@/lib/posts'; // 型定義をインポート

export function PostSearch({ posts }: { posts: Post[] }) {
  const [q, setQ] = useState('');
  const filtered = posts.filter((p) => p.title.toLowerCase().includes(q.toLowerCase()));

  return (
    <div>
      <input
        type="text"
        value={q}
        onChange={(e) => setQ(e.target.value)}
        placeholder="記事を検索..."
        className="border p-2 rounded"
      />
      <ul className="mt-4">
        {filtered.length > 0 ? (
          filtered.map((post) => (
            <li key={post.id} className="p-2 border-b">
              {post.title}
            </li>
          ))
        ) : (
          <p>該当する記事はありません</p>
        )}
      </ul>
    </div>
  );
}

この例では、PostsPage (Server Component)でデータを取得し、その結果をPostSearch (Client Component)にpropsとして渡しています。PostSearchは受け取ったデータを使って、クライアントサイドで検索機能を実装しています。

Client Componentsでのデータ取得(補足)

Client Componentsでデータをフェッチする必要がある場合(例: ユーザー操作に応じた動的なデータ更新、リアルタイム更新など)は、Reactのuseフック(React 18以降)やSWR、React Queryといったクライアントサイドのデータフェッチライブラリを使用します。

ただし、機密性の高いデータやAPIキーをClient Componentで直接扱うのは避けるべきです。 そのような場合は、後述のServer ActionsやAPI Routesを介してサーバーサイドで処理し、その結果をClient Componentで受け取る形が安全です。

Next.js App Routerでの状態管理

Server ComponentsがデフォルトとなるNext.js App Routerでは、従来のClient Components中心のアプリケーションとは異なる状態管理の考え方が求められます。

ローカルステートの管理

インタラクティブなUIを構築するために、Client Components内でuseStateuseReducerを使ってローカルステートを管理します。

// app/ui/LikeButton.tsx
'use client';

import { useState } from 'react';

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button onClick={() => setLikes(likes + 1)}>
      いいね {likes}
    </button>
  );
}

このLikeButtonは、Server Componentからインポートして使用できます。Server Componentはサーバーでレンダリングされ、その中でLikeButtonがプレースホルダーとして組み込まれ、クライアント側でハイドレーション(JavaScriptによるインタラクティブ化)されます。

グローバルステートの管理とClient Boundary

複数のClient Components間で状態を共有する必要がある場合、React Context APIやZustand、Jotaiなどの状態管理ライブラリが選択肢となります。

しかし、グローバルステートを導入する際は、Client Boundaryの拡大に注意が必要です。 グローバルステートを提供するContext Providerは"use client"を持つClient Componentである必要があり、そのProviderでラップされたすべての子コンポーネントは、たとえServer Componentとして記述されていても、クライアントバンドルに含まれることになります。

ベストプラクティス: グローバルステートのProviderはアプリケーションのルートに近い、しかし必要最小限の範囲でClient Componentとして定義し、その内部にServer Componentをchildrenとして渡す「Compositionパターン」を活用します。

// app/providers/ThemeProvider.tsx
'use client';

import { createContext, useContext, useState, ReactNode } from 'react';

type Theme = 'light' | 'dark';
type ThemeContextType = {
  theme: Theme;
  toggleTheme: () => void;
};

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// app/layout.tsx (Server Component)
import { ThemeProvider } from '@/app/providers/ThemeProvider'; // Client Component

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        {/* Client ComponentのThemeProviderでラップ */}
        <ThemeProvider>
          {children} {/* childrenにはServer Componentsが含まれる */}
        </ThemeProvider>
      </body>
    </html>
  );
}

// app/ui/ThemeSwitcher.tsx (Client Component)
'use client';

import { useTheme } from '@/app/providers/ThemeProvider';

export default function ThemeSwitcher() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme} className="p-2 border rounded">
      テーマを切り替え: {theme === 'light' ? '🌙' : '☀️'}
    </button>
  );
}

この構成では、ThemeProvider自体はクライアントサイドで実行されますが、childrenとして渡されるコンポーネント(多くはServer Components)は可能な限りサーバーでレンダリングされます。これにより、必要な部分にのみクライアントサイドのJavaScriptを適用し、バンドルサイズを最適化できます。

Server Actionsの活用:データ変更とUI更新の最適化

Server Actionsは、Next.js 13.4で導入された強力な機能で、クライアントから直接サーバーサイドの関数を呼び出し、データ変更やキャッシュの再検証、UI更新を効率的に行うことができます。これにより、API Routesを別途定義することなく、サーバーサイドのロジックを実行できます。

Server Actionsの基本

Server Actionsは、ファイルの先頭に"use server"ディレクティブを記述することで定義します。

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function addPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // データベースに保存する処理(例)
  console.log('Saving post:', { title, content });
  // await db.posts.create({ data: { title, content } }); // 実際のDB操作

  // キャッシュを再検証してUIを更新
  revalidatePath('/posts'); // /posts ページのキャッシュを無効化し、次回リクエスト時に再フェッチさせる
  redirect('/posts'); // 投稿後に記事一覧ページにリダイレクト
}

Server Actionsは、<form>要素のactionプロパティに直接渡したり、クライアントサイドからstartTransitionuseTransitionフックを使って呼び出したりできます。

// app/posts/create/page.tsx (Server ComponentからServer Actionを呼び出す例)
import { addPost } from '@/app/actions';

export default function CreatePostPage() {
  return (
    <form action={addPost} className="p-4 border rounded">
      <h2 className="text-2xl mb-4">新しい記事を作成</h2>
      <div className="mb-2">
        <label htmlFor="title" className="block text-sm font-bold mb-1">タイトル</label>
        <input type="text" id="title" name="title" required className="border p-2 w-full" />
      </div>
      <div className="mb-4">
        <label htmlFor="content" className="block text-sm font-bold mb-1">内容</label>
        <textarea id="content" name="content" rows={5} required className="border p-2 w-full"></textarea>
      </div>
      <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
        記事を投稿
      </button>
    </form>
  );
}

この例では、フォームが送信されると、ブラウザはaddPost Server Actionを呼び出します。Server Actionはデータベース操作を実行し、revalidatePathで関連ページのキャッシュをクリアし、redirectでユーザーを別のページに遷移させます。これにより、APIレイヤーを介さずに、効率的なデータ変更とUI更新が実現できます。

Server Actionsの利点

  • API Routes不要: フォーム送信などのデータ変更処理のためにAPI Routesを定義する必要がなくなります。
  • パフォーマンス: クライアント・サーバー間のネットワーク往復を最小限に抑え、効率的なデータ変更とUI更新を実現します。
  • セキュリティ: サーバーサイドで実行されるため、機密情報を安全に扱えます。
  • キャッシュの自動再検証: revalidatePathrevalidateTagを使って、関連するデータのキャッシュを簡単に再検証できます。

Next.js SC/CC移行でハマる!よくあるエラーと解決策

Server ComponentsとClient Componentsの混在環境は強力ですが、特有の落とし穴もあります。ここでは、Next.jsのApp Router移行時によく遭遇するエラーとその解決策をまとめます。

1. "use client" の記述忘れによるReact Hooksの使用エラー

問題: Server Component内でuseStateuseEffectuseContextなどのReact Hooksや、windowlocalStorageなどのブラウザAPIを使用しようとすると、ビルド時または実行時にエラーが発生します。

// app/components/MyComponent.tsx (❌エラーになる例)
import { useState } from 'react'; // Server ComponentでHooksをインポート

export default function MyComponent() {
  const [count, setCount] = useState(0); // ❌ エラー: Hooks can only be called inside of the body of a client component.
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

回避策: インタラクティブな機能や状態管理、ブラウザAPIへのアクセスが必要なコンポーネントは、ファイルの先頭に"use client"ディレクティブを明示的に記述してClient Componentにする必要があります。

// app/components/MyComponent.tsx (✅正しい例)
'use client'; // これをファイルの先頭に記述

import { useState } from 'react';

export default function MyComponent() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

2. Client Component内での直接的な機密データフェッチ

問題: Client Component内で直接データベースアクセスやAPIキーを含む外部APIへのデータフェッチを行うと、APIキーがクライアントサイドに露出するセキュリティリスクや、クライアント・サーバー間の通信回数増加によるパフォーマンスの問題が発生します。

// app/components/SensitiveDataFetcher.tsx (❌危険な例)
'use client';

import { useEffect, useState } from 'react';

export default function SensitiveDataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // ❌ 開発環境では警告、本番環境ではセキュリティリスク
    // データベースクライアントや外部APIキーを含むfetchは避けるべき
    fetch(`https://api.example.com/data?apiKey=${process.env.NEXT_PUBLIC_API_KEY}`)
      .then(res => res.json())
      .then(setData);
  }, []);

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

回避策: データフェッチは可能な限りServer Componentで行い、取得したデータをpropsとしてClient Componentに渡すのがベストプラクティスです。機密情報はサーバー側で安全に処理できます。ユーザー操作に基づいて動的にデータをフェッチする必要がある場合は、Server ActionsやAPI Routesを介してサーバーサイドで処理します。

3. Client Boundaryの意図しない拡大

問題: Client Componentがインポートする子コンポーネントは、明示的に"use client"がなくても自動的にClient Componentとして扱われます。これにより、必要以上に多くのコンポーネントがクライアントバンドルに含まれ、バンドルサイズの増加や初期ロードの遅延につながる可能性があります。

// app/ui/ClientLayout.tsx
'use client';
import ServerSidebar from './ServerSidebar'; // Server Componentをインポート

export default function ClientLayout({ children }: { children: React.ReactNode }) {
  // ClientLayoutがClient Componentなので、ServerSidebarも自動的にClient Boundary内に含まれる
  return (
    <div>
      <ServerSidebar /> {/* ServerSidebarもクライアントバンドルに含まれてしまう */}
      <main>{children}</main>
    </div>
  );
}

// app/ui/ServerSidebar.tsx (Server Componentとして意図されている)
export default function ServerSidebar() {
  // サーバーサイドでのみ実行したいロジック
  return <aside>サーバーサイドのサイドバー</aside>;
}

回避策: Compositionパターンを活用し、Server Componentでレンダリングできる部分はServer Componentとして残し、Client ComponentにはchildrenプロップとしてServer Componentを渡すことで、クライアントバンドルに含めるJavaScriptの量を最小限に抑えることができます。

// app/ui/ClientWrapper.tsx (Client Component)
'use client';
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
  // クライアントサイドのロジック
  return <div className="border p-4">{children}</div>;
}

// app/page.tsx (Server Component)
import ClientWrapper from '@/app/ui/ClientWrapper';
import ServerContent from '@/app/ui/ServerContent'; // Server Component

export default function HomePage() {
  return (
    <ClientWrapper>
      {/* ClientWrapperの子要素としてServer Componentを渡す */}
      <ServerContent />
      <p>これはClient Componentの内部ですがServerContentはサーバーでレンダリングされます</p>
    </ClientWrapper>
  );
}

// app/ui/ServerContent.tsx (Server Component)
export default function ServerContent() {
  return <p className="font-bold">私はサーバーでレンダリングされました</p>;
}

このパターンでは、ServerContentClientWrapperchildrenとして渡されるため、ClientWrapperがクライアントでハイドレーションされる際に、ServerContentは既にサーバーでレンダリングされたHTMLとして存在し、クライアントバンドルには含まれません。

4. Server ComponentでのCookie変更操作の制限

問題: Server ComponentからはCookieの変更操作(cookies().set()cookies().delete())を直接呼び出すことができません。これらの操作は副作用を伴うため、サーバーサイドで安全に処理される必要があります。

// app/page.tsx (❌エラーになる例)
import { cookies } from 'next/headers';

export default function HomePage() {
  // ❌ エラー: cookies().set() can only be called in a Server Action or Route Handler.
  cookies().set('my-cookie', 'value');
  return <h1>Home</h1>;
}

回避策: Cookie操作やAPIに対するデータ変更リクエストなどの変更操作は、Server Actionsで行うことが推奨されます。Server Actionsはサーバーサイドで実行され、安全にCookieを操作できます。

// app/actions.ts
'use server';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';

export async function setMyCookie(formData: FormData) {
  const value = formData.get('cookieValue') as string;
  cookies().set('my-cookie', value);
  revalidatePath('/'); // 必要に応じてパスを再検証
  return { success: true };
}

// app/page.tsx (✅正しい例)
import { cookies } from 'next/headers';
import { setMyCookie } from '@/app/actions';

export default function HomePage() {
  const myCookie = cookies().get('my-cookie')?.value || 'なし';
  return (
    <div>
      <h1>Home</h1>
      <p>現在のCookie: {myCookie}</p>
      <form action={setMyCookie}>
        <input type="text" name="cookieValue" defaultValue="new-value" />
        <button type="submit">Cookieをセット</button>
      </form>
    </div>
  );
}

5. Server Componentでのエラー詳細の不足

問題: Server Componentで発生したエラーの詳細がクライアント側に伝わりにくく、デバッグが困難になることがあります。ユーザーには一般的なエラーメッセージしか表示されない場合があります。

回避策: error.tsxファイルを使用してエラーハンドリングを行うことができます。error.tsxはClient Componentである必要があり、reset()プロップを受け取って再レンダリングを試みることができます。これにより、ユーザーフレンドリーなエラーUIを提供し、開発環境ではデバッグ情報を表示できます。

// app/blog/error.tsx (Client Component)
'use client'; // error.tsxはClient Componentである必要があります

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // エラーログサービスなどにエラーを送信
    console.error(error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <h2 className="text-2xl text-red-600 mb-4">
        ブログ記事の読み込み中にエラーが発生しました
      </h2>
      <p className="text-gray-700 mb-4">
        {process.env.NODE_ENV === 'development' && `詳細: ${error.message}`}
      </p>
      <button
        onClick={
          // セグメントを再レンダリングする試み
          () => reset()
        }
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
      >
        再試行
      </button>
    </div>
  );
}

このerror.tsxapp/blogディレクトリに存在する場合、app/blog/page.tsxでエラーが発生すると、このエラーUIが表示されます。

設計上のトレードオフとベストプラクティス

Next.jsのServer ComponentsとClient Componentsを効果的に活用するためには、設計上のトレードオフを理解し、ベストプラクティスに従うことが重要です。

トレードオフ

  • パフォーマンス vs インタラクティブ性: Server Componentsは初期ロード速度の向上、バンドルサイズの削減に貢献しますが、インタラクティブなUIにはClient Componentsが必要です。Client Componentsを多用しすぎると、初期レンダリングの遅延やメモリ・CPU負荷の増加につながる可能性があります。
  • シンプルさ vs 最適化: すべてをClient Componentsにすることも可能ですが、Next.jsがServer Componentsをデフォルトとしているのは、パフォーマンスやセキュリティ上のメリットがあるためです。必要な部分にのみClient Componentsを使用することで、これらのメリットを享受できますが、SC/CCの境界を意識した設計は、初期の学習コストや開発の複雑さを増す可能性があります。
  • データ取得の場所: サーバー側でデータを取得すると、データベースやAPIキーへの直接アクセスが可能になり、セキュリティが向上し、クライアント・サーバー間の通信回数を減らせます。しかし、ユーザー操作に基づいて動的にデータをフェッチする必要がある場合や、クライアントサイドでのリアルタイム更新が必要な場合は、Server ActionsやClient Componentsでのデータ取得も検討する必要があります。

ベストプラクティス

  1. デフォルトはServer Components: Next.js App Routerでは、特別な指定がなければすべてのコンポーネントがServer Componentsとして扱われます。基本的にはServer Componentsを積極的に使用し、インタラクティブな機能やブラウザAPIへのアクセスが必要な場合にのみClient Componentsに切り替えるのが原則です。
  2. Client Componentsは「境界」と考える: Client Componentsは、インタラクティブな機能やブラウザAPIを使用するための「境界」として最小限に留めるべきです。データ取得や重い依存関係は可能な限りServer Componentsに残し、Client Componentsにはchildrenやシリアライズ可能なpropsとして結果だけを渡します。
  3. データ取得はServer Componentsで: データベースや外部APIからのデータフェッチは、Server Componentsで行うのが最も推奨される方法です。これにより、機密情報の安全な取り扱い、クライアントへのJavaScript送信量の削減、初期表示速度の向上が期待できます。
  4. 状態管理は必要最小限のスコープで: グローバルステートは極力使用せず、状態の保持は必要最小スコープに置くことが重要です。これにより、無駄な再レンダリングを抑え、局所性とテスト容易性を高めます。Client Components間で状態を共有する必要がある場合は、Context APIや状態管理ライブラリ(Zustand, Jotaiなど)を検討しますが、その場合もClient Boundaryの拡大に注意が必要です。
  5. Compositionパターンを活用する: Server Componentの中にClient Componentをネストしたり、Client ComponentのchildrenプロップとしてServer Componentを渡したりすることで、両者の利点を組み合わせた柔軟なUIを構築できます。これにより、クライアントバンドルサイズを最小限に保ちつつ、必要なインタラクティブ性を提供できます。
  6. Server Actionsの活用: ユーザー操作に基づくデータ変更やフォーム送信など、サーバーサイドでのデータ操作が必要な場合はServer Actionsを積極的に利用します。これにより、API Routesを別途作成する手間を省き、効率的なデータ操作が可能です。
  7. エラーハンドリングの考慮: Server ComponentsやServer Actionsは外部データ操作を伴うため、エラーハンドリングが重要です。error.tsxnotFound.tsxを活用し、適切なエラー表示と回復処理を実装します。

まとめ

Next.jsのServer ComponentsとClient Componentsは、Webアプリケーション開発に新たなパラダイムをもたらしました。この新しいモデルを理解し、適切に使いこなすことで、パフォーマンス、セキュリティ、開発体験が大幅に向上します。

この記事では、Next.jsのServer ComponentsとClient Componentsの基本的な概念、データ取得、状態管理、そして移行時によく遭遇する具体的なハマりどころと解決策を解説しました。

重要なポイントは以下の通りです。

  • デフォルトはServer Componentsであり、データ取得は可能な限りサーバーで行う。
  • Client Componentsはインタラクティブな機能に限定し、"use client"を明示する。
  • useStateなどのHooksはClient Componentsでのみ使用可能
  • Server Actionsを活用し、効率的なデータ変更とUI更新を実現する。
  • Compositionパターンを使い、Client Boundaryの拡大を防ぎ、バンドルサイズを最適化する。
  • エラーハンドリングを適切に行い、ユーザー体験とデバッグを容易にする。

これらの知識とベストプラクティスを活かし、あなたのNext.jsアプリケーションをさらに進化させていきましょう。公式ドキュメントも併せて確認し、最新の情報をキャッチアップすることをおすすめします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?