この記事の目次
サーバーで読んだ一覧へ「候補に入れる」ボタンを付けたい。onClickのエラーを消すためにページ全体へuse clientを付けると、今度はサーバー用のファイル読取りを同じ場所に置けなくなります。操作する範囲だけを部品として分けると、両方の役割を残せます。
ReactのpropsとuseState、async関数が分かる人向けです。架空の資料室の一覧を、Next.js 16.4.5のApp Router、React / React DOM 19.2.0、Node.js 24系で作ります。Pages Routerの例ではありません。選択はブラウザー内だけで、保存や予約の成立を意味しません。
ページの表示と、操作の入口を別のファイルへ置く
App Routerでは、pageやlayoutは既定でServer Componentです。今回のpageはローカルJSONを読み、表示用の情報を選びます。クリックとstateを使う部分は、use clientを先頭に持つ別ファイルへ分けます。
Next.jsの公式説明にある境界は、見た目の上下ではなくモジュールの依存関係に関わります。Client Componentがimportする処理もクライアント側の対象になるため、サーバー用の読取り関数をそこから直接importしません。
| ファイル | 担当 | ブラウザーへ渡すもの |
|---|---|---|
| app/page.jsx | JSONを読み、掲載用データを作る | idとtitleの配列 |
| app/choices.jsx | 選択状態とクリック処理 | 受け取った配列を表示する |
| data/catalog.json | 模擬の元データ | ファイル全体は渡さない |
Server Componentで使ったデータが自動ですべて秘密になるわけではありません。JSXへ表示した文字とClient Componentへ渡したpropsは、利用者へ届く情報です。この例では不要なinternalNoteを明示的に取り除きます。実際の権限確認を省略してよいという意味ではありません。
JSONを読むページと、候補を選ぶ部品を作る
空フォルダで次を実行し、appとdataを用意します。サンプルのメモは架空の文字で、認証情報を入れる練習ではありません。
npm init -y
npm install --save-exact next@16.4.5 react@19.2.0 react-dom@19.2.0
mkdir app data
data/catalog.jsonを保存します。
[
{ "id": "map", "title": "地域の地図", "internalNote": "練習用の未公開メモA" },
{ "id": "photo", "title": "街の写真", "internalNote": "練習用の未公開メモB" }
]
app/layout.jsxにはページ共通のHTMLを書きます。
export const metadata = { title: '資料の候補|練習' };
export default function Layout({ children }) {
return <html lang="ja"><body style={{ margin: 16, fontFamily: 'sans-serif', lineHeight: 1.8 }}>
{children}
</body></html>;
}
app/page.jsxでファイルを読みます。この固定データの例は、公開用ビルドで静的に事前描画される構成です。サーバー側で実行することと、閲覧のたびに実行することは同じではありません。
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import Choices from './choices';
export default async function Page() {
const raw = await readFile(path.join(process.cwd(), 'data/catalog.json'), 'utf8');
const records = JSON.parse(raw);
const items = records.map(({ id, title }) => ({ id, title }));
return <main style={{ maxWidth: 560, margin: 'auto' }}>
<h1>資料の候補</h1>
<p>架空の一覧です。選択内容は保存・送信しません。</p>
<Choices items={items} />
</main>;
}
続いてapp/choices.jsxへ操作部分を書きます。use
clientはimportより前に置きます。
'use client';
import { useState } from 'react';
export default function Choices({ items }) {
const [selectedId, setSelectedId] = useState(null);
if (items.length === 0) return <p>公開中の資料はありません。</p>;
return <ul style={{ paddingLeft: 24 }}>
{items.map(item => <li key={item.id} style={{ marginBottom: 16 }}>
<span>{item.title} </span>
<button type="button" aria-pressed={selectedId === item.id}
onClick={() => setSelectedId(current => current === item.id ? null : item.id)}>
{selectedId === item.id ? '候補から外す' : '候補に入れる'}:{item.title}
</button>
</li>)}
</ul>;
}
npx next dev --hostname 127.0.0.1で起動します。地図を押すと地図が選択中になり、写真を押すと選択が写真へ移ります。同じボタンをもう一度押すと未選択に戻ります。一度に一件だけ選ぶ仕様なので、各行の真偽値を別々に持たずIDを一つ保持しています。
関数を渡す前に、どちらでクリックを処理するか決める
第一の失敗は、choices.jsxのuse clientを削除することです。useStateを使う部品がサーバー側として扱われ、開発画面やビルドで境界に関するエラーになります。修正は、この操作部品の先頭へ宣言を戻すことです。ページ全体へ追加する必要はありません。
第二の失敗は、Pageで普通の関数を作り、<Choices onSelect={() => console.log('選択')} ... />のように渡すことです。通常の関数をそのままサーバーからクライアントのpropsへ運ぶことはできません。use clientの公式資料で、境界を渡る値の条件を確認できます。
今回のクリックはサーバーへ送らないので、修正はonClickをchoices.jsx内へ置くことです。普通の関数を文字列へ変換して渡す方法も不要です。修正後、開発起動だけでなく公開用ビルドが通り、地図と写真の切替ができるかを再確認します。
Server Functionとして扱う関数には別の仕組みがありますが、このローカル選択のために導入する必要はありません。サーバーへ保存したい仕様なら、認可、入力検証、保存中と失敗の表示を含む別の操作として設計します。
Client Componentも、最初のHTMLに表示される
Clientという名前から「初期HTMLには空で、ブラウザーだけで表示する」と決め付けないでください。初回の表示ではClient Componentも事前描画され、JavaScriptが届いてから操作できる状態へつながります。今回の一覧とボタンの文字は初期HTMLで読めます。
このため、use clientがあってもレンダー中に無条件でwindowやlocalStorageへ触れると、サーバー側の事前描画で問題になります。ブラウザー専用の処理を必要なタイミングへ分ける判断は、Reactの状態共有の記事にあるpropsの受渡しとは別に考えます。
元JSONを更新した際の再取得やキャッシュはこの教材の範囲外です。ここではビルドし直してデータを反映します。保存後の更新を調べる場合はNext.jsのキャッシュの記事へ進み、境界を分けたことだけで最新情報の表示まで完成したとは判断しません。
公開用ビルドで、本文・操作・渡した情報を照合する
開発サーバーを終了し、npx next build、npx next start --hostname 127.0.0.1の順に実行します。15〜20分を目安に、地図を選ぶ、写真へ移す、写真を外す、の三条件を試します。aria-pressedとボタンの文が同じ選択状態を示すことを確認します。
加えてブラウザーのページソースで「地域の地図」があること、練習用の未公開メモA・Bがないことを調べます。JavaScriptを無効にすると一覧は読めますが、この選択操作は動きません。その状態で送信できるフォームを作った例ではありません。
data/catalog.jsonを空配列にしてビルドし直すと、公開中の資料がない案内になるのが追加課題の解答です。最後は二件へ戻します。TabとEnterで選択でき、狭い幅でも資料名を読めることも点検してください。カリキュラムで次の課題を選ぶ際は、サーバーの読取りとブラウザーの操作を別々に説明できたかを記録できます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。