目次
付録
  1. 100.旧 React コードのマイグレーション
100 章

旧 React コードのマイグレーション

Class component / Pages Router / Redux-only / fetch-on-mount のような旧スタイルコードを本書の modern スタイルに移すガイド。

34章で本書の本文がすべて締めくくられました。本付録は本文が意図的に扱わなかった一つの領域、旧 React コードを本書の modern スタイルに移す手順 を一箇所にまとめたものです。

本書の本文1 〜 34章は、最初から一つのスタイルだけを教えます。function component + hooks、App Router、RSC + Server Actions、TypeScript first です。本書の中で旧スタイル(Class component、componentDidMount、Pages Router、Redux-only、fetch-on-mount、PropTypes)はほぼ登場しません。1冊の入門書が2つのスタイルを同時に教えると、誰もどちらも身につけられないという判断です。

それでも、現実のコードベースには旧スタイルが生きています。本付録はそうしたコードに向き合う読者にとって1ページ分の地図になることが目標です。旧 → modern の一行ずつのマッピング大規模コードベースで壊さずに移す手順 の2つを整理します。

旧 React ユーザーの入り口 — 本付録は、旧コードを手にして本書のどこから読み始めれば良いか測るときに、まず立ち寄っていただきたい章です。本文のマッピング表でご自身のコードがどの章と出会うかを確認すれば、本書のどの章から開くと良いかが容易に把握できます。

旧 → 本書のマッピング表 #

旧スタイルでよく見るパターンが、本書のどの章で扱われるかを1ページに整理しておきます。

旧スタイル本書の章
class Foo extends React.Component4章(コンポーネントと props)+ 7章(state)
this.state / this.setState7章(useState)
componentDidMount / componentDidUpdate / componentWillUnmount11章(useEffect)
componentDidCatch11章(ErrorBoundary の節)
HOC / render props12章(useContext)+ 13章(カスタムフック)
pages/foo.tsx(Pages Router)23章(App Router)
getServerSideProps / getStaticProps25章(RSC データフェッチ)
_app.tsx / _document.tsx23章(app/layout.tsx)
next/router23章(next/navigation)
useEffect(() => { fetch(...) })25章(RSC 内で直接 fetch)
Redux store / reducer / saga24章(RSC)+ 27章(Server Actions)+ 12章(Context)
PropTypes.string.isRequired16 〜 17章(TypeScript)
styled-components / emotion本文では断定せず(CSS Modules / Tailwind を使う場合)
fetch レスポンスをそのまま信用21章(fetch と API レスポンスの型付け)

このマッピングが本付録の骨組みです。下の各節で順に解いていきます。

Class component → function + hooks #

もっともよく出会う変換です。1コンポーネント単位で移せるので、段階的マイグレーションの出発点としてもっとも適しています。

this.state + this.setStateuseState #

旧 — Class
class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <button onClick={this.increment}>
        クリック: {this.state.count}
      </button>
    );
  }
}
modern — function + useState
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      クリック: {count}
    </button>
  );
}

肝は setState({ count: this.state.count + 1 }) の落とし穴です。旧コードは this.state.count の stale な値を掴むリスクがあるため、this.setState((prev) => ...) のコールバック形式が推奨されていました。useState では setCount((c) => c + 1) の関数更新が同じ役割を果たします。7章で扱ったパターンです。

componentDidMountuseEffect(() => {...}, []) #

componentDidMount() {
  fetchUser(this.props.userId).then((u) => this.setState({ user: u }));
}
componentDidUpdate(prevProps) {
  if (prevProps.userId !== this.props.userId) {
    fetchUser(this.props.userId).then((u) => this.setState({ user: u }));
  }
}
componentWillUnmount() {
  this.cancelled = true;
}
modern
useEffect(() => {
  let cancelled = false;
  fetchUser(userId).then((u) => {
    if (!cancelled) setUser(u);
  });
  return () => { cancelled = true; };
}, [userId]);

3つのライフサイクルが1つの hook にまとまります。依存配列に userId を入れれば mount / update が1つの流れになり、return した cleanup 関数が unmount + 依存変更時の両方で呼ばれます。11章(useEffect)で扱ったモデルそのままです。

ただし 本書の力点は useEffect 内の fetch を避ける方向 だという点も合わせて覚えておいてください。上の変換は1:1 マッピングに過ぎず、もっと良い住処は RSC の server コンポーネント内での直接 fetch です。本付録の §「fetch-on-mount」の節で再度扱います。

componentDidCatch → ErrorBoundary コンポーネント #

componentDidCatch は興味深いことに 関数型に変換できない唯一のライフサイクル です。React が hook で等価物を提供していません。そのため ErrorBoundary だけは依然として class component として書きます。

ErrorBoundary — 依然として class
class ErrorBoundary extends React.Component<
  { fallback: React.ReactNode; children: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('Caught by boundary:', error, info);
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

本書の modern コードの中でも、ErrorBoundary 1個だけは class として一度書いておけば終わりです。App Router では app/error.tsx がルート単位の ErrorBoundary の役割を自動で果たすので、直接書く場面は減り続ける流れです。

HOC / render props → custom hook #

旧コードの2つのパターンが、modern の custom hook 1つに収束します。

旧 — HOC
const withUser = (Component) => (props) => {
  const [user, setUser] = useState(null);
  useEffect(() => { fetchCurrentUser().then(setUser); }, []);
  return <Component {...props} user={user} />;
};
const Profile = withUser(ProfileBase);
旧 — render props
function UserProvider({ render }) {
  const [user, setUser] = useState(null);
  useEffect(() => { fetchCurrentUser().then(setUser); }, []);
  return render(user);
}
<UserProvider render={(user) => <Profile user={user} />} />
modern — custom hook
function useCurrentUser() {
  const [user, setUser] = useState<User | null>(null);
  useEffect(() => { fetchCurrentUser().then(setUser); }, []);
  return user;
}

function Profile() {
  const user = useCurrentUser();
  // ...
}

14章(custom hook)で扱った正確なパターンです。HOC の wrapper hell も、render props の callback ネストも消えます。

Pages Router → App Router #

旧コードベースのマイグレーションでもっとも大きい領域が、通常ここです。幸い Next.js は /app/pages の共存を公式サポートしているので、一度にすべてを移す必要はありません。

ディレクトリマッピング #

Pages RouterApp Router
pages/index.tsxapp/page.tsx
pages/todos/[id].tsxapp/todos/[id]/page.tsx
pages/_app.tsxapp/layout.tsx(root)
pages/_document.tsxapp/layout.tsx 内の <html> / <body>
pages/api/foo.tsapp/api/foo/route.ts
pages/_error.tsx / pages/404.tsxapp/error.tsx / app/not-found.tsx

もっとも大きな精神的飛躍は ファイル1個 = ルート1個 から フォルダ1個 = ルート1個 + 特殊ファイル群 に変わる部分です。page.tsx / layout.tsx / loading.tsx / error.tsx / not-found.tsx が1つのフォルダの中に一緒に住むモデルです。23章で扱った図そのままです。

getServerSideProps / getStaticProps → RSC 内で直接 fetch #

旧 — getServerSideProps
export async function getServerSideProps(context) {
  const { id } = context.params;
  const todo = await db.todos.findById(id);
  return { props: { todo } };
}

export default function TodoPage({ todo }) {
  return <article>{todo.title}</article>;
}
modern — RSC
export default async function TodoPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const todo = await db.todos.findById(id);
  return <article>{todo.title}</article>;
}

ページコンポーネント自体が async になり、データフェッチコードがコンポーネントの中に入ってきます。別の export 関数が消え、props の直列化境界も消えます。関数が2つから1つに減った が変換のもっとも直感的な記述です。25章で扱ったモデルです。

getStaticProps の静的ビルドは、App Router では fetch の cache: 'force-cache' または export const revalidate = N で表現されます。ISR(Incremental Static Regeneration)も同じオプションのバリエーションです。

_app.tsx / _document.tsxapp/layout.tsx #

グローバル layout がシンプルになります。

旧 — _app.tsx
function MyApp({ Component, pageProps }) {
  return (
    <Provider store={store}>
      <ThemeProvider>
        <Component {...pageProps} />
      </ThemeProvider>
    </Provider>
  );
}
modern — app/layout.tsx
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const theme = (await cookies()).get('theme')?.value ?? 'light';
  return (
    <html lang="ja" data-theme={theme}>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

_app.tsxComponentpageProps を受け取って wrapping するような魔法が消え、ただの React コンポーネント1個になります。server コンポーネントなので、初回応答からテーマ / セッションなどを知った上でレンダリングします。

_document.tsx<Html> / <Head> / <Main> / <NextScript> も消えます。<html> / <body> が root layout の中に普通に書かれます。<head> 内のメタデータは metadata export または generateMetadata 関数に移します。

next/routernext/navigation #

import { useRouter } from 'next/router';

function Foo() {
  const router = useRouter();
  router.push('/login');
  const { id } = router.query;
}
modern
import { useRouter, useParams, useSearchParams } from 'next/navigation';

function Foo() {
  const router = useRouter();
  router.push('/login');
  const params = useParams();
  const searchParams = useSearchParams();
}

もっともよく出会う落とし穴は router.query が消え、2つの hook(useParams / useSearchParams)に分かれた という点です。旧コードは動的ルートパラメータと query string を1つのオブジェクトに混ぜて受け取っていましたが、App Router は2種類を明示的に区別します。

段階的共存 — /app/pages #

Next.js は2つのルーターの共存を公式サポートしています。同じ path の衝突が無ければ、pages/old-page.tsxapp/new-page/page.tsx が1つのビルドの中に共存できます。

ただし落とし穴が2つあります。

  1. client 側 navigation の非互換/pagesnext/link でクリックすると SPA navigation で /app ルートには移動できません。全ページリロードが発生します。移行中はこれを甘受します。
  2. API Route の振る舞いの違い/pages/api/foo.ts/app/api/foo/route.ts は別のモデルです。一度に移すときは、1つの API だけを移すのではなく1ドメイン単位(例:/api/auth/* 全体)で移すのが安全です。

移す順序は通常 leaf ページから layout へ読み取り専用から変更ルートへ の2つの流れで進めます。

Redux-only → RSC + Server Actions + 小型 client store #

旧コードベースの Redux 使用は、通常 すべての状態を store に入れる 形でした。ユーザー情報、サーバデータ、UI 状態が1つの store の中に混ざっています。

modern の出発点は次の問いです。

この状態は server 状態か、client 状態か、そして client 状態なら本当に全域か。この問いをまず投げます。

この3つに分けると、Redux の出番は急速に減ります。

server 状態は RSC + Server Actions へ #

旧コードの半分以上は通常 server 状態です。Todo リスト、ユーザープロフィール、コメント、投稿など。これは store に置くものではなく、サーバが再取得すれば良いデータです。

旧 — Redux
// store/todos.ts
const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], loading: false },
  reducers: {
    setItems: (state, action) => { state.items = action.payload; },
  },
});

// どこかのコンポーネント
useEffect(() => {
  dispatch(fetchTodos());
}, []);
const todos = useSelector((s) => s.todos.items);
modern — RSC
// app/todos/page.tsx
export default async function TodosPage() {
  const items = await db.todos.findAll();
  return <TodoList items={items} />;
}

store、slice、action、selector、useEffect がすべて消えます。サーバが真実の出所 という24章のモデルが、コード量を8 〜 10倍減らします。

本物の client 状態は小さな道具で #

次のあたりが本物の client 状態です。

  • ダークモード(ただし本書は Cookie で SSR フレンドリー)
  • モーダル / ドロップダウンの開閉
  • 多段階フォームの現在のステップ
  • サイドバーの折り畳み / 展開

このような状態は2箇所に入れます。

  1. コンポーネントの近く(useState) — もっとも普通の出発点になります。1画面内でだけ使うなら、ここに置けば良いです。
  2. Context 1個(12章) — 複数のコンポーネントが共有する必要があるなら、小さな Context。

Zustand / Jotai のような小さな store 道具は、Context では足りないとき(特に頻繁な更新で再レンダリングが負担になるとき)に合います。Redux Toolkit の重さは、ほとんどの場合過剰です。

Redux を維持すべきエッジケース #

次の場合は Redux が依然としてもっとも自然です。

  • 複雑な undo / redo が必須(図形エディタ、コードエディタ)
  • タイムトラベルデバッグに見合うだけの複雑度
  • 数十個の非同期 action が明示的な state machine として表現される必要があるドメイン(saga が必要な場合)

上の3つのどれにも該当しなければ、modern stack に移す間に Redux の95%は自然に消えます。残る5%が本当に Redux が必要な領域です。

マイグレーション手順 #

大規模コードベースの一般的な順序を書いておきます。

Redux → modern の順序
1. server 状態の特定(Todo / User / Comment など)後、RSC + Server Action へ移行
2. 薄い client 状態の特定後、useState でコンポーネントの近くへ
3. 本当に全域な client 状態だけを Context(または Zustand)に分離
4. 残った Redux store のサイズ点検 — 小さくなっているはず
5. 小さくなった store も不要なら削除、必要ならそのまま維持

一度に移しません。ドメイン1個ずつ(例:「todos slice だけ RSC へ」)切り出していきます。

fetch-on-mount → RSC data fetching #

function Profile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then((r) => r.json())
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <Spinner />;
  if (error) return <Error />;
  return <article>{user.name}</article>;
}
modern — RSC
import { Suspense } from 'react';

export default function ProfilePage({ params }: { params: Promise<{ id: string }> }) {
  return (
    <Suspense fallback={<Spinner />}>
      <Profile params={params} />
    </Suspense>
  );
}

async function Profile({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const user = await db.users.findById(id);
  return <article>{user.name}</article>;
}

旧コードの useState 3つ、useEffect、fetch、JSON パース、error / loading 状態が、1行の await db.users.findById(id) に縮まります。error は app/error.tsx に、loading は <Suspense fallback> または app/loading.tsx に委譲されます。

依然として client で fetch が必要な場合 #

100% が server に移行するわけではありません。次の場合は client fetch が自然です。

  • リアルタイム — SSE / WebSocket で変化するデータ。
  • mutation 後の即時更新 — ただし本書は Server Action + revalidatePath でほとんど解決されるので、こちらを先に試します。
  • ユーザー動作(スクロール / 検索)に応じた段階的ロード — TanStack Query のような道具の領域。

本書の本文は TanStack Query を扱いませんでした。RSC + Server Actions が90%以上の場合をカバーするからです。残る10%の client fetch が本当に必要な場面で、TanStack Query の価値が活きます。

PropTypes → TypeScript #

旧コードの PropTypes はランタイム検証で、TypeScript はコンパイル時検証です。2つの道具の役割は異なりますが、実際に PropTypes が防いでくれるバグのほぼすべてを TypeScript がより早く捕まえます

import PropTypes from 'prop-types';

function Avatar({ src, size, alt }) {
  return <img src={src} width={size} height={size} alt={alt} />;
}
Avatar.propTypes = {
  src: PropTypes.string.isRequired,
  size: PropTypes.number,
  alt: PropTypes.string,
};
Avatar.defaultProps = { size: 40 };
modern
type AvatarProps = {
  src: string;
  size?: number;
  alt?: string;
};

export function Avatar({ src, size = 40, alt = '' }: AvatarProps) {
  return <img src={src} width={size} height={size} alt={alt} />;
}

デフォルト値が関数引数の分割代入に移ります。defaultProps は React 19 から関数型では deprecated になり、もう推奨されません(28章で扱った変更です)。

codemod の限界 #

react-codemod の PropTypes → TypeScript 変換は出発点を作ってくれます。ただし次の場合は手を入れる必要があります。

  • PropTypes.oneOfType のような union は TS の union に移されますが、意図が明確に自動変換されないことがあります。
  • PropTypes が実際と違っていた部分 — コードでは PropTypes に string と書かれているのに実際は null も流れていた、というケースがよくあります。自動変換後に TS が赤線を引いてくれるので、その赤線を辿って一度ずつ見直すのが変換の本当の価値です。

PropTypes 変換は通常 TypeScript 導入の最後の段階 です。まず新しいコードから TS で書き、既存の PropTypes はしばらく同居させておくのが安全です。

CSS-in-JS — styled-components / emotion #

本書の本文は CSS 道具について断定を置きません。ただし styled-components と emotion は RSC 互換性で摩擦があるという点だけ押さえておきます。

両ライブラリともランタイムスタイル生成に依存するので、server component の中で直接使うのが難しいです。Next.js の App Router の上で使うには 'use client' 境界の中に一度閉じ込める必要があり、これ自体が RSC の価値を下げる方向になります。

modern の一般的な出発点は CSS Modules または Tailwind CSS です。両方ともビルド時にクラス名が決まるので、server / client のどこでも同じ形で動作します。

マイグレーションの一般的な流れは次の2つです。

  1. 段階的 — styled-components が生きているコンポーネントは client component として維持、新しいコード / RSC のコンポーネントだけを CSS Modules / Tailwind に。
  2. 一度に — 自動変換ツール(twin.macro など)または手作業。コンポーネント数が多くないとき。

選択はコード規模と優先順位次第です。本書の本文が断定を置かない理由も同じです。

よく出会う落とし穴 #

マイグレーション中によくぶつかる落とし穴をいくつか整理しておきます。

'use client' の伝染 #

'use client' を1ファイル付けると、その中で import しているすべてのコンポーネントが client として一緒に入ってきます。server コンポーネントの価値(DB 直接クエリ、シークレットの安全)が消えないように、client 境界を小さく保つ ことが大切です。24章で扱ったモデルです。

useEffect 内 fetch の誘惑 #

旧コードを持ってきた直後は、ついそのまま useEffect + fetch を残しておきたくなります。動作はします。ただし RSC の価値が得られません。一度にすべてを移せない場合は leaf から移すのが通常安全な順序 です。

Redux の名残として残った Context #

Redux を取り除く過程で Context に移した部分が、そのまま「全域 store」のように膨れ上がるケースがあります。Context は12章で扱った通り 小さい単位に分離する のが正しい形です。一つの巨大な AppContext は、Redux の小さな複製版になります。

Pages Router の _app.tsx 内 Provider が両側でちらつく #

/app/pages の共存期に ThemeProvider / Redux Provider などが両方のルーターでそれぞれ動作します。ルートが切り替わるときにちらつきや state リセットが起きる場合、通常これが原因です。1つのドメインの中では1つのルーターに統一するのがもっとも安全です。

TypeScript any の累積 #

旧コードを移す間に as any を一時的に差し込んでおくと、その一時が永遠になりがちです。as any ごとに TODO コメント + GitHub issue を一緒に作っておく習慣を推奨します。マイグレーションの終わりに grep で一度に掃除します。

大規模コードベース用のマイグレーション手順 #

数百コンポーネント / 数十ルートのコードベースを本書のスタイルに移す作業は、数日では終わりません。四半期または半期単位の計画になります。次の順序がもっとも事故の少ない流れです。

6段階マイグレーション
1. TypeScript 導入             — 安全網。もっとも先に、もっともゆっくり。
2. Class → function + hooks    — 1コンポーネントずつ。回帰リスクのもっとも少ない変換。
3. App Router の一部導入        — /(marketing) のような隔離された領域から。
4. 段階的 RSC 転換             — 新規ルートはすべて RSC、旧ルートはそのまま維持。
5. Redux 依存の縮小            — server 状態から RSC へ移行。
6. 仕上げ — Pages Router の除去   — 最後。すべてのルートが /app に移った後。

各段階ごとの推奨事項を書いておきます。

1段階 — TypeScript 導入 #

もっとも先に、もっともゆっくり。一度に strict には行きません。allowJs: true + strict: false で始めて .ts / .tsx ファイルを段階的に増やします。16章(TypeScript セットアップ)で扱った出発点です。

最初の tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "ES2022"],
    "allowJs": true,
    "jsx": "preserve",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": false,
    "noUncheckedIndexedAccess": false,
    "skipLibCheck": true
  }
}

strict: true への段階的転換は、コンパイラオプションを1個ずつ点ける形で進めます。noImplicitAny を先に、次に strictNullChecks、そして残りの順が一般的です。

2段階 — Class → function + hooks #

1コンポーネント単位なのでもっとも安全です。上の §「Class component → function + hooks」の節のマッピングをそのまま適用します。1 PR に1 〜 2コンポーネントずつ段階的に変換します。回帰テストが機能しているなら自動変換ツールも検討できますが、通常は手で移す方が小さな整理まで一緒に拾えます。

3段階 — App Router の一部導入 #

すべてのページを移すのではなく、隔離された領域1つ(例:マーケティング / ログイン / 新機能)だけを /app で作ります。2つのルーターの共存モードで安定性を確認します。

4段階 — 段階的 RSC 転換 #

新しく作るページはすべて RSC。既存ページはそのまま維持。RSC の価値(サーバ側データフェッチ、シークレットの安全、初回ペイントの速さ)が1ページで現れれば、他のページのマイグレーション動機が自然と生まれます。

5段階 — Redux 依存の縮小 #

§「Redux-only」の節の手順をそのまま辿ります。ドメイン1個ずつ、server 状態から。

6段階 — Pages Router の除去 #

最後。/pages に残ったルートが0になれば、フォルダを削除し、next.config.ts で一時的に点けておいた互換性フラグを整理します。

マイグレーション中にサイトが壊れない手順 #

各段階の中でサイトが生きたまま変換するには、いくつかの習慣が必要です。

  • PR 単位のサイズを小さく — 1コンポーネント / 1ルート単位。revert が容易であるべきです。
  • feature flag で露出を制御 — 新しいルートは一部のユーザーにだけ先に露出(PostHog flag、環境変数)。33章で扱ったパターンです。
  • 2つのモデルのデータ互換性確認 — Redux も RSC も同じバックエンド API を見ているなら、データ経路の一貫性を PR ごとに確認します。
  • E2E 1シナリオの維持 — 30章の Playwright シナリオがマイグレーション前後で同じ形で回るかを確認。ユーザー動線の回帰をもっとも早く捕まえる安全網です。

自分で試す #

ご自身の手に持っている旧 React コード(あれば)、または1つの小さなサンプルプロジェクトを選んで、次を一度ずつ試してみてください。

  1. Class component を1個選んで function + hooks に変換componentDidMount / componentDidUpdate / componentWillUnmount がコンポーネントにあるものを選ぶと、変換の妙味が活きます。11章の useEffect をもう一度確認。
  2. 1ページ選んで Pages Router → App RoutergetServerSideProps があるページを選んで RSC に移してみてください。props の直列化が消え、関数が1つに減ることを確認します。
  3. Redux slice を1個選んで RSC + Server Action に。server 状態の slice を選んで store を空にし、RSC 内の直接 fetch + Server Action に移します。旧コードの行数と modern コードの行数を一度数えてみてください。
  4. PropTypes 1ファイル → TypeScript。codemod の自動変換後、赤線が引かれた部分だけを一度手で見直します。「PropTypes が実際と違っていた部分」が見えることがあります。

4つすべてを終えれば、ご自分のコードベース全体を移す絵が頭の中に組み上がります。

練習問題 #

  1. ライフサイクルのマッピング。次の旧コードを hook モデルで一行ずつ移してみてください。(a) componentDidMount で fetch + state set、(b) componentDidUpdate で prop 比較後の再 fetch、(c) componentWillUnmount で subscribe 解除。結果のコードの useEffect の依存配列と cleanup 関数がどのように構成されるかを答えます。
  2. server 状態 vs client 状態。次の5つが server 状態か client 状態か、client なら本当に全域かを分類してください。(a) ログイン済みユーザー情報、(b) ダークモード、(c) 現在開いているモーダルの ID、(d) カート項目、(e) 画面幅(レスポンシブ分岐用)。それぞれが本書のどの道具が担うかを一行ずつ書きます。
  3. マイグレーション順序の設計。次の項目を仮想のコードベースでどの順序で移すか答えてください。(a) Class component 200個、(b) Pages Router 50ルート、(c) Redux store 30 slice、(d) PropTypes 100ファイル、(e) styled-components 800コンポーネント。本付録の6段階手順を参考にしつつ、ご自身の優先順位(サービス安定性 vs 迅速な modern 移行)も合わせて書きます。

一行まとめ:旧 React コードは一行ずつのマッピング(Class → function + hooks、Pages → App、Redux server 状態 → RSC + Server Action、fetch-on-mount → RSC fetch、PropTypes → TS)で本書の modern スタイルと1:1で繋がり、大規模コードベースの変換は TypeScript 導入 → function 化 → App Router の一部導入 → 段階的 RSC → Redux 縮小 → Pages Router 除去の6段階がもっとも事故の少ない流れである。1 PR 単位のサイズを小さく、feature flag で露出を制御し、E2E 1シナリオで回帰を捕まえる習慣が、サイトが生きたまま移す安全網になる。

本書の締めくくり #

本付録をもって『モダン React』の本文 + 付録がすべて締めくくられます。

本書が約束したことをもう一度整理します。2026 時点の React 標準(function + hooks、App Router、RSC + Server Actions、TypeScript first)を最初から1つのスタイルで教えること、そして入門からフルスタックまで途切れないカーブを1冊で繋ぐこと。1 〜 34章 + 本付録を終えた方は、この2つの約束が目標地点に到達した場所に立っています。

本書の中で扱わなかった領域(React Native、Remix、TanStack Start、デザインシステム、アニメーション、WebGL)は他の本で扱います。本書がその領域への出発点となったなら、本書の役割は十分に果たされたと言えます。楽しいコーディングを願っています。

X