Next.jsで学ぶReact講座(完全初心者向け・30日) | 第5章:仕上げと次の一歩 – 最終アプリ実装

React Next.js
スポンサーリンク

この章でやることの全体像

ここまで「最終アプリ設計」までやってきたので、いよいよそれを実装 → デザイン調整 → 動作確認 → 修正の流れで形にしていきます。
ここでは、前章の「講座管理ミニアプリ」(講座一覧+詳細+完了フラグ)を例にしながら、

実装するときの進め方
デザインを整えるときの考え方
動作確認で見るポイント
修正・リファクタリングのコツ

を、コード例付きでかみ砕いて話していきます。

「この通りのアプリを作ること」が目的ではなくて、
アプリを“育てる”流れそのものを体に入れてほしいイメージです。


実装(まずは最低限動くものを作る)

1. データと型を用意する

まずは、前回決めた Lesson 型とダミーデータを実コードにします。
app/lessons/data.ts を作る想定です。

// app/lessons/data.ts
export type LessonLevel = "初級" | "中級" | "上級";

export type Lesson = {
  id: number;
  title: string;
  level: LessonLevel;
  summary: string;
  description: string;
  isCompleted: boolean;
};

export const initialLessons: Lesson[] = [
  {
    id: 1,
    title: "React コンポーネント入門",
    level: "初級",
    summary: "コンポーネントと JSX の基本を学びます。",
    description: "より詳しい説明がここに入ります。",
    isCompleted: false,
  },
  {
    id: 2,
    title: "Next.js ページとルーティング",
    level: "中級",
    summary: "App Router を使ったページ分割を学びます。",
    description: "より詳しい説明がここに入ります。",
    isCompleted: false,
  },
  {
    id: 3,
    title: "状態管理の設計",
    level: "上級",
    summary: "state の持ち方や props 設計を深掘りします。",
    description: "より詳しい説明がここに入ります。",
    isCompleted: false,
  },
];
TypeScript

ここまでで、「アプリの土台となるデータ」が決まりました。

2. 講座一覧ページ /lessons を実装する

次に、一覧ページをクライアントコンポーネントとして作ります。
最初は「完了ボタンなし」で OK です。

// app/lessons/page.tsx
"use client";

import { useState } from "react";
import { initialLessons, type Lesson } from "./data";

export default function LessonsPage() {
  const [lessons, setLessons] = useState<Lesson[]>(initialLessons);

  return (
    <main style={{ maxWidth: "800px", margin: "24px auto", fontFamily: "sans-serif" }}>
      <h1>講座一覧</h1>
      <p>Next.js × React 入門講座の一覧です。</p>

      <section style={{ marginTop: "24px", display: "grid", gap: "16px" }}>
        {lessons.map((lesson) => (
          <article
            key={lesson.id}
            style={{
              backgroundColor: "white",
              padding: "16px 20px",
              borderRadius: "8px",
              boxShadow: "0 1px 3px rgba(15,23,42,0.08)",
            }}
          >
            <h2 style={{ margin: "0 0 4px", fontSize: "18px" }}>{lesson.title}</h2>
            <p style={{ margin: "0 0 8px", fontSize: "13px", color: "#4b5563" }}>
              レベル:{lesson.level}
            </p>
            <p style={{ margin: 0, fontSize: "14px" }}>{lesson.summary}</p>
          </article>
        ))}
      </section>
    </main>
  );
}
TSX

ここまでで、

/lessons にアクセスすると、
ダミーデータがカードとして並ぶ

という「最低限動く一覧」が完成します。

3. 詳細ページ /lessons/[id] を実装する

次は、動的ルートを使った詳細ページです。
URL の id から該当の講座を探して表示します。
まずは「完了ボタンなし」で実装してしまいます。

// app/lessons/[id]/page.tsx
import { notFound } from "next/navigation";
import { initialLessons } from "../data";
import Link from "next/link";

type PageProps = {
  params: { id: string };
};

export default function LessonDetailPage({ params }: PageProps) {
  const lessonId = Number(params.id);
  const lesson = initialLessons.find((l) => l.id === lessonId);

  if (!lesson) {
    notFound();
  }

  return (
    <main style={{ maxWidth: "800px", margin: "24px auto", fontFamily: "sans-serif" }}>
      <h1>{lesson.title}</h1>
      <p style={{ margin: "4px 0 12px", color: "#4b5563" }}>レベル:{lesson.level}</p>
      <p style={{ marginBottom: "16px" }}>{lesson.description}</p>

      <p>
        <Link href="/lessons">講座一覧に戻る</Link>
      </p>
    </main>
  );
}
TSX

これで、

/lessons のデータと同じ initialLessons を使って
/lessons/1/lessons/2 にアクセスすると詳細が出る

という状態になります。

4. 完了フラグの切り替え(状態管理)

ここが一番「状態管理っぽい」部分です。
小さく進めるために、

完了状態を管理するのは /lessons ページだけ
詳細ページの完了表示は「見た目だけ」にしておく

という設計で行きます。

一覧ページに、完了切り替えボタンを追加します。

// app/lessons/page.tsx の一部だけ変更

function toggleCompleted(id: number) {
  setLessons((prev) =>
    prev.map((lesson) =>
      lesson.id === id
        ? { ...lesson, isCompleted: !lesson.isCompleted }
        : lesson
    )
  );
}

return (
  <main /* 省略 */>
    {/* 省略 */}
    <section /* 省略 */>
      {lessons.map((lesson) => (
        <article key={lesson.id} /* 省略 */>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
            <div>
              <h2 /* 省略 */>{lesson.title}</h2>
              <p /* 省略 */>レベル:{lesson.level}</p>
            </div>

            {lesson.isCompleted && (
              <span
                style={{
                  padding: "2px 8px",
                  borderRadius: "9999px",
                  backgroundColor: "#ecfdf5",
                  color: "#15803d",
                  fontSize: "12px",
                }}
              >
                完了
              </span>
            )}
          </div>

          <p style={{ margin: "8px 0 12px", fontSize: "14px" }}>{lesson.summary}</p>

          <div style={{ display: "flex", gap: "8px" }}>
            <button
              onClick={() => toggleCompleted(lesson.id)}
              style={{ fontSize: "12px" }}
            >
              {lesson.isCompleted ? "未完了に戻す" : "完了にする"}
            </button>
          </div>
        </article>
      ))}
    </section>
  </main>
);
TSX

ここで重要なのは、

toggleCompletedsetLessons に「新しい配列」を渡していること
map とスプレッド構文({ ...lesson, isCompleted: !lesson.isCompleted })で、
特定の要素だけを更新していること

です。

これで、「完了フラグのオンオフ」が一覧上で動くようになります。


デザイン調整(“それなり”に見せるコツ)

1. 余白・幅・フォントで 7 割決まる

細かい CSS のテクニックよりも、

ページ幅を制限する(max-width)
上下左右の余白をとる(padding / margin)
フォントを整える

この3つを押さえるだけで、ぐっと見やすくなります。

すでに例のコードでも、

maxWidth: "800px", margin: "24px auto", fontFamily: "sans-serif"

などで「中央寄せ+読みやすいフォント」にしています。

2. カードの影・角丸・背景色

一覧の各カードには、

背景色: 白
角丸: borderRadius
影: boxShadow

を入れると、「カードUI」っぽくなって、情報が視覚的にまとまります。

style={{
  backgroundColor: "white",
  padding: "16px 20px",
  borderRadius: "8px",
  boxShadow: "0 1px 3px rgba(15,23,42,0.08)",
}}
TSX

アプリ全体の背景色を #f3f4f6 のような薄いグレーにすると、カードが浮き上がる感じになります。
これはグローバル CSS(app/globals.css)側でやってもいいです。

3. 完了状態の視覚表現

完了フラグを、単なる文字だけでなく、

小さなピル型バッジ
色(緑系)
タイトルの色や透明度

などで表現してあげると、パッと見で状態が分かります。

例では、

完了 バッジ(淡い緑背景+緑テキスト)
タイトルはそのまま

ですが、一歩進めて、

完了しているカードの背景色を少し薄くする
タイトルをグレーに近づける

なども試してみると、「状態の視覚化」という実務っぽい感覚が養われます。


動作確認(どこを見るべきか)

1. 「仕様どおりか?」をチェックする

ただ「動くかどうか」ではなく、前に決めた仕様と照らし合わせて見ます。

一覧ページで
講座が全件表示されているか
完了・未完了を切り替えられるか
完了バッジが期待通りに出るか

詳細ページで
URL に応じて正しい講座が表示されるか
存在しない id のときにどうなるか(notFound で 404 になるか)

もし「仕様として決めてないけど、こう動いたらいいな」と思う挙動があれば、
それもメモしておくと「次の改善ネタ」になります。

2. 「変なパターン」で壊れないかを見る

良い UX のためには、「ちょっと意地悪な使い方」を自分でしてみるのが大事です。

連続で完了・未完了をすばやく押しまくる
ブラウザの戻る・進むで一覧⇄詳細を行き来する
URL 直打ちで /lessons/999 にアクセスする

こういうときに、

画面が真っ白にならないか
意味の分からないエラーが出ないか

を確認します。

もしエラーが出るなら、それは「ユーザーを守るための修正ポイント」です。


修正(リファクタリングの入り口)

1. 重複・ゴチャつきの気持ち悪さに敏感になる

一通り動くようになったら、コードを少し眺めてみてください。
そのときに、

同じようなスタイルを何度も書いている
長くなりすぎている JSX ブロックがある
1 コンポーネントに責務が詰め込まれている

など、「なんとなく気持ち悪い部分」が見えてくるはずです。

例えば、一覧ページのカード部分は LessonCard コンポーネントに切り出せます。

type LessonCardProps = {
  lesson: Lesson;
  onToggleCompleted: (id: number) => void;
};

function LessonCard({ lesson, onToggleCompleted }: LessonCardProps) {
  return (
    <article /* 省略(さっきのスタイルをそのまま移植) */>
      {/* 中身もそのまま */}
      <button onClick={() => onToggleCompleted(lesson.id)}>
        {lesson.isCompleted ? "未完了に戻す" : "完了にする"}
      </button>
    </article>
  );
}
TSX

そして LessonsPage からはこう呼びます。

{lessons.map((lesson) => (
  <LessonCard
    key={lesson.id}
    lesson={lesson}
    onToggleCompleted={toggleCompleted}
  />
))}
TSX

これだけで、LessonsPage の見通しがかなり良くなります。

2. 怖くない単位で直す

修正のときのポイントは、「小さく直して、小さく確認する」です。

まずはコンポーネント抽出だけ
次にスタイルの共通化だけ
さらに TypeScript の型をちょっと整理する

と、一度にすべてやろうとしないこと。
「この変更をしたあと、ちゃんと元どおり動いているか」を毎回確認できるサイズにするのがコツです。


まとめと“本当の完成”の感覚

この「最終アプリ実装」でやった流れは、かなり実務に近いです。

データと型をまず決めてから、一覧 → 詳細 → 状態更新の順に小さく実装した。
デザインは、幅・余白・カード・バッジといった「効きのいいポイント」から整えた。
動作確認では、仕様どおりかと「変なパターンで壊れないか」を両方チェックした。
最後に、コンポーネント分割や重複削減など「気持ち悪さ」を手がかりに小さく修正した。

本当の意味で「完成」と言えるのは、

自分が書いたアプリを、自分で触って、
「ここはこうしたいな」「ここはこれでいいな」と自分で判断できる状態

になったときです。

もしよければ、この講座の流れをベースにして、
あなた自身のテーマ(メモ、読書ログ、家計簿、習慣トラッカーなど)で
同じ「設計→実装→デザイン→確認→修正」の一周をやってみてください。

その一周を自力で回せたとき、
もうそれは「入門者」ではなく、
自分のアプリを自分の足で前に進められるエンジニアの入り口に立っている、ということです。

タイトルとURLをコピーしました