目次
6部 総合実習
  1. 34.総合実習 — フルスタック Todo アプリの完成
34 章

総合実習 — フルスタック Todo アプリの完成

1 〜 33章のパターンを一つの動作するフルスタックサービスに繋ぎます。RSC・Server Actions・DB・認証・テストまで。

33章で5部(運用・テスト・デプロイ)が締めくくられました。本章は6部(総合実習)の単独章で、1 〜 33章で身につけたすべての道具を一つのフルスタック Todo アプリの中で一つの流れに繋いでみます。

キャップストーンの意味は単純です。道具を個別に身につけたままだと、「それぞれは分かるが、一つのサービスの中でどうかみ合うのかが見えない」段階で止まってしまいます。本章はその空白を埋める章です。コードを最初からすべて書き出すのではなく、各ステップが本書のどの章をどう適用するか を辿るガイドツアー形式で進めます。実際に動作するリポジトリ一式を横に置いて読むと、もっとも効果が大きいです。

既存の react-todo-app 5編の単純なクライアント Todo と本章の違いは明確です。5編は useState と localStorage で単一ユーザーのタスクリストを扱う入門シリーズです。本章は同じドメイン(タスク管理)を持ってきて、DB 永続化、認証済みユーザーごとの分離、RSC + Server Actions、テスト、デプロイ、観測まで、フルスタック一周分に移します。

何を作るのか #

フルスタック Todo アプリ — 認証済みユーザーごとのタスク管理サービスです。機能は次のように決めます。

  • 会員登録 / ログイン(Auth.js v5 — 32章)
  • Todo CRUD(タイトル / 説明 / 期限 / 完了フラグ)
  • タグフィルタ + 検索(URL searchParams 同期)
  • 完了トグル時の楽観的更新(useOptimistic — 28章)
  • 期限間近の通知バッジ
  • ダークモードトグル(テーマ Cookie で SSR フレンドリー)

このひとつのアプリの中に、本書が扱ったほぼすべての道具が入ります。意図的に小さく取りました。ドメイン自体は誰にとっても一目で分かるので、視線は道具のかみ合わせに集中できます。

技術スタック — 本書の基準そのまま #

領域採用
フレームワークNext.js 15(App Router)23章
言語TypeScript 516章
コンポーネントモデルRSC + ‘use client’24章
データフェッチRSC 内で直接 fetch25章
変更Server Actions27章
楽観的更新useOptimistic28章
DBSQLite + Drizzle ORM21章 + 25章
検証zod21章
認証Auth.js v532章
ユニットテストVitest + Testing Library29章
E2EPlaywright30章
パフォーマンスnext/font + Lighthouse CI + Web Vitals31章
ホスティングVercel33章
観測性Sentry + PostHog33章

本書の4 〜 5部で決めた基準をそのまま持ってきました。キャップストーンで新しい道具を導入することはありません。

13ステップの流れ #

全体の作業は次の13ステップで進めます。各ステップが本書のどの章のパターンを適用するかも併記しています。

13ステップ
 1. プロジェクトセットアップ      (2章 + 16章)
 2. DB モデルとスキーマ           (21章 + 25章)
 3. 認証                          (32章)
 4. レイアウトとルーティング      (23章)
 5. Todo リスト — RSC             (24章 + 25章)
 6. Todo 追加 — Server Action     (27章)
 7. 完了トグル — Optimistic UI    (28章)
 8. 検索とタグフィルタ            (10章 + 21章)
 9. ダークモード                  (12章)
10. コンポーネントテスト          (29章)
11. E2E テスト                    (30章)
12. パフォーマンス点検            (31章)
13. デプロイと観測                (33章)

各ステップは意図的に小さく取ってあります。1ステップはおおよそ1 PR 分の分量です。

1. プロジェクトセットアップ #

Next.js 15 + TypeScript で新しいプロジェクトを始めます。

プロジェクト作成
pnpm create next-app@latest fullstack-todo --typescript --eslint --app --tailwind
cd fullstack-todo

2章(Vite セットアップ)の精神は同じです。道具が強要しない限り最小限のデフォルト値で始めます。16章(TypeScript セットアップ)で扱った strict: truenoUncheckedIndexedAccess: truetsconfig.json で確認します。

フォルダ構成は次のような形で始めます。

src/
src/
├── app/                    # App Router ルート
│   ├── (auth)/             # 認証ルートグループ
│   ├── (app)/              # ログイン後ルートグループ
│   └── layout.tsx
├── components/             # 再利用コンポーネント
├── db/                     # Drizzle schema と client
├── lib/                    # Server Action、zod スキーマ、ユーティリティ
└── auth.ts                 # Auth.js 設定

ルートグループ (auth)(app) で認証前後を分離します。13章(ルーティング)で扱ったパターンです。

2. DB モデルとスキーマ #

SQLite + Drizzle ORM で始めます。SQLite は単一ファイルなので学習用として摩擦が少なく、Drizzle は TypeScript first なので16 〜 21章で扱った型付けの習慣に自然と繋がります。

src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';

export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  email: text('email').notNull().unique(),
  hashedPassword: text('hashed_password'),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});

export const todos = sqliteTable('todos', {
  id: text('id').primaryKey(),
  userId: text('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  description: text('description'),
  dueAt: integer('due_at', { mode: 'timestamp' }),
  completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
  tags: text('tags', { mode: 'json' }).$type<string[]>().default([]),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});

export const todosRelations = relations(todos, ({ one }) => ({
  user: one(users, { fields: [todos.userId], references: [users.id] }),
}));

export type Todo = typeof todos.$inferSelect;
export type NewTodo = typeof todos.$inferInsert;

$inferSelect / $inferInsert が肝です。DB スキーマから型がそのまま出てくるので、ドメイン型を手で別途書くことはありません。17章(props と children の型付け)で扱った「型は一箇所でだけ定義する」の原則が DB まで繋がります。

zod スキーマは Server Action の入力検証で出会います。

src/lib/schemas.ts
import { z } from 'zod';

export const todoCreateSchema = z.object({
  title: z.string().min(1, 'タイトルは空にできません').max(200),
  description: z.string().max(2000).optional(),
  dueAt: z.coerce.date().optional(),
  tags: z.array(z.string()).default([]),
});

export type TodoCreateInput = z.infer<typeof todoCreateSchema>;

21章(fetch と API の型付け)で扱ったパターンです。フォーム入力 → zod → 検証済みオブジェクト → DB の流れを一行で表現します。

3. 認証 #

Auth.js v5 で GitHub OAuth + credentials の2系統を同時に受け取ります。

src/auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from '@/db/client';
import { verifyPassword } from '@/lib/password';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    GitHub,
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      authorize: async (credentials) => {
        const user = await db.query.users.findFirst({
          where: (u, { eq }) => eq(u.email, credentials.email as string),
        });
        if (!user?.hashedPassword) return null;
        const ok = await verifyPassword(
          credentials.password as string,
          user.hashedPassword,
        );
        return ok ? { id: user.id, email: user.email } : null;
      },
    }),
  ],
  session: { strategy: 'jwt' },
});

32章(認証とセッション)で扱った正確なパターンです。セッションをどこからでも一行で取り出せます。

import { auth } from '@/auth';

const session = await auth();
if (!session?.user) {
  redirect('/login');
}

この一行が、RSC、Route Handler、Server Action のどの段階でも同じ形で動作します。

4. レイアウトとルーティング #

App Router のルートグループで認証前後を分けます。

app/
app/
├── (auth)/
│   ├── login/page.tsx
│   └── signup/page.tsx
├── (app)/
│   ├── layout.tsx          # サイドバー + ユーザーメニュー
│   ├── page.tsx            # Todo リスト(デフォルト)
│   └── todos/
│       ├── new/page.tsx
│       └── [id]/page.tsx
└── layout.tsx              # ルートレイアウト(テーマ / フォント)

(app)/layout.tsx の中でセッションを確認し、未ログインユーザーを /login に送ります。

src/app/(app)/layout.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { Sidebar } from '@/components/sidebar';

export default async function AppLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth();
  if (!session?.user) redirect('/login');

  return (
    <div className="grid grid-cols-[240px_1fr] min-h-screen">
      <Sidebar user={session.user} />
      <main className="p-6">{children}</main>
    </div>
  );
}

23章(App Router)の layout ネストモデルがそのままです。この layout は server component として動くので、セッション確認が一箇所で完結し、sentinel 情報がクライアントに流れ込むことはありません。

5. Todo リスト — RSC #

Todo リストは server component で DB を直接クエリします。fetch も、API ルートも、クライアント状態も経由しません。

src/app/(app)/page.tsx
import { auth } from '@/auth';
import { db } from '@/db/client';
import { todos } from '@/db/schema';
import { eq, desc } from 'drizzle-orm';
import { TodoList } from '@/components/todo-list';

export default async function TodosPage() {
  const session = await auth();
  const items = await db
    .select()
    .from(todos)
    .where(eq(todos.userId, session!.user!.id))
    .orderBy(desc(todos.createdAt));

  return <TodoList items={items} />;
}

24章(Server vs Client Components)で扱った図がそのまま実際のコードになりました。ページコンポーネントは server、リスト UI のインタラクティブな部分だけが client に落ちます。

リストコンポーネントは表示だけを担います。

src/components/todo-list.tsx
import type { Todo } from '@/db/schema';
import { TodoItem } from './todo-item';

export function TodoList({ items }: { items: Todo[] }) {
  if (items.length === 0) {
    return <p className="text-muted">まだタスクがありません。追加してみてください。</p>;
  }
  return (
    <ul className="space-y-2">
      {items.map((t) => (
        <TodoItem key={t.id} todo={t} />
      ))}
    </ul>
  );
}

このコンポーネントは ‘use client’ を付けていません。RSC の合理的なデフォルトです。インタラクティブな部分、つまりトグルボタンだけが TodoItem の中で client に落ちます。

6. Todo 追加 — Server Action #

追加フォームは Server Action 一つの関数で済みます。27章(Server Actions とフォーム)の正確なパターンです。

src/lib/actions.ts
'use server';

import { auth } from '@/auth';
import { db } from '@/db/client';
import { todos } from '@/db/schema';
import { todoCreateSchema } from './schemas';
import { revalidatePath } from 'next/cache';
import { nanoid } from 'nanoid';

export async function createTodo(formData: FormData) {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');

  const parsed = todoCreateSchema.safeParse({
    title: formData.get('title'),
    description: formData.get('description'),
    dueAt: formData.get('dueAt') || undefined,
    tags: formData.getAll('tags'),
  });

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }

  await db.insert(todos).values({
    id: nanoid(),
    userId: session.user.id,
    ...parsed.data,
    createdAt: new Date(),
  });

  revalidatePath('/');
}

フォームコンポーネントは <form action={createTodo}> 一行で繋がります。

src/app/(app)/todos/new/page.tsx
import { createTodo } from '@/lib/actions';

export default function NewTodoPage() {
  return (
    <form action={createTodo} className="space-y-3">
      <input name="title" required className="input" placeholder="タイトル" />
      <textarea name="description" className="input" placeholder="説明(任意)" />
      <input name="dueAt" type="date" className="input" />
      <button type="submit" className="btn">追加</button>
    </form>
  );
}

19章(イベントとフォームの型付け)で扱った FormData の絞り込みパターンが zod と結合することで、型安全な入力検証になりました。API ルート、fetch 呼び出し、クライアント状態がすべて消えています。

7. 完了トグル — Optimistic UI #

完了チェックボックスは応答を待たずに即座に UI が変わるよう、useOptimistic を使います。28章(React 19 新機能)で扱ったフックです。

src/components/todo-item.tsx
'use client';

import { useOptimistic, useTransition } from 'react';
import type { Todo } from '@/db/schema';
import { toggleTodo } from '@/lib/actions';

export function TodoItem({ todo }: { todo: Todo }) {
  const [optimistic, setOptimistic] = useOptimistic(
    todo,
    (state, next: boolean) => ({ ...state, completed: next }),
  );
  const [pending, startTransition] = useTransition();

  return (
    <li className="flex items-center gap-2">
      <input
        type="checkbox"
        checked={optimistic.completed}
        disabled={pending}
        onChange={(e) => {
          const next = e.target.checked;
          startTransition(async () => {
            setOptimistic(next);
            await toggleTodo(todo.id, next);
          });
        }}
      />
      <span className={optimistic.completed ? 'line-through text-muted' : ''}>
        {optimistic.title}
      </span>
    </li>
  );
}

useOptimistic がサーバの応答を待たずに UI を先に変えます。Server Action が失敗して revalidate が走れば自動的に元の状態に戻ります。クライアント状態を別途持つ必要がありません。

8. 検索とタグフィルタ #

検索語とタグフィルタはクライアント状態ではなく、URL searchParams に載せます。リロード / 共有 / 戻るに優しい形です。

src/app/(app)/page.tsx — 拡張
export default async function TodosPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; tag?: string }>;
}) {
  const { q, tag } = await searchParams;
  const session = await auth();

  const items = await db.query.todos.findMany({
    where: (t, { and, eq, like, sql }) => and(
      eq(t.userId, session!.user!.id),
      q ? like(t.title, `%${q}%`) : undefined,
      tag ? sql`json_extract(${t.tags}, '$') LIKE ${'%' + tag + '%'}` : undefined,
    ),
    orderBy: (t, { desc }) => desc(t.createdAt),
  });

  return <TodoList items={items} />;
}

検索入力は client コンポーネントですが、入力値を URL に反映する小さな責任だけを持ちます。

src/components/todo-search.tsx
'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { useTransition } from 'react';

export function TodoSearch() {
  const router = useRouter();
  const params = useSearchParams();
  const [, startTransition] = useTransition();

  return (
    <input
      defaultValue={params.get('q') ?? ''}
      placeholder="検索"
      onChange={(e) => {
        const next = new URLSearchParams(params);
        if (e.target.value) next.set('q', e.target.value);
        else next.delete('q');
        startTransition(() => router.replace(`?${next}`));
      }}
    />
  );
}

10章(条件付きレンダリング)の精神と同じです。真実の出所を一つ決めて、UI はその出所を表示するだけにします。ここでは URL が真実の出所です。

9. ダークモード #

テーマは Cookie に保存します。SSR に優しく、最初のペイントから正しいテーマでレンダリングされるのでちらつきがありません。

src/lib/theme.ts
'use server';

import { cookies } from 'next/headers';

export async function setTheme(theme: 'light' | 'dark') {
  (await cookies()).set('theme', theme, {
    maxAge: 60 * 60 * 24 * 365,
    sameSite: 'lax',
  });
}
src/app/layout.tsx
import { cookies } from 'next/headers';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const theme = (await cookies()).get('theme')?.value ?? 'light';
  return (
    <html lang="ja" data-theme={theme} className={theme}>
      <body>{children}</body>
    </html>
  );
}

12章(条件付きレンダリング)で扱った「状態は一箇所にだけ住む」のバリエーションです。localStorage ではなく Cookie なので、サーバも最初からその状態を知って初回応答を作ります。

10. コンポーネントテスト #

Vitest + Testing Library で核心コンポーネント3 〜 4個を出発点として取ります。29章(Vitest)で扱ったパターンです。

src/components/todo-item.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { TodoItem } from './todo-item';

vi.mock('@/lib/actions', () => ({
  toggleTodo: vi.fn().mockResolvedValue(undefined),
}));

describe('TodoItem', () => {
  it('チェックボックスクリック時に即座に UI が変わる(optimistic)', async () => {
    const user = userEvent.setup();
    render(
      <TodoItem todo={{
        id: '1', userId: 'u', title: '本を買う', description: null,
        dueAt: null, completed: false, tags: [], createdAt: new Date(),
      }} />,
    );

    const checkbox = screen.getByRole('checkbox');
    expect(checkbox).not.toBeChecked();

    await user.click(checkbox);
    expect(checkbox).toBeChecked();
    expect(screen.getByText('本を買う')).toHaveClass('line-through');
  });
});

29章で扱った「ユーザーが見るものをテストする」の標語がそのままです。内部状態ではなく、ユーザーがクリックして画面がどう変わるかを確認します。

最初に取るテスト4個のリストは次のような形が自然です。

  1. TodoItem — optimistic トグル
  2. TodoList — 空リスト時の案内文
  3. TodoSearch — 入力時に URL が更新されるか
  4. lib/schemas — zod 入力検証のエッジケース2 〜 3個

11. E2E テスト #

Playwright で「会員登録 → ログイン → Todo 追加 → 完了」を一つのシナリオとしてまとめます。30章(Playwright)で扱ったパターンです。

e2e/flows/happy-path.spec.ts
import { test, expect } from '@playwright/test';

test('会員登録から Todo 完了まで', async ({ page }) => {
  const email = `user-${Date.now()}@example.com`;

  await page.goto('/signup');
  await page.getByLabel('Email').fill(email);
  await page.getByLabel('Password').fill('correct-horse-battery-staple');
  await page.getByRole('button', { name: '登録' }).click();

  await page.waitForURL('/');
  await page.getByRole('link', { name: '新規 Todo' }).click();
  await page.getByLabel('タイトル').fill('最初のタスク');
  await page.getByRole('button', { name: '追加' }).click();

  await expect(page.getByText('最初のタスク')).toBeVisible();
  await page.getByRole('checkbox').click();
  await expect(page.getByText('最初のタスク')).toHaveClass(/line-through/);
});

この一つのテストが、本章の1 〜 9ステップを一度に検証します。CI の PR プレビューデプロイ上で毎回回せば、production ビルド専用バグまで捕まえられます(33章で扱ったパターンです)。

12. パフォーマンス点検 #

31章(Web Vitals)の道具で早めに一度、リリース直前にもう一度点検します。

Lighthouse CI
pnpm dlx @lhci/cli@latest autorun --upload.target=temporary-public-storage

本アプリの初回ペイントは、RSC が DB まで一度に切って応答する形なので LCP が自然に良くなります。INP は useOptimistic のおかげでトグル即反応です。31章の lab データを最初のリリース時点で一度キャプチャしておけば、以後の回帰と比較できます。

production の実ユーザー分布は33章で扱った PostHog で受け取ります。lab と実ユーザー環境の差は小さくありません。

13. デプロイと観測 #

Vercel に一度に乗せます。33章の手順をそのまま辿ります。

デプロイの流れ
1. GitHub repo push
2. Vercel: New Project → repo 選択
3. 環境変数入力:
   - DATABASE_URL(Turso / Cloudflare D1 / Vercel Postgres から選択)
   - AUTH_SECRET
   - AUTH_GITHUB_ID / AUTH_GITHUB_SECRET
   - NEXT_PUBLIC_POSTHOG_KEY / NEXT_PUBLIC_POSTHOG_HOST
   - NEXT_PUBLIC_SENTRY_DSN
4. Deploy

SQLite はローカル学習用としては良いですが、production では単一ファイルが限界になります。本キャップストーンは Turso(libSQL)で切り替えるのがもっともなめらかです。SQLite 互換なのでコードをほぼ変えずに移せます。

Sentry と PostHog は wizard 一行ずつでセットアップされ、最初の24時間で新しいエラーが流れるか、autocapture funnel が描かれるかをまず確認します。

本書全体を一つの表に #

本キャップストーンがどこでどの章を適用したかを1ページに整理しておきます。

ステップ本章での適用元の章
プロジェクトセットアップNext.js + TS strict2章 / 16章
DB スキーマDrizzle $inferSelect でドメイン型17章
zod 検証フォーム入力 → server 検証21章
認証Auth.js v5 GitHub + credentials32章
レイアウトApp Router ルートグループ23章
Todo リストRSC 内で DB 直接クエリ24章 / 25章
Todo 追加Server Action + revalidatePath27章
トグルuseOptimistic + useTransition28章
検索URL searchParams = 真実の出所10章 / 13章
ダークモードCookie ベース SSR フレンドリー12章
ユニットテストVitest + Testing Library29章
E2EPlaywright プレビューデプロイ30章
パフォーマンスLighthouse + Web Vitals31章
デプロイVercel + Turso33章
観測Sentry + PostHog33章

本章の中で新しく導入したパターンはありません。すべての道具が本書の中で一度ずつ扱った道具です。キャップストーンの価値はそこにあります。本書を辿ってきた方は本章で新しく学ぶことが無いのが正常 です。

react-todo-app 5編との比較 #

項目react-todo-app 5編本章
ユーザー単一認証済みユーザーごとの分離
保存localStorageDB(SQLite / Turso)
状態useStateRSC + Server Action
ルーティングなし(単一ページ)App Router
JS / 薄い TSDB スキーマ → ドメイン型
変更クライアントのみServer Action
ダークモードlocalStorage(ちらつき)Cookie(SSR フレンドリー)
テストなしVitest + Playwright
デプロイ静的ホスティングVercel + 観測性

5編を終えた方が本章に移ってくると、同じドメインがフルスタックにどう変形するかが一度に見えます。5編はサイトに無料でそのまま残っているので、気軽に比較しながら読めます。

自分で試す — 一周を最後まで #

本書最後の実習です。本章の13ステップをご自身の手で最後まで一度回してみてください。一度で完成させなくても構いません。ステップごとに PR 1個ずつ、1 〜 2週間かけてゆっくり積み上げても良いです。

  1. 1 〜 3ステップ — プロジェクト / DB / 認証セットアップ。ローカルで会員登録 + ログインが動くのを見る。
  2. 4 〜 7ステップ — レイアウトと Todo CRUD + optimistic トグル。1ユーザーとして手で書きながら UX を整える。
  3. 8 〜 9ステップ — 検索 / タグ / ダークモード。
  4. 10 〜 11ステップ — Vitest 4個 + Playwright 1シナリオ。CI を GitHub Actions で繋ぐ。
  5. 12 〜 13ステップ — Lighthouse 初回計測 → Vercel デプロイ → Sentry / PostHog セットアップ。

ここまで終えると、本書のすべての道具がご自分の手で一度ずつ動作した状態になります。これが本書が約束した目標地点です。

練習問題 #

  1. server vs client の分担。本キャップストーンの次のコンポーネントが server か client か答え、その理由を書いてください。(a) TodoList、(b) TodoItem、(c) TodoSearch、(d) RootLayout、(e) 新規 Todo 追加フォームページ。クライアントに落とすべき最小単位を正確に特定するのが要点です。
  2. URL を真実の出所に。本章では検索語とタグをクライアント状態の代わりに URL searchParams に置きました。次の項目はどう扱うか答えてください。(a)「完了済みのみ表示」トグル、(b) ページネーション(現在のページ番号)、(c) 並び替え(作成日 / 期限)、(d) ダークモード。どれが URL フレンドリーで、どれが Cookie または server state が自然かを区別します。
  3. テストの優先順位。本章で最初に取るテスト4個を挙げました。もし時間が半分しか無いなら、どの2つを先に書きますか。その2つを選んだ理由を一行ずつ書いてください。(ヒント:回帰時にユーザーへもっとも大きく見える項目から。)

一行まとめ:キャップストーンは新しい道具を学ぶ段階ではなく、本書のすべての道具が一つのフルスタック Todo アプリの中でどうかみ合うかを確認する段階であり、RSC + Server Action + zod + Drizzle + Auth.js + useOptimistic + Vitest + Playwright + Vercel + Sentry / PostHog が13ステップの PR 分量の流れに自然と繋がる。新しく学ぶことが無いのが本章の正常な状態であり、その地点に到達した方が本書が約束した目標地点に立った方である。

次の章 #

本章で1 〜 34章の本文がすべて締めくくられます。次の 付録 A — 旧 React コードのマイグレーションは、本文が扱わなかった一つの領域、Class component / Pages Router / Redux-only / fetch-on-mount 時代のコードを本書の modern スタイルへ移す手順を一箇所にまとめました。本文ではなく付録である理由は明確です。本書は最初から modern だけを教える本なので、旧コードとのマッピングは本文の流れを乱さないように付録として分離しました。旧コードベースを手にして本書を辿る読者にとっては、付録 A がもっとも先に読む章になります。

X