React 19 系(19.2 / 19.3)と React Compiler を前提にした、実務で役立つベストプラクティスのまとめです。


目次

  1. プロジェクトの始め方
  2. コンポーネント設計
  3. 状態管理
  4. useEffect の正しい使い方
  5. データ取得とサーバー連携
  6. フォームと Actions
  7. パフォーマンス
  8. TypeScript
  9. スタイリング
  10. アクセシビリティ
  11. テスト
  12. ディレクトリ構成
  13. Lint とツール
  14. アンチパターン早見表

1. プロジェクトの始め方

  • Create React App は使わない(非推奨)。
  • フルスタック / SSR が必要なら フレームワーク を使う
    • Next.js(App Router)、React Router(フレームワークモード)、TanStack Start など
  • SPA だけでよいなら Vite(npm create vite@latest)
  • 最初から TypeScript と React Compiler を有効にしておく

2. コンポーネント設計

純粋関数として書く

コンポーネントは「同じ props / state なら同じ JSX を返す」純粋関数にします。レンダー中に外部の変数を書き換えたり、副作用を起こしたりしないこと。

// ❌ レンダー中に外部の値を書き換える
let count = 0;
function Bad() {
  count++;
  return <p>{count}</p>;
}
 
// ✅ 入力(props/state)だけから出力を決める
function Good({ count }: { count: number }) {
  return <p>{count}</p>;
}

小さく、単一責任に

  • 1 コンポーネント = 1 つの関心事
  • JSX が長くなったら、意味のある単位で分割する
  • ただし「再利用されないのに細かく分けすぎる」のも可読性を下げるので注意

合成(Composition)を活用する

props のバケツリレー(prop drilling)や巨大な設定 props を避けるには、children やスロットを使った合成が有効です。

// ❌ 設定 props が増え続ける
<Card title="..." showFooter footerText="..." footerButtonLabel="..." />
 
// ✅ 合成で柔軟に
<Card>
  <Card.Header>タイトル</Card.Header>
  <Card.Body>本文</Card.Body>
  <Card.Footer>
    <Button>OK</Button>
  </Card.Footer>
</Card>

コンポーネントの中でコンポーネントを定義しない

親が再レンダーされるたびに別の型として扱われ、子の state がリセットされ、パフォーマンスも落ちます。

// ❌
function Parent() {
  function Child() { return <div />; }
  return <Child />;
}
 
// ✅ トップレベルで定義する
function Child() { return <div />; }
function Parent() { return <Child />; }

key を正しく使う

  • リストには 安定した一意の ID を key に使う(配列の index は並び替え・削除があると不具合の原因)
  • key を変えると state をリセットできる。リセット目的の useEffect より簡潔
<ProfileForm key={userId} userId={userId} />

React 19 の書き方に合わせる

  • forwardRef は不要。ref は普通の props として受け取れる
  • Context は <MyContext value={...}> と直接書ける(.Provider 不要)
  • <title> / <meta> / <link> はコンポーネント内に書けば <head> に配置される
function Input({ ref, ...props }: React.ComponentProps<"input">) {
  return <input ref={ref} {...props} />;
}

3. 状態管理

state は最小限に

計算で求められる値は state にしない。 レンダー中に計算すれば十分です。

// ❌ 派生値を state で持ち、useEffect で同期
const [fullName, setFullName] = useState("");
useEffect(() => setFullName(`${first} ${last}`), [first, last]);
 
// ✅ レンダー中に計算
const fullName = `${first} ${last}`;

state の置き場所

  • 使う場所のできるだけ近く に置く(コロケーション)
  • 複数のコンポーネントで共有するなら、最も近い共通の親に持ち上げる
  • 状態の種類ごとに適切な道具を選ぶ
状態の種類推奨
ローカル UI 状態(開閉・入力値など)useState / useReducer
サーバーから取得したデータTanStack Query、SWR、Server Components、フレームワークの loader
URL で表現できる状態(検索条件・タブ・ページ番号)URL の search params
フォーム<form> + Actions、React Hook Form など
アプリ全体で共有するクライアント状態Context(低頻度更新)、Zustand / Jotai など

サーバーデータをグローバルストア(Redux 等)に手動でキャッシュするのは避け、専用ライブラリに任せるのが今の主流です。

state の更新はイミュータブルに

// ❌ 直接変更
items.push(newItem);
setItems(items);
 
// ✅ 新しい配列・オブジェクトを作る
setItems([...items, newItem]);
setUser({ ...user, name: "Taro" });

前の値に依存する更新は関数形式で

setCount((c) => c + 1);

複雑な状態遷移は useReducer

関連する複数の state が一緒に変わる場合や、更新ロジックが複雑な場合は useReducer にまとめると見通しが良くなります。

矛盾した状態を作らない

// ❌ isLoading と isError が同時に true になり得る
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
 
// ✅ 1 つのステータスで表現
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");

4. useEffect の正しい使い方

useEffect は「React の外部システムと同期する」ためのもの です。多くのケースで不要です。

useEffect が不要なケース

やりたいこと代わりの方法
props/state から値を計算するレンダー中に計算する
重い計算をキャッシュするReact Compiler に任せる / useMemo
props が変わったら state をリセットkey を変える
ユーザー操作に反応するイベントハンドラに書く
親に変更を通知するイベントハンドラ内で onChange を呼ぶ
データ取得データ取得ライブラリ / Server Components

useEffect を使うべきケース

  • WebSocket、ブラウザ API(IntersectionObserver 等)、サードパーティのウィジェットとの接続
  • DOM の手動操作

クリーンアップを必ず書く

useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]);

依存配列を偽らない

  • 依存配列から値を省略しない(Lint の警告を無視・抑制しない)
  • 「この値の変化では再実行したくない」ロジックは useEffectEvent に切り出す(React 19.2〜)
const onConnected = useEffectEvent(() => {
  showNotification("接続しました", theme); // theme の変化では再接続しない
});
 
useEffect(() => {
  const conn = createConnection(roomId);
  conn.on("connected", onConnected);
  conn.connect();
  return () => conn.disconnect();
}, [roomId]);

外部ストアの購読は useSyncExternalStore

ブラウザの状態や外部ストアを購読するときは、useEffect + useState ではなく useSyncExternalStore を使います。


5. データ取得とサーバー連携

useEffect で fetch しない

自前で useEffect + fetch を書くと、競合状態(race condition)、キャッシュ、重複リクエスト、ローディング・エラー処理などを全部自分で面倒を見ることになります。

推奨される方法:

  1. フレームワークの仕組み(Server Components、loader)
  2. データ取得ライブラリ(TanStack Query、SWR)
  3. どうしても自前で書く場合は AbortController などで競合状態を必ず処理
// TanStack Query の例
const { data, isPending, error } = useQuery({
  queryKey: ["user", userId],
  queryFn: () => fetchUser(userId),
});

Server Components(RSC)を使う場合

  • デフォルトは Server Component、インタラクティブな部分だけ "use client" にする
  • "use client" の境界はできるだけ 末端(葉) に置き、クライアントに送る JS を減らす
  • 秘密情報や DB アクセスはサーバー側に閉じ込める(server-only パッケージで誤インポートを防止)
  • Server Component から Client Component に渡す props はシリアライズ可能な値に限る

Suspense でローディングを宣言的に

<Suspense fallback={<Spinner />}>
  <UserProfile id={id} />
</Suspense>
  • use(promise) で Promise を読み取り、Suspense と組み合わせられる
  • Promise はレンダーのたびに新しく作らず、Server Component やキャッシュ済みのものを渡す

ウォーターフォールを避ける

親で取得 → 子で取得 → 孫で取得… と直列になると遅くなります。独立したデータは並列に取得(Promise.all、ルートレベルでの prefetch など)しましょう。

Error Boundary を置く

想定外のエラーでアプリ全体が真っ白にならないよう、画面やセクション単位で Error Boundary を設置します(react-error-boundary などが便利)。


6. フォームと Actions

React 19 では、フォームの送信処理を Actions で扱えます。

function NameForm() {
  const [error, submitAction, isPending] = useActionState(
    async (_prev: string | null, formData: FormData) => {
      const result = await updateName(formData.get("name") as string);
      return result.error ?? null;
    },
    null,
  );
 
  return (
    <form action={submitAction}>
      <input name="name" />
      <SubmitButton />
      {error && <p role="alert">{error}</p>}
    </form>
  );
}
 
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "送信中…" : "送信"}</button>;
}
  • useActionState:送信結果やエラーを state として扱う
  • useFormStatus:親フォームの送信状態を子コンポーネントから参照
  • useOptimistic:サーバー応答を待たずに UI を楽観的に更新
  • バリデーションは必ずサーバー側でも行う(Zod などでスキーマを共有すると便利)
  • 大規模で複雑なフォームは React Hook Form 等のライブラリも選択肢

7. パフォーマンス

基本方針:まず計測、それから最適化

  • React DevTools の Profiler、ブラウザの Performance パネル(React のトラックが表示される)で計測してから手を入れる
  • 推測で memo を散りばめない

React Compiler を使う

React Compiler(1.0 安定版)が自動でメモ化を行うため、手動の useMemo / useCallback / React.memo はほとんど不要 になります。

  • 新規プロジェクトでは有効化を推奨
  • コンパイラが正しく動くには「Rules of React」(純粋性、Hooks のルール)を守ることが前提
  • 既存コードの手動メモ化は、無理に削除しなくてよい(挙動が変わるリスクがあるため、テストした上で段階的に)

コンパイラなしの場合のメモ化

  • 重い計算 → useMemo
  • memo 化した子コンポーネントに渡す関数 → useCallback
  • 何でもかんでもメモ化しない(コストと可読性の悪化がある)

その他のテクニック

  • コード分割:lazy() + Suspense でルート単位・重いコンポーネント単位に分割
  • 長いリストの仮想化:TanStack Virtual、react-window など
  • 重い更新を後回しに:useTransition、useDeferredValue
  • 非表示の画面の状態を保持:<Activity mode="hidden">(React 19.2〜)でタブ切り替え時などの状態を維持しつつ優先度を下げる
  • state を下に移す:頻繁に変わる state を、それを使う小さなコンポーネントに閉じ込めると再レンダー範囲が狭まる
  • 画像の最適化:適切なサイズ・形式、loading="lazy"

8. TypeScript

type ButtonProps = {
  variant?: "primary" | "secondary";
} & React.ComponentProps<"button">;
 
export function Button({ variant = "primary", ...props }: ButtonProps) {
  return <button data-variant={variant} {...props} />;
}
  • props は type または interface で明示的に型付けする
  • React.FC は必須ではない。普通の関数 + props の型注釈で十分
  • HTML 要素をラップする場合は React.ComponentProps<"button"> などを活用
  • any を避け、unknown + 型ガードを使う
  • 状態のバリエーションは 判別可能なユニオン型 で表現すると、ありえない状態を型で防げる
type State =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };
  • API レスポンスなど外部データは Zod などで実行時にも検証する
  • tsconfig は strict: true を有効に

9. スタイリング

主な選択肢:

手法特徴
Tailwind CSSユーティリティファースト。現在最も広く使われる
CSS Modulesスコープ付き CSS。追加依存なし
ゼロランタイム CSS-in-JS(vanilla-extract、Panda CSS など)型安全、RSC と相性が良い
ランタイム CSS-in-JS(styled-components、Emotion)RSC との相性に注意
  • プロジェクト内で手法を 統一 する
  • UI の土台には shadcn/ui、Radix UI、React Aria などの アクセシブルなヘッドレスコンポーネント を活用すると、品質とスピードを両立しやすい

10. アクセシビリティ

  • セマンティックな HTML を使う(<button>、<nav>、<main>、見出しの階層)
  • <div onClick> でボタンを作らない
  • 画像には alt、フォーム入力には <label> を関連付ける
  • キーボードだけで操作できるようにする(フォーカス順、フォーカスの可視化)
  • モーダルではフォーカストラップと Esc キーでのクローズ
  • id の生成には useId を使う(SSR でも一致する)
  • 色だけで情報を伝えない
  • eslint-plugin-jsx-a11y で機械的なチェックを入れる

11. テスト

方針

  • 実装の詳細ではなく、ユーザーから見た振る舞い をテストする
  • 「テストがユーザーの使い方に近いほど、信頼性が高い」

ツール

種類ツール
単体・コンポーネントテストVitest + React Testing Library
API モックMSW(Mock Service Worker)
E2E テストPlaywright
UI カタログ・ビジュアルテストStorybook
test("送信ボタンを押すと完了メッセージが表示される", async () => {
  const user = userEvent.setup();
  render(<ContactForm />);
 
  await user.type(screen.getByLabelText("名前"), "山田太郎");
  await user.click(screen.getByRole("button", { name: "送信" }));
 
  expect(await screen.findByText("送信しました")).toBeInTheDocument();
});
  • クエリは getByRole / getByLabelText を優先(アクセシビリティの確認にもなる)
  • getByTestId は最後の手段
  • fireEvent より userEvent を使う

12. ディレクトリ構成

機能(feature)単位 で分けるのが、規模が大きくなっても破綻しにくい構成です。

src/
├── app/                 # ルーティング、エントリーポイント、プロバイダー
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api/
│   │   └── index.ts     # 外部に公開するものだけ export
│   └── todos/
├── components/          # 機能横断で使う汎用 UI(Button, Modal など)
├── hooks/               # 汎用カスタムフック
├── lib/                 # ライブラリの設定・ラッパー
└── utils/               # 純粋なユーティリティ関数
  • 関連するファイル(コンポーネント、テスト、スタイル)は近くに置く
  • feature 同士の直接依存は避け、共通化が必要なら上位層に移す
  • ロジックの再利用には カスタムフック を使う(名前は use で始める)

13. Lint とツール

  • ESLint
    • eslint-plugin-react-hooks(Hooks のルール、React Compiler 関連のルールも含む)— 必須
    • eslint-plugin-jsx-a11y
    • @typescript-eslint
  • フォーマッター:Prettier または Biome
  • Strict Mode:開発時は <StrictMode> を有効に。副作用やクリーンアップ漏れを早期に発見できる
  • React DevTools:コンポーネントツリー、props/state、Profiler の確認
  • CI で Lint・型チェック・テストを自動実行する

14. アンチパターン早見表

アンチパターン改善策
派生値を state + useEffect で同期レンダー中に計算
useEffect で fetchデータ取得ライブラリ / RSC / loader
state を直接変更(push、プロパティ代入)新しいオブジェクト・配列を作る
配列の index を key にする安定した ID を使う
コンポーネント内でコンポーネントを定義トップレベルで定義
依存配列の Lint 警告を抑制ロジックを見直す / useEffectEvent
何でも useMemo / useCallbackReact Compiler に任せる、計測してから最適化
巨大なグローバル Context に何でも入れる分割する / 状態をコロケーション
サーバーデータをグローバルストアに手動キャッシュTanStack Query など専用ツール
<div onClick> でボタンを作る<button> を使う
prop drilling が深い合成(children)、Context、構成の見直し
"use client" をルート近くに付ける境界を末端に寄せる

参考リンク

0件のページ