この課題のねらい
この演習のテーマは「コンポーネント分割」と「props を使った汎用化」です。
つまり、こういう流れを体で覚えてもらうことが目的です。
最初は 1 つのコンポーネントにベタ書き
リスト表示部分を別コンポーネントとして切り出す
さらに props を使って、どんな配列にも使い回せる「汎用コンポーネント」に近づける
この感覚が身につくと、「あとから整理できる人」になれます。実務ではこれがとても大事です。
必須課題 リスト表示部分を別コンポーネント化
分割前の「ベタ書き」バージョン
まずは「分割前」の状態を見てみましょう。
講座タイトルの配列をそのままページコンポーネントに書いている例です。
"use client";
const lessons = [
"Reactの基本",
"Next.jsのページとルーティング",
"useStateとイベント処理",
];
export default function LessonsPage() {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h1>講座一覧</h1>
<ul style={{ marginTop: "12px" }}>
{lessons.map((title) => (
<li key={title}>{title}</li>
))}
</ul>
</main>
);
}
TSXこの時点では、機能としては全く問題ありません。
でも、ページがもっと複雑になってくると、LessonsPage の中がどんどん長くなっていきます。
そこで、「リスト表示部分だけ」を別コンポーネントに切り出します。
リスト表示を別コンポーネントにする
まずは「ページの中に定義するだけ」の分割をしてみます。
ファイルは同じ app/lessons/page.tsx のままで OK です。
"use client";
const lessons = [
"Reactの基本",
"Next.jsのページとルーティング",
"useStateとイベント処理",
];
function LessonList() {
return (
<ul style={{ marginTop: "12px" }}>
{lessons.map((title) => (
<li key={title}>{title}</li>
))}
</ul>
);
}
export default function LessonsPage() {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h1>講座一覧</h1>
<LessonList />
</main>
);
}
TSXここでやったことはシンプルです。
LessonList というコンポーネントを作り、その中に ul と map の処理を移した。
ページ側は <LessonList /> を呼び出すだけにして、スッキリさせた。
この時点では LessonList が外部の lessons 変数を直接参照しているため、まだ「完全な汎用コンポーネント」とは言えませんが、
「ページの責務」と「リスト表示の責務」が分かれただけでもだいぶ読みやすくなっています。
挑戦課題 propsを使って汎用化
「このコンポーネントは何をもらえれば仕事できるか」を考える
次のステップは、「リスト表示コンポーネントに必要な情報を props で渡す」ことです。
今の LessonList はファイルの外側にある lessons に依存しているので、他のページでは使いにくい状態です。
ここで一度、問い直します。
LessonList は、何を渡してもらえれば仕事ができるコンポーネントか。
答えはシンプルで、「表示したい文字列の配列」があれば仕事ができます。
なので、LessonList の props として「配列」を受け取るようにします。
文字列配列を受け取る汎用リストにする
まずは「文字列の配列」を表示する汎用リストコンポーネントにしてみましょう。
"use client";
type StringListProps = {
items: string[];
};
function StringList({ items }: StringListProps) {
return (
<ul style={{ marginTop: "12px" }}>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
const lessons = [
"Reactの基本",
"Next.jsのページとルーティング",
"useStateとイベント処理",
];
export default function LessonsPage() {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h1>講座一覧</h1>
<StringList items={lessons} />
</main>
);
}
TSXここでの変更点を整理します。
StringListProps 型で「props に items: string[] を受け取る」と定義した。
StringList コンポーネントは items を受け取り、それを map して li に変換している。
ページ側では <StringList items={lessons} /> と書いて、配列を渡している。
これで StringList は、「どんな文字列配列でも表示できる汎用リスト」として他のページからも使い回せるようになりました。
オブジェクト配列を受け取るリストの汎用化イメージ
実務寄りにするなら、「オブジェクトの配列」を受け取って表示するコンポーネントもよく使います。
すこしだけレベルを上げて、Lesson 型の配列を受け取るコンポーネントも考えてみましょう。
type Lesson = {
id: number;
title: string;
};
type LessonListProps = {
lessons: Lesson[];
};
function LessonList({ lessons }: LessonListProps) {
return (
<ul style={{ marginTop: "12px" }}>
{lessons.map((lesson) => (
<li key={lesson.id}>{lesson.title}</li>
))}
</ul>
);
}
const lessonsData: Lesson[] = [
{ id: 1, title: "Reactの基本" },
{ id: 2, title: "Next.js入門" },
{ id: 3, title: "状態管理の基礎" },
];
export default function LessonsPage() {
return (
<main style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h1>講座一覧</h1>
<LessonList lessons={lessonsData} />
</main>
);
}
TSXここでの大事なポイントは、次の通りです。
「リスト表示の責務」は LessonList に閉じ込めた。
「どんなデータを表示するか」は親コンポーネント(LessonsPage)が決めて、props で渡している。
key は lesson.id にして、配列の中で一意になるものを使っている。
こうしておくと、LessonList を他のページからも呼び出せるし、lessonsData を API から取得したデータに差し替えるのも簡単になります。
コンポーネント分割で意識してほしいこと
最後に、この課題で身につけてほしい感覚をまとめます。
まず、「長くなってきた JSX の一部を、名前付きコンポーネントに切り出す」という第一歩。
次に、「そのコンポーネントが何を渡されれば仕事できるか」を考えて、props として受け取る形にする。
最後に、「データを持つのは親、表示ロジックは子」という役割分担を意識する。
この流れが自然にできるようになると、
一覧部分だけを専用コンポーネントに切り出す
カード表示を再利用できるコンポーネントにする
フォームの入力欄を汎用コンポーネントにする
といった「実務でよくある分割」が、かなりスムーズにできるようになります。
もし余力があれば、今まで作った「講座リスト」や「TODOリスト」を題材にして、
リスト部分を専用コンポーネントに切り出す
配列を props で渡す
ところまで一度やってみてください。
