クライアントコンポーネントを使う
このチュートリアルでは、クライアントコンポーネントを使用してアプリケーションにインタラクティブな要素を作成する方法を学びます。クライアントサイドレンダリングと動的なコンテンツ更新により、ユーザーエクスペリエンスを向上させましょう。
ナビゲーションと画像表示にクライアントコンポーネントを使用するシンプルなフォトギャラリーアプリケーションを構築します。クライアントコンポーネントの作成方法とユーザーインタラクションを扱う方法を学びます。
また、フレームワークが提供するファイルシステムベースのルーティングソリューションを使用して、アプリケーションのルートを作成します。これによりページ遷移を実現し、適切なコンテンツを表示できるようになります。
以下のコマンドを使用して新しいReactアプリケーションを作成します:
mkdir photos
cd photos
pnpm init
pnpm add @lazarv/react-server react-click-away-listener @faker-js/faker
pnpm add -D @types/react @types/react-dom autoprefixer postcss tailwindcss@3 typescript typescript-plugin-css-modules
pnpx tailwindcss@3 init -p
mkdir src
mkdir src/app
mkdir src/components
以下のようにtailwind.config.js
を変更する必要があります:
tailwind.config.js/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx}",
"./src/app/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
これらの変更により、Tailwind CSSがcomponentsとappディレクトリをスキャンして最終ビルドに含めるスタイルを検索できるようになり、放射状グラデーションと円錐グラデーションのサポートが追加されました。
src/app
ディレクトリにTailwind CSSスタイルをインポートするために必要なglobal.css
ファイルを作成します:
src/app/global.css@tailwind base;
@tailwind components;
@tailwind utilities;
tsconfig.json
は以下のようになります:
tsconfig.json{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"strict": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["react/experimental", "react-dom/experimental"],
"module": "ESNext",
"moduleResolution": "Bundler",
"plugins": [{ "name": "typescript-plugin-css-modules" }]
},
"include": ["**/*.ts", "**/*.tsx", ".react-server/**/*.ts"],
"exclude": ["**/*.js", "**/*.mjs"]
}
プロジェクトでCSSモジュールを使えるようにするために、typescript-plugin-css-modules
プラグインを有効にしています。また、組み込みのLink
コンポーネントとファイルシステムベースのルーターを使った型付きルーティングを有効にするために、.react-server/**/*.ts
のファイルもプロジェクトに含めています。
ファイルシステムベースのルーターを使用する場合、react-server.config.json
ファイルでルートのディレクトリを指定することができます。これにより、ルーターはプロジェクト全体のディレクトリツリーをクロールすることなく、どこを探索すればよいかを明確にできます。特にディレクトリやファイルが多い大規模なプロジェクトでこの設定が役立ちます。プロジェクトのルートディレクトリに以下の内容でreact-server.config.json
ファイルを作成してください:
react-server.config.json{
"root": "src/app"
}
以下のようにsrc/photos.ts
という新しいファイルを作成しましょう:
src/photos.tsimport { faker } from "@faker-js/faker";
export type Photo = {
id: string;
username: string;
imageSrc: string;
};
const photos: Photo[] = new Array(9).fill(null).map((_, index) => ({
id: `${index}`,
username: faker.internet.userName(),
imageSrc: faker.image.urlPicsumPhotos(),
}));
export default photos;
このファイルは、@faker-js/faker
パッケージを使用して、ランダムに選ばれた9枚の写真を生成し配列に格納します。各写真にはid
、username
、imageSrc
プロパティが付与されます。これがギャラリー用のランダム写真セットとなり、アプリケーションのデータソースとなります。
このシンプルなフォトギャラリーには、ギャラリーを表示するためのメインルートが1つと、写真1枚をモーダルで表示するためのルーターアウトレットが1つだけ存在します。
すべてのページをHTMLドキュメントレイアウトにラップしたいので、以下のようにsrc/app/(root).layout.tsx
ファイルを作成する必要があります:
src/app/(root).layout.tsximport "./global.css";
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Photos</title>
<meta
name="description"
content="A sample app showing dynamic routing with modals as a route."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
{children}
</body>
</html>
);
}
このレイアウトは、アプリケーション内のすべてのルートで使用されます。グローバルCSSスタイルが含まれ、ページのタイトルと説明を設定します。これはReact Server Componentであり、サーバー側でのみレンダリングされます。
レイアウトファイルの名前を(root).layout.tsx
にしているのは、アプリ全体のルートレイアウトとして使うためです。(root)
はあくまで識別のための名前で、ルーターは括弧で囲まれた部分を無視します。また、.layout.tsx
という拡張子を付けることで、ルーターはこのファイルをレイアウトとして認識し、以降すべてのルートに適用します。
メインのギャラリービューとインデックスページを作成するために、以下のsrc/app/page.tsx
ファイルを作成する必要があります:
src/app/page.tsximport { Link } from "@lazarv/react-server/navigation";
import swagPhotos from "../photos";
export const ttl = 30000;
export default function Home() {
const photos = swagPhotos;
return (
<main className="container mx-auto">
<h1 className="text-center text-4xl font-bold m-10">Photos</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 auto-rows-max gap-6 m-10">
{photos.map(({ id, imageSrc }) => (
<Link
key={id}
to={`/photos/${id}`}
prefetch
ttl={30000}
rollback={30000}
>
<img
alt=""
src={imageSrc}
height={500}
width={500}
className="w-full object-cover aspect-square"
/>
</Link>
))}
</div>
</main>
);
}
このページにはフォトギャラリーが表示されます。ギャラリー内の各写真へのリンクは、@lazarv/react-server/navigation
モジュールを使用して作成しています。prefetch
、ttl
、rollback
プロパティは、写真データをプリロードして一定時間キャッシュすることで、ナビゲーションエクスペリエンスの最適化に役立ちます。
Link
コンポーネントのto
プロパティが/photos/${id}
に設定されていることにお気づきでしょうか。これは、ルーターがこのパスを基にルートをマッチングし、対応する写真を表示するためのコンポーネントをレンダリングするために使用されます。さらに、このto
プロパティは型安全であり、src/app
ディレクトリに定義されたルートと照合されるため、安心して使用できます。
デフォルトでエクスポートされる関数Home
は、サーバーサイドでレンダリングされるReact Server Componentです。一方、Link
コンポーネントはクライアントサイドコンポーネントであり、サーバーサイドでレンダリングされた後、クライアントサイドでハイドレートされます。これにより、アプリケーション内でサーバーコンポーネントとクライアントコンポーネントを簡単に組み合わせることができます。また、Link
コンポーネントの子要素として使用される画像要素は、サーバーサイドでのみレンダリングされます。クライアントサイドで使用される機能や状態を利用するためには、"use client";
ディレクティブでアノテーションされたコンポーネントのみがクライアントサイドに読み込まれます。
Link
コンポーネントを使用すると、新しいページはクライアントサイドナビゲーションを利用して読み込まれ、写真間を移動する際にブラウザはページをリロードしません。ユーザーが移動するページのペイロードは、HTMLドキュメント全体ではなく、ページのレンダリングに必要なReact Server Componentのペイロードのみになります。これによりナビゲーションの速度と応答性が向上します。
1枚の写真のモーダルビューを表示する最初のクライアントコンポーネントを作成しましょう。以下の内容でsrc/components/modal/Modal.tsx
ファイルを作成してください:
src/components/modal/Modal.tsx"use client";
import { useEffect } from "react";
import ClickAwayListener from "react-click-away-listener";
export default function Modal({ children }: { children: React.ReactNode }) {
useEffect(() => {
document.body.classList.add("overflow-hidden");
return () => document.body.classList.remove("overflow-hidden");
}, []);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
history.back();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<div className="fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60">
<ClickAwayListener onClickAway={() => history.back()}>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl sm:w-10/12 md:w-8/12 lg:w-1/2 p-6">
{children}
</div>
</ClickAwayListener>
</div>
);
}
最も重要な部分はファイルの最初の行に記述する"use client";
ディレクティブです。このディレクティブは、フレームワークに対してこのコンポーネントをクライアントサイドでロードするように指示します。これがクライアントコンポーネントを作成する方法です。ファイルの残りの部分は、モーダルビューで写真を表示するシンプルなモーダルコンポーネントです。モーダルが開いているときにページのスクロールを防ぐため、body
要素にoverflow-hidden
クラスを追加または削除するためにuseEffect
フックを使用します。また、キーボードナビゲーションをサポートするために別のuseEffect
フックを使用します。さらに、ユーザーがモーダルの外側をクリックした際にモーダルを閉じるために、react-click-away-listener
パッケージのClickAwayListener
コンポーネントを使用します。ClickAwayListener
は既にクライアントサイドコンポーネントであるModal
コンポーネント内でインポートされるため、"use client";
ディレクティブを再度指定する必要はありません。
クライアントコンポーネントでは、ブラウザイベントを処理するuseEffect
フックなど、クライアント側でのみ機能するすべてのReactフックを使用できます。また、react-click-away-listener
パッケージのClickAwayListener
コンポーネントなど、ブラウザーのクライアント側で実行する必要があるReactコンポーネントも使用できます。
children
プロパティは、Reactコンポーネントであればどのようなタイプでも構いません。React Server Componentでも、他のクライアントコンポーネントでも構いません。今回はモーダルビューに写真を表示するために使用します。
すべてのクライアントコンポーネントはサーバー側でレンダリングされ、その後クライアント側でハイドレートされます。つまり、サーバーがコンポーネントをレンダリングしてクライアントに送信し、クライアント側で再レンダリングされてクライアント側のロジックが適用されます。
モーダルビューに1枚の写真を表示するためのルートを作成します。以下のようにsrc/app/@modal/photos/[id].page.tsx
ファイルを作成します。
src/app/@modal/photos/[id].page.tsximport Frame from "../../../components/frame/Frame";
import Modal from "../../../components/modal/Modal";
import photos from "../../../photos";
export default function PhotoModal({ id: photoId }: { id: string }) {
const photo = photos.find((p) => p.id === photoId);
return (
<Modal>{!photo ? <p>Photo not found!</p> : <Frame photo={photo} />}</Modal>
);
}
このページでは、モーダルビューに1枚の写真を表示します。ディレクトリ名に@modal
パターンを使用することで、このルートをmodal
という名前のアウトレットとしてレンダリングすることをルーターに指示し、メインのギャラリーページ上で使用できるようにします。photos
配列はsrc/photos.ts
ファイルからインポートされ、photoId
パラメータを使用して一致するid
を持つ写真を探します。
Frame
コンポーネントは、モーダルビューに写真を表示するシンプルなコンポーネントです。以下のようにsrc/components/frame/Frame.tsx
ファイルを作成してください:
src/components/frame/Frame.tsximport { Photo } from "../../photos";
export default function Frame({ photo }: { photo: Photo }) {
return (
<>
<img
alt=""
src={photo.imageSrc}
height={600}
width={600}
className="w-full object-cover aspect-square col-span-2"
/>
<div className="bg-white p-4 px-6">
<p>Taken by {photo.username}</p>
</div>
</>
);
}
このコンポーネントはReact Server Componentです。写真と写真を撮影したユーザーのユーザー名を表示します。
アウトレットルートに戻ると、Frame
コンポーネントを使用して、Modal
クライアントコンポーネントを利用してモーダルビューに写真を表示していることがわかります。アプリケーション内でReact Server Componentsとクライアントコンポーネントを組み合わせることで、動的でインタラクティブなユーザーインターフェースを作成し、レンダリングプロセスを最適化できます。クライアントコンポーネントは、モーダル、ポップアップ、JavaScript駆動型アニメーション、その他のクライアントサイド機能など、アプリケーションのインタラクティブまたは動的な部分にのみ使用するべきです。
レイアウトコンポーネントに戻り、ルートが/photos/[id]
パターンに一致したときにモーダルをレンダリングする新しいアウトレットを追加します。src/app/(root).layout.tsx
ファイルを以下の内容で更新します:
src/app/(root).layout.tsximport "./global.css";
export default function Layout({
modal,
children,
}: React.PropsWithChildren<{
modal: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Photos</title>
<meta
name="description"
content="A sample app showing dynamic routing with modals as a route."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
{children}
{modal}
</body>
</html>
);
}
ファイルシステムベースのルーターを使用するレイアウトでは、後続のすべてのアウトレットをpropsとして受け取ります。modal
propには、ルートが一致した場合にモーダルアウトレットによってレンダリングされるコンポーネントが含まれます。このpropを使用して、レイアウトコンポーネント内のモーダルビューをギャラリービュー上でレンダリングできます。
また、ReactServerComponent
コンポーネントを使用してモーダルビューのクライアント側ナビゲーションを有効にすることで、ナビゲーションを最適化することもできます。ページ更新時の初期コンテンツには、outletプロパティを使用できます。ReactServerComponent
をLink
コンポーネントと併用すると、リンクがモーダルアウトレットを更新する際、サーバー側でのみRSCペイロードとしてアウトレットをレンダリングするため、ネットワークペイロードが小さくなります。ペイロードのサイズは、ページ全体をRSCとして再レンダリングした場合の0.1 倍に縮小され、約15kから約1.5kに縮小されます。より複雑なアプリケーションでは、これがどれほど大きな影響を与えるか想像してみてください。src/app/(root).layout.tsx
ファイルを以下のように更新します:
src/app/(root).layout.tsximport { ReactServerComponent } from "@lazarv/react-server/navigation";
import "./global.css";
export default function Layout({
modal,
children,
}: React.PropsWithChildren<{
modal: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Photos</title>
<meta
name="description"
content="A sample app showing dynamic routing with modals as a route."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
{children}
<ReactServerComponent outlet="modal">
{modal}
</ReactServerComponent>
</body>
</html>
);
}
アプリケーションを実行するには、以下のスクリプトをpackage.json
ファイルに追加します:
package.json{
"scripts": {
"dev": "react-server",
"build": "react-server build",
"start": "react-server start"
}
}
ファイルシステムベースのルーターを使用することで、アプリケーションのエントリポイントを指定する必要がなくなります。ルーターはsrc/app
ディレクトリ内のルートを自動的に検出し、各ルートに適したコンポーネントをレンダリングします。
これで以下のコマンドでアプリケーションを実行できます:
pnpm dev
ブラウザを開き、http://localhost:3000
にアクセスしてフォトギャラリーアプリケーションを起動します。写真をクリックするとモーダルビューが開き、拡大表示されます。
アプリケーションを本番環境用にビルドするには以下のコマンドを実行します:
pnpm build
以下のコマンドを使用して本番サーバーを起動できます:
pnpm start
このチュートリアルでは、クライアントコンポーネントを使用してアプリケーションにインタラクティブな要素を追加する方法を学びました。ナビゲーションと画像表示にクライアントコンポーネントを活用したシンプルなフォトギャラリーアプリケーションを構築しました。また、フレームワークが提供するファイルシステムベースのルーティングを使用して、アプリケーションのルートを作成する方法も学びました。このサンプルアプリケーションはGitHubリポジトリに公開されています。リポジトリをクローンし、pnpm install
を実行して依存関係をインストールした後、pnpm --filter ./examples/photos dev
コマンドでアプリケーションを実行できます。