旧 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.Component | 4章(コンポーネントと props)+ 7章(state) |
this.state / this.setState | 7章(useState) |
componentDidMount / componentDidUpdate / componentWillUnmount | 11章(useEffect) |
componentDidCatch | 11章(ErrorBoundary の節) |
| HOC / render props | 12章(useContext)+ 13章(カスタムフック) |
pages/foo.tsx(Pages Router) | 23章(App Router) |
getServerSideProps / getStaticProps | 25章(RSC データフェッチ) |
_app.tsx / _document.tsx | 23章(app/layout.tsx) |
next/router | 23章(next/navigation) |
useEffect(() => { fetch(...) }) | 25章(RSC 内で直接 fetch) |
| Redux store / reducer / saga | 24章(RSC)+ 27章(Server Actions)+ 12章(Context) |
PropTypes.string.isRequired | 16 〜 17章(TypeScript) |
| styled-components / emotion | 本文では断定せず(CSS Modules / Tailwind を使う場合) |
fetch レスポンスをそのまま信用 | 21章(fetch と API レスポンスの型付け) |
このマッピングが本付録の骨組みです。下の各節で順に解いていきます。
Class component → function + hooks #
もっともよく出会う変換です。1コンポーネント単位で移せるので、段階的マイグレーションの出発点としてもっとも適しています。
this.state + this.setState → useState
#
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>
);
}
}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章で扱ったパターンです。
componentDidMount → useEffect(() => {...}, [])
#
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;
}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 として書きます。
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つに収束します。
const withUser = (Component) => (props) => {
const [user, setUser] = useState(null);
useEffect(() => { fetchCurrentUser().then(setUser); }, []);
return <Component {...props} user={user} />;
};
const Profile = withUser(ProfileBase);function UserProvider({ render }) {
const [user, setUser] = useState(null);
useEffect(() => { fetchCurrentUser().then(setUser); }, []);
return render(user);
}
<UserProvider render={(user) => <Profile user={user} />} />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 Router | App Router |
|---|---|
pages/index.tsx | app/page.tsx |
pages/todos/[id].tsx | app/todos/[id]/page.tsx |
pages/_app.tsx | app/layout.tsx(root) |
pages/_document.tsx | app/layout.tsx 内の <html> / <body> |
pages/api/foo.ts | app/api/foo/route.ts |
pages/_error.tsx / pages/404.tsx | app/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
#
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>;
}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.tsx → app/layout.tsx
#
グローバル layout がシンプルになります。
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
</Provider>
);
}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.tsx が Component と pageProps を受け取って wrapping するような魔法が消え、ただの React コンポーネント1個になります。server コンポーネントなので、初回応答からテーマ / セッションなどを知った上でレンダリングします。
_document.tsx の <Html> / <Head> / <Main> / <NextScript> も消えます。<html> / <body> が root layout の中に普通に書かれます。<head> 内のメタデータは metadata export または generateMetadata 関数に移します。
next/router → next/navigation
#
import { useRouter } from 'next/router';
function Foo() {
const router = useRouter();
router.push('/login');
const { id } = router.query;
}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.tsx と app/new-page/page.tsx が1つのビルドの中に共存できます。
ただし落とし穴が2つあります。
- client 側 navigation の非互換 —
/pagesのnext/linkでクリックすると SPA navigation で/appルートには移動できません。全ページリロードが発生します。移行中はこれを甘受します。 - 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 に置くものではなく、サーバが再取得すれば良いデータです。
// 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);// 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箇所に入れます。
- コンポーネントの近く(
useState) — もっとも普通の出発点になります。1画面内でだけ使うなら、ここに置けば良いです。 - 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 が必要な領域です。
マイグレーション手順 #
大規模コードベースの一般的な順序を書いておきます。
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>;
}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 };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つです。
- 段階的 — styled-components が生きているコンポーネントは client component として維持、新しいコード / RSC のコンポーネントだけを CSS Modules / Tailwind に。
- 一度に — 自動変換ツール(
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 で一度に掃除します。
大規模コードベース用のマイグレーション手順 #
数百コンポーネント / 数十ルートのコードベースを本書のスタイルに移す作業は、数日では終わりません。四半期または半期単位の計画になります。次の順序がもっとも事故の少ない流れです。
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 セットアップ)で扱った出発点です。
{
"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つの小さなサンプルプロジェクトを選んで、次を一度ずつ試してみてください。
- Class component を1個選んで function + hooks に変換。
componentDidMount/componentDidUpdate/componentWillUnmountがコンポーネントにあるものを選ぶと、変換の妙味が活きます。11章の useEffect をもう一度確認。 - 1ページ選んで Pages Router → App Router。
getServerSidePropsがあるページを選んで RSC に移してみてください。props の直列化が消え、関数が1つに減ることを確認します。 - Redux slice を1個選んで RSC + Server Action に。server 状態の slice を選んで store を空にし、RSC 内の直接 fetch + Server Action に移します。旧コードの行数と modern コードの行数を一度数えてみてください。
- PropTypes 1ファイル → TypeScript。codemod の自動変換後、赤線が引かれた部分だけを一度手で見直します。「PropTypes が実際と違っていた部分」が見えることがあります。
4つすべてを終えれば、ご自分のコードベース全体を移す絵が頭の中に組み上がります。
練習問題 #
- ライフサイクルのマッピング。次の旧コードを hook モデルで一行ずつ移してみてください。(a)
componentDidMountで fetch + state set、(b)componentDidUpdateで prop 比較後の再 fetch、(c)componentWillUnmountで subscribe 解除。結果のコードの useEffect の依存配列と cleanup 関数がどのように構成されるかを答えます。 - server 状態 vs client 状態。次の5つが server 状態か client 状態か、client なら本当に全域かを分類してください。(a) ログイン済みユーザー情報、(b) ダークモード、(c) 現在開いているモーダルの ID、(d) カート項目、(e) 画面幅(レスポンシブ分岐用)。それぞれが本書のどの道具が担うかを一行ずつ書きます。
- マイグレーション順序の設計。次の項目を仮想のコードベースでどの順序で移すか答えてください。(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)は他の本で扱います。本書がその領域への出発点となったなら、本書の役割は十分に果たされたと言えます。楽しいコーディングを願っています。