React Server Componentsの落とし穴を乗り越える
Emily Parker
Product Engineer · Leapcell

ウェブ開発の次のフロンティアとその隠された落とし穴
React Server Components(RSC)は、ウェブアプリケーションの構築方法において重要な進化を遂げており、サーバーサイドレンダリングがクライアントサイドのインタラクティビティとシームレスに統合される未来を約束します。開発者がコンポーネントを完全にサーバーでレンダリングできるようにすることで、RSCはバンドルサイズを削減し、初期ページロードパフォーマンスを向上させ、機密情報をクライアントに公開することなく、データベースやファイルシステムのようなバックエンドリソースに直接アクセスできるようにすることを目指しています。このパラダイムシフトは、より高速で、より効率的で、より安全なアプリケーションを構築するための計り知れない可能性を提供します。しかし、あらゆる強力な新技術と同様に、RSCには独自のニュアンスと潜在的な落とし穴があります。これらの一般的なトラップを理解することは、RSCの力を効果的に活用し、フラストレーションのたまるデバッグセッションを避けるために不可欠です。この記事では、2つの一般的な問題、すなわち、サーバーコンテキストであるべき状況でのクライアントサイドでのデータ取得と、「'use client'」ディレクティブの誤用について掘り下げ、RSCを成功裏に活用するためのはるかに明確な道筋を提供します。
React Server Componentsのコアコンセプトの理解
一般的な落とし穴に飛び込む前に、React Server Componentsに関連するいくつかのコアコンセプトを簡単に再確認することが不可欠です。
React Server Components (RSC): これらはサーバーでのみレンダリングされるコンポーネントです。サーバーサイドリソースに直接アクセスでき、クライアントサイドのウォーターフォールなしでデータ取得を実行でき、JavaScriptコードをクライアントに送信しません。静的コンテンツ、データ取得、バックエンドサービスとの対話に理想的です。
React Client Components: これらはクライアントでレンダリングされる従来のReactコンポーネントです。インタラクティブで、ブラウザAPI(windowやlocalStorageなど)にアクセスでき、useState、useEffect、useRefのようなフックを使用できます。JavaScriptをバンドルしてクライアントに送信する必要があります。
'use client' directive: ファイルの先頭に配置されるこの特別なディレクティブは、Reactビルドシステムに、コンポーネント(およびそれがインポートするすべてのモジュール)が、サーバーコンポーネントツリー内でレンダリングされる場合でも、クライアントコンポーネントとして扱われることを示します。これはサーバーコードとクライアントコードの境界です。
RSCでのデータ取得: Server Componentsは、標準的なJavaScript async/await構文を使用して直接データを取得したり、個別のAPIレイヤーを必要とせずにデータベースに直接クエリを実行したりできます。このデータは、その後、他のServer ComponentsまたはClient Componentsにpropsとして渡されます。
これらの基本的な概念を念頭に置いて、一般的な間違いを見ていきましょう。
サーバーコンポーネントコンテキストでのクライアントサイドデータ取得の落とし穴
React Server Componentsの主な利点の1つは、サーバーのデータソースへの近接性を活用し、初期データのクライアントサイドネットワークリクエストを排除して、サーバーで直接データ取得を実行できることです。しかし、一般的な間違いは、コンポーネントがServer Componentであることを意図している場合でも、クライアントサイドでのデータ取得を続けることです。これは、開発者がRSC環境だと思い込み、パラダイムシフトを完全に理解せずに、(useEffectとfetchを使用するなど)既存のクライアントサイドデータ取得パターンを誤ってRSC環境に移植する場合によく見られます。
製品リストを表示したいシナリオを考えてみましょう。従来のクライアントサイドアプリケーションでは、次のようなことを行うかもしれません。
// components/ProductList.js (従来のクライアントコンポーネント) import React, { useState, useEffect } from 'react'; function ProductList() { const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchProducts = async () => { try { const response = await fetch('/api/products'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); setProducts(data); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchProducts(); }, []); if (loading) return <div>Loading products...</div>; if (error) return <div>Error: {error.message}</div>; return ( <ul> {products.map(product => ( <li key={product.id}>{product.name} - ${product.price}</li> ))} </ul> ); } export default ProductList;
調整なしでこのコードをServer Componentファイルに単純に移動すると、失敗します。useStateとuseEffectはクライアントサイドのフックであり、Server Componentでは使用できません。コンポーネント全体を'use client'とマークする必要があり、サーバーサイドデータ取得の目的を損ないます。
Server Componentの正しいアプローチは、async/awaitを使用して直接データを取得することです。
// app/products/page.js (Server Component) import ProductCard from '../../components/ProductCard'; // ServerまたはClient Componentが可能 async function getProducts() { // 実際のアプリケーションでは、これは直接データベースにクエリしたり、 // 外部HTTPを通過しない内部APIルートを呼び出したりするかもしれません。 const res = await fetch('https://api.example.com/products'); if (!res.ok) { // これが最も近い`error.js` Error Boundaryをアクティブにします throw new Error('Failed to fetch data'); } return res.json(); } export default async function ProductsPage() { const products = await getProducts(); // データはサーバーで直接取得されます return ( <section> <h1>Our Products</h1> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px' }}> {products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> </section> ); }
ここで、getProductsはコンポーネントがレンダリングされる前にサーバーで実行されるasync関数です。データは直接利用可能であり、初期取得のためにクライアントサイドJavaScriptは必要ありません。この間違いを犯すということは、RSCのコアメリットを活用していないことを意味し、結果として、明確なディレクティブなしで実質的にクライアントコンポーネントであるコンポーネントをレンダリングし、混乱と最適ではないパフォーマンス特性につながります。
'use client'の誤用
'use client'ディレクティブは、サーバーコードとクライアントコードの間の境界を示す強力な明示的なマーカーです。その目的は、コンポーネント(およびそれがインポートするものすべて)がハイドレートされ、クライアントで実行されることを明確に示すことです。しかし、開発者はしばしば、'use client'を広範囲に使用したり、その影響を誤解したりするトラップに陥りがちです。
トラップ1: コンポーネントツリー全体を不必要にクライアントコンポーネントとしてマークする。
多くのサブコンポーネントを含む複雑なコンポーネントがあり、そのうちのほんの一部だけがクライアントサイドのインタラクティビティを必要とする場合、親コンポーネントを'use client'とマークすると、そのすべての子供(およびそれらの依存関係)が、サーバーでレンダリングできたとしても、クライアントコンポーネントとして強制されます。これは不必要にクライアントバンドルサイズを増加させます。
ユーザープロフィールを表示するページを考えてみましょう。プロフィールのほとんどの情報は静的ですが、「フォロー」ボタンがあり、クライアントサイドのインタラクションが必要です。
// app/profile/[id]/page.js (Server Component) import { fetchUserProfile } from '@/lib/data'; // Server-side data fetching import ProfileDetails from '@/components/ProfileDetails'; // Server Componentが可能 import FollowButton from '@/components/FollowButton'; // Client Componentが必要 export default async function UserProfilePage({ params }) { const user = await fetchUserProfile(params.id); return ( <div> <h1>User Profile</h1> <ProfileDetails user={user} /> {/* Server Component */} <FollowButton userId={user.id} /> {/* Client Component */} <UserActivityFeed userId={user.id} /> {/* 別のServer Component */} </div> ); }
そして FollowButton.js で:
// components/FollowButton.js 'use client'; // このコンポーネントはクライアントサイドのインタラクティビティが必要です import { useState } from 'react'; export default function FollowButton({ userId }) { const [isFollowing, setIsFollowing] = useState(false); // 例のステート const handleClick = () => { // クライアントサイドのアクションを実行、例: APIリクエストを送信 console.log(`Toggling follow for user ${userId}`); setIsFollowing(!isFollowing); }; return ( <button onClick={handleClick}> {isFollowing ? 'Following' : 'Follow'} </button> ); }
この構造では、ProfileDetails と UserActivityFeed はServer Componentのままで、サーバーでデータを取得し、ほとんど静的なコンテンツをレンダリングできます。FollowButtonだけが'use client'ディレクティブを必要とします。なぜなら、useStateを使用し、ユーザーインタラクションを処理するからです。UserProfilePage自体が'use client'とマークされていた場合、これらのすべてのコンポーネントがクライアントコンポーネントとなり、必要以上に多くのJavaScriptが出力されます。
トラップ2: クライアントコンポーネントにインポートされたサーバー専用モジュールの影響を無視する。
クライアントコンポーネント('use client'とマークされた)がモジュールをインポートすると、そのモジュール(およびその依存関係)もクライアントバンドルの一部になります。これは、サーバー専用ユーティリティ(例: 機密性の高い認証情報を使用して直接データベースにクエリを実行する関数)が誤ってクライアントコンポーネントにインポートされた場合に問題を引き起こす可能性があります。ビルドシステムは一般的にエラーを発生させますが、クライアント/サーバー境界の根本的な誤解を浮き彫りにします。
getDataユーティリティを考えてみましょう。
// lib/data.js (サーバー専用ユーティリティ) import 'server-only'; // このファイルがクライアント用にバンドルされないことを保証します import { db } from './db'; // dbクライアントがサーバーサイド専用であると仮定します export async function getUsers() { const users = await db.query('SELECT * FROM users'); return users; }
誤ってgetUsersをクライアントコンポーネントにインポートした場合:
// components/BadComponent.js 'use client'; import { useEffect, useState } from 'react'; import { getUsers } from '@/lib/data'; // !!! 危険: サーバー専用をクライアントにインポート export default function BadComponent() { const [users, setUsers] = useState([]); useEffect(() => { // 'server-only'が使用されている場合、ビルド時に失敗します。 // キャッチされなかった場合、認証情報を公開します。 getUsers().then(setUsers); }, []); // ... }
'server-only'パッケージは、モジュールがクライアントコンポーネントにインポートされた場合にビルドエラーを発生させることで、これを防ぐのに役立ちます。しかし、根本的な問題は、どのコードがどこに属するかについての誤解です。Client Componentsは、Server Componentsからpropsとしてデータを受け取るか、クライアントからアクセス可能なAPIルートからデータを取得すべきであり、サーバー専用ロジックから直接取得すべきではありません。
ベストプラクティスの採用
これらのトラップを回避するために、開発者は以下のことを行うべきです。
- Server Componentsをデフォルトにする: コンポーネントがServer Componentであると仮定することから始めます。ブラウザ固有のAPI(例:
window、localStorage)、ステート(useState、useReducer)、エフェクト(useEffect)、またはイベントハンドラが本当に必要な場合にのみ、'use client'を導入します。 - 関数とPropsを下位に渡す: Server Componentsは、propsとしてデータをClient Componentsに渡すことができます。また、Client Componentsによって(例:
onClickハンドラ経由で)呼び出される関数をトリガーとして、サーバーアクションまたはサーバーサイドロジックを実行することもできます。 - クライアントサイドロジックをカプセル化する: クライアントサイドのインタラクティビティを可能な限り小さなClient Componentsに分離します。これにより、アプリケーションロジックとレンダリングの大部分をサーバー上に維持し、クライアントバンドルを最小限に抑えます。
- モジュールグラフを理解する: モジュールがどのようにインポートされるかに注意してください。Client Componentがモジュールをインポートする場合、そのモジュールとその依存関係のツリー全体がクライアントバンドルに含まれます。絶対にクライアントに触れてほしくないモジュールには
'server-only'パッケージを使用します。
結論
React Server Componentsは、より効率的でパフォーマンスの高いアプリケーションを可能にする、ウェブ開発における強力な進化を提供します。しかし、このパラダイムへの移行には、コードがどこで実行され、データがどこに配置されるかについての考え方の根本的な変化が必要です。クライアントサイドでのデータ取得がサーバーコンテキストで行われること、および'use client'ディレクティブが無差別に( indiscriminately)使用されるといった一般的な落とし穴は、RSCを従来のReactコンポーネントとして扱うことからしばしば生じます。サーバーファーストの考え方を受け入れ、クライアント境界を戦略的に配置し、各コンポーネントタイプの意味を理解することで、開発者はReact Server Componentsの可能性を最大限に引き出し、堅牢で驚異的な速さの体験を構築できます。RSCをマスターする鍵は、コンポーネントの配置とデータフローに対する意図的で情報に基づいたアプローチにあります。

