Blog
Next.js解説!構築の際に押さえておきたい開発ヒント
Next.jsの特徴
Next.jsは、Reactをベースにしたフレームワークで、多くの開発者に支持されています。
その最大の特徴は、静的サイト生成(SSG)とサーバーサイドレンダリング(SSR)の両方を簡単に実現できる点です。
これにより、SEO対策が容易になり、高速なユーザー体験を提供することが可能です。
さらに、Next.jsは自動的なコード分割、画像の最適化、APIルートの提供など、開発を効率化する機能が豊富に揃っています。
また、Reactのエコシステムと完全に互換性があるため、既存のReactコンポーネントやライブラリをそのまま利用できるのも魅力です。
また最新バージョンでは、以下のような新機能や改善点が追加されています。
- AppRouter
アプリケーション全体のルーティングが容易に。 - Server Actions
サーバーサイドでのアクションを簡単に実行。 - サーバーコンポーネントとクライアントコンポーネントの明確な分離
効率的なレンダリングが可能に。
AppRouterに関して
AppRouterはNext.jsの新しいルーティング機能で、ツリー構造を利用してルートの管理を簡単に行うことができます。
以下のようなディレクトリ構造を想像してください。
app
├── page.tsx
├── about
│ └── page.tsx
└── blog
└── [id]
└── page.tsx
このように、各ディレクトリにpage.tsx
ファイルを配置することで、自動的にルートが生成されます。
例えば、/about
にアクセスすると/app/about/page.tsx
が表示され、/blog/[id]
にアクセスすると動的ルートが処理されます。
( )を使ったURLには影響しない構造
Next.jsでは、括弧 ( )
を使ってルートごとに異なるレイアウトを定義することができます。
例えば、以下のように書くことで、特定のページやセクションに異なるテンプレートやレイアウトを適用できます。
app
├── (main)
│ └── page.tsx
└── (dashboard)
└── dashboard
└── page.tsx
この設定により、/
や/about
はメインレイアウトを使用し、/dashboard
はダッシュボード専用のレイアウトを使用するように分けることができます。
// app/(main)/page.tsx
export default function MainPage() {
return (
<div>
<h1>Main Layout Page</h1>
<p>This page uses the main layout.</p>
</div>
);
}
// app/(dashboard)/dashboard/page.tsx
export default function DashboardPage() {
return (
<div>
<h1>Dashboard Layout Page</h1>
<p>This page uses the dashboard layout.</p>
</div>
);
}
[…XXXX]をつかった複数階層の動的パス
スプレッドを使用すると、可変長のURLセグメントをキャプチャすることができます。
これにより、任意の深さのパスを処理できます。
app
└── docs
└── [...slug]
└── page.tsx
たとえばこの設定では、/docs/guide/getting-started
のようなURLにアクセスすると、/app/docs/[...slug]/page.tsx
がレンダリングされます。
// app/docs/[...slug]/page.tsx
import { useRouter } from 'next/router';
const DocsPage = () => {
const router = useRouter();
const { slug } = router.query;
return (
<div>
<h1>Documentation: {slug?.join('/')}</h1>
</div>
);
};
export default DocsPage;
Server Actionsに関して
Server Actionsは、サーバーサイドで実行される非同期関数で、フォームの送信やデータの変更を処理するために使用されます。
サーバーアクションを宣言するには、関数の文頭に'use server'
ディレクティブを追加します。
次のようなシンプルな例を見てみましょう。
サーバーコンポーネントでの使用例
サーバーコンポーネントでは、関数内に'use server'
を追加してサーバーアクションを定義します。
export default function Page() {
// サーバーアクションの定義
async function createInvoice(formData: FormData) {
'use server'
const data = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status')
};
// データの処理やキャッシュのリバリデーション
console.log(data);
}
return (
<form action={createInvoice}>
<input type="text" name="customerId" placeholder="Customer ID" />
<input type="number" name="amount" placeholder="Amount" />
<input type="text" name="status" placeholder="Status" />
<button type="submit">Create Invoice</button>
</form>
);
}
クライアントコンポーネントでの使用例
クライアントコンポーネントでは、モジュールレベルで'use server'
を使用してサーバーアクションを定義し、それをインポートして使用します。
'use server'
export async function createInvoice(formData: FormData) {
const data = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status')
};
// データの処理やキャッシュのリバリデーション
console.log(data);
}
'use client'
import { createInvoice } from '@/app/actions';
export function InvoiceForm() {
return (
<form action={createInvoice}>
<input type="text" name="customerId" placeholder="Customer ID" />
<input type="number" name="amount" placeholder="Amount" />
<input type="text" name="status" placeholder="Status" />
<button type="submit">Create Invoice</button>
</form>
);
}
サーバーアクションを使用することで、Next.jsのキャッシュとリバリデーションアーキテクチャと統合し、効率的なデータ処理を実現できます。
Next.jsで構築する際に気をつけること
Next.jsでの開発は非常に便利ですが、いくつかの注意点もあります。
以下に、構築時に気をつけるべきポイントをまとめました。
サーバーコンポーネントとクライアントコンポーネントの違い
Next.jsでは、サーバーコンポーネントとクライアントコンポーネントの2つのコンポーネントが存在します。
これらの違いを理解し、適切に使い分けることが重要です。
- サーバーコンポーネント
サーバーサイドでレンダリングされるコンポーネント。これにより、初期ロードが速く、SEOに優れたページを作成できます。ただし、ブラウザ側でのインタラクティブな操作はできません。 - クライアントコンポーネント
クライアントサイドでレンダリングされるコンポーネント。ブラウザ上でのインタラクションが必要な場合に使用しますが、初期ロードが遅くなる可能性があります。
適切なコンポーネントの選択は、パフォーマンスとユーザーエクスペリエンスの両方に影響を与えるため、慎重に行いましょう。
サーバーコンポーネントのソースコード上の注意点
Server Componentsは、サーバーサイドでレンダリングされるコンポーネントです。
これにより、初回ロード時のパフォーマンスが向上し、SEO対策にも有効です。
ただし、以下の点に注意が必要です。
- クライアントサイドの状態管理ライブラリ(例:ReduxやMobX)を直接使用しない。
- ブラウザ専用のAPI(例:
window
やdocument
)を使用しない。
export default function ServerComponent() {
const data = await fetchData();
return (
<div>
<h1>Server Side Rendered Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
クライアントコンポーネントのソースコード上の注意点
クライアントコンポーネントは、クライアントサイドで実行されるコンポーネントです。
これにより、インタラクティブなUIを提供することができます。
注意点としては、以下の点が挙げられます。
- 必要以上に大きなデータを扱わない。
- 初回レンダリング時のパフォーマンスに配慮する。
'use client'
import { useState } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Client Side Counter</h1>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
);
}
まだまだISRがきちんと動作しないサーバーもある
ISR(Incremental Static Regeneration)は、Next.jsの重要な機能で、静的生成されたページを動的に再生成することができます。
しかし、すべてのサーバーがこの機能に完全に対応しているわけではありません。
以下の点に注意が必要です。
- サーバー環境の確認
ISRを利用するためには、サーバーがこの機能に対応しているかを事前に確認することが重要です。特に、古いサーバー環境や一部のホスティングサービスではISRがうまく動作しない場合があります。 - キャッシュの管理
ISRを使用する際には、キャッシュの設定が重要です。正しく設定しないと、期待通りにページが再生成されなかったり、古いコンテンツが表示されたりすることがあります。 - パフォーマンスの考慮
ISRは便利な機能ですが、頻繁にページを再生成する必要がある場合、サーバーの負荷が増加する可能性があります。そのため、更新頻度や再生成のトリガーとなる条件を慎重に設計することが求められます。
ISRを適切に活用することで、ユーザーに最新の情報を提供しつつ、高速なパフォーマンスを維持することができます。
ただし、実装前にしっかりと環境のチェックと設計を行うことが成功の鍵となります。
実際にサーバーコンポーネントとクライアントコンポーネントを併用したサンプルを作成
実際にサーバーコンポーネントとクライアントコンポーネントを併用して、動作や特徴を確認するために絞り込み検索を作成してみました。
構造を図にするとこのようになります。
実際のプロジェクトのソースコードに近い簡略化したサンプルのコードです。
構造はこのようにしました。
app
├── components
│ └── SearchBar.tsx
├── hooks
│ └── useSearch.tsx
└── page.tsx
app/components/SearchBar.tsx
// app/components/SearchBar.tsx
'use client';
import { useSearch } from '@/app/hooks/useSearch';
export default function SearchBar({ initialQuery }) {
const { query, setQuery } = useSearch(initialQuery);
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search items..."
/>
);
}
app/hooks/useSearch.tsx
// app/hooks/useSearch.tsx
import { useState, useEffect } from 'react';
export function useSearch(initialQuery) {
const [query, setQuery] = useState(initialQuery);
useEffect(() => {
const params = new URLSearchParams();
if (query) params.set('query', query);
history.replaceState(null, '', `?${params.toString()}`);
}, [query]);
return { query, setQuery };
}
app/page.tsx
// app/page.tsx
import SearchBar from './components/SearchBar';
// Mock Data
const items = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' },
{ id: 4, name: 'Date' },
{ id: 5, name: 'Elderberry' }
];
export default function Page({ searchParams }) {
const query = searchParams.get('query') || '';
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<h1>Item List</h1>
<SearchBar initialQuery={query} />
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
このサンプルコードでは、サーバーコンポーネントとしてのページ側で検索パラメータを受け取り、検索部分をクライアントコンポーネントとして作成し、カスタムフックを使用して検索機能を実装しています。
これにより、サーバーサイドレンダリングとクライアントサイドのそれぞれの機能と特徴を生かしつつ両立させることができます。