1
1

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】実装ワンポイント

Last updated at Posted at 2025-04-26

概要

この記事では、Next.jsを使った開発において、つい見落としがちだけど、意識したいポイントについて簡単にまとめました。

目次

設計観点

データフェッチ観点

コンポーネント実装観点

テスト観点

本文

SSR / ISR / SSG を選択する

Next.jsでは、ページの生成方法として主に3つの戦略があります:サーバーサイドレンダリング(SSR)、インクリメンタル静的再生成(ISR)、静的サイト生成(SSG)です。

ページの要件に応じて、最適な方法を選択しましょう。

方式 パフォーマンス 更新頻度 推奨シーン
SSG 静的コンテンツ
ISR 定期的に更新されるコンテンツ
SSR 動的で常に変化するコンテンツ

指定方法

page.tsx で指定します。

// SSR
export const dynamic = 'force-dynamic'

// ISR
export const revalidate = 10; 

// SSG
export const dynamic = 'force-static'

コンポーネントツリーの末端をClient Componentにする

コンポーネントツリーの末端を Client Components にすることで、Client Boundary を下層に限定することができます。

例えば検索バーを持つヘッダーを実装する際に、ヘッダーごと Client Components にするのではなく検索バーの部分だけClient Components として切り出し、ヘッダー自体は Server Components に保つことを検討しましょう。

image.png

Compositionパターンを活用する

上記の方法はシンプルな解決策ですが、どうしても上位層のコンポーネントを Client Components にする必要がある場合もあります。その際には Composition パターンを活用して、Client Components を分離することが有効です。

Client ComponentsServer Components をimportすることができませんが、コンポーネントツリーとしてはClient ComponentsのchildrenなどのpropsにServer Componentsを渡すことで、レンダリングが可能です。

🔍 参考:

コンポーネントの設計に迷ったらサードパーティのコンポーネントの実装を見る

コンポーネントの設計に迷った場合は、よく使われているサードパーティのコンポーネントライブラリの実装を参考にすることが効果的です。

  • Radix UI: アクセシビリティに配慮した低レベルコンポーネント
  • Chakra UI: 柔軟なカスタマイズが可能なコンポーネント
  • shadcn/ui: スタイルとロジックが分離されたヘッドレスUIコンポーネント

layout.tsx で 認可をしない

特定パス配下の認可を実装する場合、layout ではなく middleware で行いましょう。

App Routerにおいてページとレイアウトは並行にレンダリングされるため、必ずしもレイアウト層に実装した認可チェックがページより先に実行されるとは限りません。

基本的に認可処理はmiddleware で行うようにしましょう。

Server Componentsでデータをfetchする

基本的にデータの取得は 「Server Components」 で行うようにしましょう。

Server Components でリクエストを行うことのメリット

  • Next.js サーバーと API サーバーは高速で安定しているため、リクエストが高速処理される
  • API のエンドポイントをパブリックなネットワークに公開せずに済む

データフェッチ層はserver-onlyで保護する

Next.js 14以降では、server-onlyパッケージを使用して、データフェッチ層のモジュールを Client Components に誤ってインポートした場合にビルド時エラーを発生させることができます。

データフェッチ層を誤ってクライアントサイドで利用することを防ぐためにも、server-only パッケージを利用しましょう。

import 'server-only'

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY, 
    },
  })

  return res.json()
}

Request Memoizationの活用

App Router ではデータフェッチをコロケーションすることが推奨されています。
しかし末端のコンポーネントでデータフェッチを行うと、ページ全体を通して重複するリクエストが発生する可能性が高まります。

App Routerはこれに対処するため、レンダリング中の同一リクエストをメモ化し排除するRequest Memoizationを実装しています。

image.png

設計・プラクティス

Request Memoization がリクエストを重複と判定するには、同一URL・同一オプションの指定が必要で、オプションが1つでも異なれば別リクエストが発生します。

オプションの指定ミスにより Request Memoization が効かないことを防ぐため、複数のコンポーネントで利用するデータフェッチ処理は分離しましょう。

export async function getProduct(id: string) {
  const res = await fetch(`https://dummyjson.com/products/${id}`);
  return res.json();
}

バージョン変更を考慮してfetchの第二引数は必ず指定する

Next.jsの以前のバージョンでは、fetchを使用するとデフォルトのキャッシュ値がforce-cacheになっていました。これはバージョン15で変更され、デフォルトはcache: no-storeになりました。

つまり、Next.js 15からはデフォルトでキャッシュは使用されなくなりました。明示的にキャッシュを利用したい場合はcache: 'force-cache'を指定する必要があります。

今後、バージョンアップデートによる動作変更の影響を受けないようにするため、基本的に引数は明示的に指定しておくようにしましょう。

await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

fetchが利用できない場合はReact.cacheでキャッシュ戦略を最適化する

GraphQL を使用しているなど、拡張された fetch を使用できない場合や、より細かいキャッシュ制御が必要な場合、React.cacheを利用しましょう。

React.cache

React.cacheは、関数の結果をメモ化するためのユーティリティです。
同じ引数で呼び出された場合、計算を再実行せずにキャッシュされた値を返します。

GraphQL(ApolloClient) での実装例

GraphQLを使用する場合、Apollo Clientのclient.queryをReact.cacheでラップすることで、リクエストをキャッシュ化できます。

import { apolloClient, GetPostDocument } from "@/app/graphql";
import { cache } from "react";

export const getPost = cache(async (id: string) => {
  const { data } = await apolloClient.query({
    query: GetPostDocument,
    variables: { id }
  });
  return { json: () => Promise.resolve(data.post) };
});

画像にnext/imageを使用する

Next.jsのプロジェクトでは、imgタグではなく、next/imageを使用することが推奨されています。

Imageタグの機能

  • 画面サイズに応じて画像を最適化(webp, avif などに自動変換)
  • 画像の表示領域を画像がダウンロードされる前から確保し Cumulative Layout Shift (CLS) を防止
  • 遅延読み込み
import Image from "next/image";

<Image
  src={dogImage}
  alt="dogImage"
  width={500}
  height={500}
/>

next/routerではなくnext/navigationからインポートする

Next.js 13以降のApp Router環境では、next/navigationモジュールからルーティング関連の機能をインポートする必要があります。

// ❌
import { useRouter } from 'next/router';

// ⭕️
import { useRouter } from "next/navigation";

これは、App Router向けの新しいナビゲーションAPIが提供されているためです。
間違ったモジュールからインポートすると、機能しないだけでなく、実行時エラーの原因になります。

メソッド戻り値の指定が不足していないか確認する

TypeScript に関連する話題ですが、メソッド戻り値の型指定が必要であるか必ず確認しましょう。
型推論では目的の型と認識されない場合は、明示的に型指定することを忘れないようにしましょう。

// ❌ 型が不明確
async function getTodoList() {
  const response = await fetch(`${apiEndpoint}/todos`);
  if (!response.ok) throw new Error('Failed to fetch');
  return await response.json();
}
// => (local function) getTodoList(): Promise<any>

// ✅ 型が明確
async function getTodoList(): Promise<Todo[]> {
  const response = await fetch(`${apiEndpoint}/todos`);
  if (!response.ok) throw new Error('Failed to fetch');
  return await response.json();
}
// => (local function) getTodoList(): Promise<Todo[]>

重いコンポーネントのレンダリングは Streaming SSR を活用する

Dynamic Renderingを使用する場合でも、特に重いコンポーネントのレンダリングは<Suspense>で遅延させ、Streaming SSRを活用しましょう。

Suspenseによる非同期処理の分離

Suspenseは、データ取得やコンポーネントの読み込みなどの非同期処理を、レンダリングプロセスから分離します。これにより、重いコンポーネントの処理を待たずに、ページの他の部分を先に表示することができます。

import { Suspense } from 'react';
import MyAsyncComponent from './MyAsyncComponent';

export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MyAsyncComponent />
    </Suspense>
  );
}

Streaming SSRのメリット

Server Components と組み合わせることで、HTMLを段階的にストリーミングできます。全てのデータが揃うまで白い画面を表示するよりも、できた部分から表示してユーザーにコンテンツを見せることができます。

App Router では Streaming SSR をネイティブにサポートしており、重いデータフェッチを伴う Server Components のレンダリングを遅延させ、ユーザーにいち早くレスポンスを返し始めることができます。

デメリットと考慮点

画面の一部にfallbackを表示しそれが後に置き換えられるため、いわゆるLayout Shiftが発生する可能性があります。特にユーザーインターフェースの重要な部分では、適切なサイズのスケルトンUIやプレースホルダーを用意することでこの問題を軽減できます。

キャッシュ済みのページやデータを更新するにはrevalidatePathとrevalidateTagを使用する

SSGISR で生成されたページやデータをオンデマンドで更新したい場合は、revalidatePathrevalidateTag関数を使用します。

  • revalidatePath: 特定のパスに関連するキャッシュを無効化
  • revalidateTag: 特定のキャッシュタグに関連するデータを無効化

これらを使うと、新しいコンテンツ作成後や更新後に、関連するキャッシュを即時に更新できます。

export async function createPost(id: string) {
  // Postの作成

  revalidatePath('/posts')
  redirect(`/post/${id}`)
}

DOM取得は getByRole を優先する

React Testing Library を使用したテストでは、DOM要素の取得方法には優先順位があります。この優先順位はユーザーの実際の操作体験に近づけることを基準としています。

要素を取得するためのマッチャーとして、getByRole を優先使用するようにしましょう!

  1. getByRole
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByDisplayValue
// ❌
const button = screen.getByTestId('submit-button');

// ✅
const button = screen.getByRole('button', { name: '送信' });

🔍 参考:

まとめ

以上です!
今後も定期的に更新していこうと思います🙋‍♂️
(コメントも大歓迎です)

良いエンジニアライフを!

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?