React Server Componentsでよくある間違い
Next.jsのApp Routerを採用しました。Server Componentsがデフォルトです。すべてがより高速で、シンプルで、効率的になるはずです。
しかし実際には、深夜2時にハイドレーションの不一致をデバッグし、バンドルサイズが増えた理由を考え、'use client'ディレクティブを置いた場所が正しいのか疑問に思っています。
React Server Componentsは、Reactアプリケーションの動作方法における根本的な変化を表しています。サーバー/クライアント境界は、もはやデプロイメントの懸念事項だけではありません。すべてのコンポーネントで行うアーキテクチャ上の決定なのです。これを誤ると、微妙なバグ、パフォーマンスの低下、そしてフレームワークを活用するのではなく対立するコードにつながります。
この記事では、本番環境のコードベースで私が見てきた最も一般的なReact Server Componentsの間違いと、それらを回避する方法について説明します。
重要なポイント
- Next.js App RouterではServer Componentsがデフォルトです。バンドルサイズを最小化するために、
'use client'をコンポーネントツリーのできるだけ下に配置しましょう。 server-onlyパッケージを使用して、機密性の高いサーバーコードが誤ってクライアントバンドルに公開されるのを防ぎます。- ServerからClient Componentsに渡す際は、常にシリアライズ不可能な値(関数やクラスインスタンスなど)を変換してください。
- Server Actions(
'use server')はRPCスタイルのエンドポイントであり、Server Componentsではありません。すべての入力を検証し、クライアントデータを決して信頼しないでください。 - Next.jsのデフォルト設定はバージョン間で変更されているため、
revalidateまたはcache: 'no-store'でキャッシュ動作を明示的に指定しましょう。
Server ComponentsとClient Componentsの理解
落とし穴に入る前に、基本を確立しましょう。App Routerでは、コンポーネントはデフォルトでServer Componentsです。これらはサーバー上で実行され、ブラウザAPIにアクセスできず、クライアントにJavaScriptを一切送信しません。
Client Componentsには'use client'ディレクティブが必要です。これらはuseStateやuseEffectなどのフックを使用でき、ブラウザAPIにアクセスし、ユーザーインタラクションを処理できます。
両者の境界が、ほとんどの間違いが発生する場所です。
‘use client’ディレクティブの過度な使用
Next.js App Router RSCで最も頻繁に見られる落とし穴は、'use client'に早すぎる段階で手を伸ばすことです。コンポーネントにuseStateが必要?クライアントコンポーネントとしてマークします。onClickハンドラーが必要?クライアントコンポーネントです。
問題点:'use client'は境界を作成します。そのコンポーネントがインポートするすべてのものが、たとえそれらのインポートがサーバーに留まることができたとしても、クライアントバンドルの一部になります。
// ❌ ページ全体がクライアントコンポーネントになる
'use client'
import { useState } from 'react'
export default function ProductPage({ product }) {
const [quantity, setQuantity] = useState(1)
return (
<div>
<ProductDetails product={product} />
<ProductReviews productId={product.id} />
<QuantitySelector value={quantity} onChange={setQuantity} />
</div>
)
}
// ✅ インタラクティブな部分のみがクライアントコンポーネント
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import QuantitySelector from './QuantitySelector'
export default function ProductPage({ product }) {
return (
<div>
<ProductDetails product={product} />
<ProductReviews productId={product.id} />
<QuantitySelector /> {/* これだけがクライアントコンポーネント */}
</div>
)
}
'use client'をコンポーネントツリーのできるだけ下に配置してください。インタラクティビティを必要とする最小のコンポーネントに分離します。
サーバー専用コードをClient Componentsにインポートする
クライアントコンポーネントがモジュールをインポートすると、そのモジュール全体(とその依存関係)がブラウザに送信されます。データベースクライアントや環境シークレットを読み取るファイルをインポートしましたか?サーバー専用コードをクライアントグラフに公開してしまったことになります。
// lib/db.js
import 'server-only' // 誤ったクライアントインポートを防ぐために追加
export async function getUsers() {
return db.query('SELECT * FROM users')
}
server-onlyパッケージ(Next.jsが提供)は、モジュールがクライアントコンポーネントにインポートされた場合にビルドエラーを発生させます。ブラウザに絶対に到達してはならないコードに使用してください。
Discover how at OpenReplay.com.
境界を越えてシリアライズ不可能な値を渡す
Server Componentsはシリアライゼーションを通じてClient Componentsにpropsを渡します。関数、クラスインスタンス、Map、Setはこの境界を越えることができません。
// ❌ クラスインスタンスはシリアライズできない
export default async function UserProfile({ userId }) {
const user = await getUser(userId)
return <ClientProfile user={user} /> // userはクラスインスタンス
}
// ✅ プレーンオブジェクトに変換
export default async function UserProfile({ userId }) {
const user = await getUser(userId)
return (
<ClientProfile
user={{
id: user.id,
name: user.name,
createdAt: user.createdAt.toISOString()
}}
/>
)
}
React Server Actionsの誤解
'use server'ディレクティブは、関数をServer Actionsとしてマークします。これはクライアントから呼び出し可能ですが、サーバー上で実行されます。これはコンポーネントをServer Componentにするものではありません。Server Componentsはデフォルトであるため、ディレクティブは必要ありません。
// これはServer Actionであり、Server Componentではない
async function submitForm(formData) {
'use server'
await db.insert({ email: formData.get('email') })
}
Server Actionsは実質的にRPCスタイルのエンドポイントです。APIルートのように扱ってください:入力を検証し、エラーを処理し、クライアントデータを決して信頼しないでください。
RSCキャッシングモデルの無視
Next.jsのキャッシング動作は大幅に進化してきました。fetch呼び出しがデフォルトでキャッシュされると仮定しないでください。これはNext.jsのバージョン、ルートセグメント構成、ランタイム設定によって異なります。データの鮮度について明示的にしてください。
// キャッシングの意図を明示的に
const data = await fetch(url, {
next: { revalidate: 3600 } // 1時間キャッシュ
})
// または完全にオプトアウト
const data = await fetch(url, { cache: 'no-store' })
Server ActionsでrevalidatePath()とrevalidateTag()を使用して、ミューテーション後にキャッシュされたデータを無効化します。RSCキャッシングモデルは、データの鮮度について意図的な決定を必要とします。
まとめ
React Server Componentsは、コードがどこで実行されるかについて慎重に考えることに報いてくれます。Server Componentsをデフォルトにしてください。クライアント境界を下に押し下げてください。エッジでデータをシリアライズしてください。Server Actionの入力を検証してください。キャッシングについて明示的にしてください。
メンタルモデルを内面化するには時間がかかりますが、その見返り(小さなバンドル、高速なロード、シンプルなデータフェッチング)は投資に値します。
よくある質問
部分的に可能です。Server Componentsはサーバー上でのみ実行されるため、useStateやuseEffectなどの状態やエフェクトフックを使用できません。ただし、useContextなどのフックはサポートされています。コンポーネントに状態、エフェクト、またはブラウザAPIが必要な場合は、use clientディレクティブを追加してClient Componentにする必要があります。これらのインタラクティブな部分をできるだけ小さく分離してください。
コンポーネントにインタラクティビティ、ブラウザAPI、またはuseStateやuseEffectなどのReactフックが必要かどうかを自問してください。必要な場合は、Client Componentである必要があります。データをレンダリングするだけ、またはデータベースからフェッチするだけの場合は、Server Componentのままにしてください。迷った場合は、Server Componentから始めて、ビルドまたはランタイムが明示的に要求する場合にのみuse clientを追加してください。
最も一般的な原因は、use clientをコンポーネントツリーの高い位置に配置していることです。親がClient Componentになると、そのすべてのインポートがクライアントバンドルに参加します。use clientディレクティブを監査し、最小のインタラクティブコンポーネントまで押し下げてください。また、クライアントコードでサーバー専用ライブラリを誤ってインポートしていないか確認してください。
use clientディレクティブは、フックとブラウザAPIにアクセスできるブラウザで実行されるコンポーネントをマークします。use serverディレクティブは、関数をServer Actionとしてマークします。これはクライアントから呼び出し可能ですが、サーバー上で実行されます。Server Componentsは、Next.js App Routerでデフォルトであるため、ディレクティブは一切必要ありません。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.