Next.jsのコンポーネント入門:props・state・childrenを理解する
Next.jsのコンポーネントをTypeScriptで学ぶ入門記事。props・state・children、ページと部品の配置、型推論、App Routerでの境界をコードで解説します。
Reactのコンポーネントは、画面を部品に分けて組み立てるための仕組みです。Next.jsでも、ページ全体、共通のレイアウト、ボタンやカードなどをReactのコンポーネントとして作成します。ReactはUIのライブラリで、Next.jsはルーティングやHTMLの生成などを加えるフレームワークです。
この記事ではTypeScriptとPages Routerを使います。src/pages/がページ、src/components/が再利用する部品という構成で説明します。部品用のディレクトリ名は開発者が決めるもので、Next.jsの必須規則ではありません。
コンポーネントの入力・状態・子要素を理解する
関数から画面を返す
コンポーネント名は大文字で始めます。HTMLに似たJSXを返す関数として定義し、JSXを含むTypeScriptのファイルには.tsxを使います。
ファイル:src/components/Greeting.tsx
type GreetingProps = {
name: string;
message?: string;
};
export default function Greeting({
name,
message = '今日も一緒に学びましょう',
}: GreetingProps) {
return (
<section>
<h2>{name}さん、こんにちは</h2>
<p>{message}</p>
</section>
);
}
GreetingPropsは、この部品に渡せる値の型です。nameは必須の文字列、messageは省略可能な文字列です。?を付けた項目まで必須になるわけではありません。messageを省略するかundefinedを渡すと、引数のデフォルト値が使われます。
JSXでは{name}のように波括弧でJavaScriptの値を埋め込みます。通常の文字列はReactがエスケープして表示します。外部のHTMLを直接挿入する処理とは別なので、HTML文字列をそのまま安全に使えると考えないようにしてください。
propsを変えて同じ部品を使う
ファイル:src/pages/greetings.tsx
import Greeting from '../components/Greeting';
export default function GreetingsPage() {
return (
<main>
<h1>コンポーネントの再利用</h1>
<Greeting name="葵" />
<Greeting name="空" message="復習する記事を選んでください" />
</main>
);
}
npm run devを実行して/greetingsを開くと、同じ部品で異なる内容を表示できます。name={123}のように数値を渡したり、必須のnameを省略したりすると、型チェックで間違いを見つけられます。
propsは、子コンポーネントが読み取る入力です。受け取ったオブジェクトを子から直接変更せず、更新が必要なら親が渡すコールバックなどを呼びます。型定義は外部APIの応答を自動で検証するものではないため、外部データを渡す前には実行時の検証も必要です。
操作で変わる値をstateにする
ファイル:src/components/LikeButton.tsx
import { useState } from 'react';
type LikeButtonProps = { label: string };
export default function LikeButton({ label }: LikeButtonProps) {
const [liked, setLiked] = useState(false);
return (
<button
type="button"
aria-pressed={liked}
onClick={() => setLiked(current => !current)}
>
{label}: {liked ? '保存済み' : '未保存'}
</button>
);
}
labelは親から受け取るprops、likedはこの部品が持つstateです。初期値がfalseなので、状態はbooleanと推論されます。TypeScriptでも、すべてのuseStateに型引数を明記する必要はありません。
setLiked()に次の値を計算する関数を渡し、Reactへ更新を依頼します。状態変数へ直接代入する方法ではありません。なお、この例は画面内の状態だけを変更し、データベースやブラウザの保存領域には記録しません。再読み込みすると初期状態に戻ります。
先ほどのページからLikeButtonを読み込んで<LikeButton label="この記事" />を配置すると、操作できます。コンポーネントを二つ配置すれば、各インスタンスは別々の状態を持ちます。
子要素を受け取るレイアウトを作る
共通の余白や見出し枠を部品にまとめる場合は、childrenで内側の要素を受け取れます。
ファイル:src/components/NoteFrame.tsx
import type { ReactNode } from 'react';
type NoteFrameProps = {
title: string;
children: ReactNode;
};
export default function NoteFrame({ title, children }: NoteFrameProps) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
childrenは、<NoteFrame title="メモ">...</NoteFrame>の内側に書いた内容です。ReactNodeには文字列やReact要素など、子要素として描画できる値が含まれます。この例ではchildrenをpropsの必須項目として宣言しています。
ページ・部品・レイアウトの違い
- Pages Routerのページは
src/pages/へ置き、既定のエクスポートを用意します。通常のページファイルの位置がURLに対応します。 - 再利用する部品は
src/components/などへ置きます。ここへ置いただけではURLは作られません。 - レイアウトもReactのコンポーネントです。Pages Routerではページや
_app.tsxから読み込みます。App Routerのlayout.tsxという特殊ファイルとは仕組みが異なります。
App Routerで同じ部品を使う場合
App Routerではページやレイアウトは標準でServer Componentです。状態やクリック処理を使うLikeButtonはClient Componentの境界内へ置きます。部品のファイルを境界にするなら、先頭のimportより前へ'use client';を追加します。
表示だけを担当する部品まで一律にClient Componentへ変更する必要はありません。ルーターによって配置と境界が変わるため、Pages RouterのコードをApp Routerへ移すときはその違いを確認してください。
関連記事と公式資料
- コンポーネントの作成と使用方法
- useStateの使用方法と注意点
- React公式:Passing Props to a Component
- React公式:Using TypeScript
- Next.js公式:Server and Client Components
部品を分ける判断基準
同じ役割の表示をまとめる
見た目が似ているだけでなく、同じ種類のデータと操作を扱うかを確認します。巨大な部品へ多数の条件を追加するより、変更する理由ごとに役割を分けると理解しやすくなります。
状態を置く場所を決める
複数の部品で同じ値を使う場合は、共通の親が管理する方法を検討します。propsを各部品のstateへ無条件にコピーすると、元の値と表示がずれることがあります。