Next.jsのコンポーネント実践:TypeScriptのpropsとコールバックで連携する
型付きpropsとコールバックで記事カードを作り、親ページで選択状態を管理します。ジェネリクスを使う一覧、安定したkey、コンポーネントの読み込み方も解説します。
部品を使い回すには、受け取るデータとユーザー操作をpropsの型として定義し、値を管理する場所を決めます。この記事ではTypeScriptとPages Routerで記事カードと一覧を作り、選択状態を親ページへ持たせます。
React 18・Next.js 13で使えるコードです。src/pages/を使うプロジェクトを前提とし、App RouterのClient Componentの境界については最後に補足します。
記事を選択する画面を組み立てる
共有するデータ型を定義する
ファイル:src/types/article.ts
export type Article = {
id: string;
title: string;
description?: string;
};
idとtitleは必須、descriptionは省略可能です。typeだけでなくinterfaceでもこの形を表せます。型はコンパイル時の情報なので、APIの応答をこの型だと指定するだけでは内容の検証になりません。
コンポーネントの引数を分割代入しただけで、呼び出し元からpropsの型が常に推論されるわけではありません。部品の境界では、次のように受け取る型を明示すると契約を確認できます。
値とコールバックを受け取るカードを作る
ファイル:src/components/SelectableArticle.tsx
import type { Article } from '../types/article';
type Props = {
article: Article;
selected: boolean;
onSelect: (article: Article) => void;
};
export default function SelectableArticle({ article, selected, onSelect }: Props) {
return (
<article>
<h2>{article.title}</h2>
<p>{article.description ?? '説明は準備中です'}</p>
<button
type="button"
aria-pressed={selected}
onClick={() => onSelect(article)}
>
{selected ? '選択中' : 'この記事を選ぶ'}
</button>
</article>
);
}
カード自身は選択状態を保持せず、selectedを表示し、クリック時にはonSelect()で親へ知らせます。親と子の両方に同じ選択状態を持たせて同期する必要がなくなります。
イベントへ渡しているのは() => onSelect(article)という関数です。onClick={onSelect(article)}と書くと、クリックを待たず描画中に呼び出してしまうため、意味が異なります。
ジェネリクスで一覧の要素型を保つ
異なるデータの一覧にも使いたい場合は、要素の型を型パラメータで受け取れます。
ファイル:src/components/ItemList.tsx
import type { Key, ReactNode } from 'react';
type ItemListProps<T> = {
items: readonly T[];
getKey: (item: T) => Key;
renderItem: (item: T) => ReactNode;
};
export default function ItemList<T>({ items, getKey, renderItem }: ItemListProps<T>) {
if (items.length === 0) return <p>表示する項目がありません</p>;
return (
<ul>
{items.map(item => (
<li key={getKey(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
Tは渡された配列の要素型と、各コールバックが受け取る型を結び付けます。記事を渡せばコールバック内でもArticleとして扱えます。実行時に別のコンポーネントを自動生成する仕組みではありません。
keyには、同じ一覧の兄弟間で一意で、並び替えや更新でも安定する値を使います。順序が変わる一覧で配列の添字や毎回の乱数を使うと、状態と要素の対応が意図せず変わることがあります。
親ページで選択状態を管理する
ファイル:src/pages/article-picker.tsx
import { useState } from 'react';
import ItemList from '../components/ItemList';
import SelectableArticle from '../components/SelectableArticle';
import type { Article } from '../types/article';
const articles: Article[] = [
{ id: 'types', title: 'TypeScriptの基本', description: '型の書き方を学ぶ' },
{ id: 'components', title: 'Reactの部品設計' },
];
export default function ArticlePickerPage() {
const [selected, setSelected] = useState<Article | null>(null);
return (
<main>
<h1>読む記事を選ぶ</h1>
<p>選択: {selected?.title ?? '未選択'}</p>
<ItemList
items={articles}
getKey={article => article.id}
renderItem={article => (
<SelectableArticle
article={article}
selected={selected?.id === article.id}
onSelect={setSelected}
/>
)}
/>
<button type="button" onClick={() => setSelected(null)}>選択を解除</button>
</main>
);
}
npm run devで/article-pickerを開きます。記事を選ぶと親ページの表示とカードの状態が変わり、解除すると未選択へ戻ります。
初期値がnullでも、後で記事を保持するためuseState<Article | null>(null)と型を明記しています。型引数を省略した場合は初期値だけから推論され、この用途に必要な型になりません。
この例の一覧は固定です。APIで一覧が更新されるアプリでは、記事オブジェクトを別々の場所へ保存する代わりに選択IDだけを状態へ持ち、現在の一覧から記事を取り出す設計も検討してください。
ファイルと型の使い分け
export defaultしたコンポーネントは、読み込み側で名前を付けてimportします。export typeした型は、import typeで読み込み、実行時の値と区別できます。- JSXを含む部品は
.tsx、型定義だけのファイルは.tsを使えます。 React.FCは必須ではありません。引数にpropsの型を書いた通常の関数でもコンポーネントを定義できます。
型が正しくても、画面の意味やアクセシビリティが自動で保証されるわけではありません。この例では操作要素にbuttonを使い、aria-pressedで選択状態を伝えています。
App Routerで使用するとき
App Routerでは、useStateやイベントを使うコンポーネントをClient Componentの境界内へ置きます。ページ全体を対話用にする簡単な例なら、ページの先頭に'use client';を指定します。より大きなページでは、対話が必要な記事選択部分だけを別のClient Componentへ分けられます。
Server ComponentからClient Componentへ関数のpropsを自由に渡せるわけではありません。この例のsetSelectedやrenderItemは、同じクライアント側の部品の間で受け渡しています。
関連記事と公式資料
- コンポーネントの基礎について
- useStateの使用方法と注意点
- TypeScriptのジェネリクス
- React公式:Using TypeScript
- React公式:Rendering Lists
- Next.js公式:Server and Client Components
再利用する部品の確認点
表示するデータと操作を分ける
子は表示と操作の通知を担当し、親が選択状態を管理する形にすると、一覧以外の画面でも使いやすくなります。通信や保存まで子に集める前に、部品の利用範囲を確認します。
一覧の変化を試す
項目の追加、並べ替え、削除を行い、選択状態が別の項目へ移らないことを確認します。安定したkeyと、選択中のIDが存在しなくなったときの処理を設計してください。