15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コンポーネントの設計で最近気をつけていることまとめ(React / Next.js)

Last updated at Posted at 2025-11-28

React / Next.js に慣れてくると、次に悩むのはだいたい「設計レイヤー」まわりですよね。

  • コンポーネントが太りやすい
  • useEffect が増えてロジックが迷子になる
  • 状態をどこに置くか毎回迷う
  • Hooks をどこまで分割すべきか分からない

このあたりのモヤモヤさえ事前に言語化しておくと、

  • コードの読みやすさ
  • 変更時の“怖さ”の小ささ
  • チーム開発でのストレス

がけっこう変わります。

この記事では 2024〜2025 時点の React 19 / Next.js 15(App Router) の設計思想を踏まえつつ、
実務で迷わないための “判断軸” を、例外や補足も含めて整理してみます。

※ Next.js 15 は React 19 と組み合わせるのが公式に推奨されていますが、
ここで紹介する考え方は React 18 ベースの既存プロジェクトにも流用できます。


1. 設計は「UI の変化」から逆算すると一気にラクになる

API や DB スキーマから考え始めたくなりますが、ユーザーが実際に触るのは UI の変化 そのものです。

  • 何を入力したら
  • どの UI が
  • どのタイミングで
  • どう変わるのか

この UI の状態遷移 を最初にざっと書き出すだけで、

  • 必要な state
  • コンポーネントの分割
  • Hooks の切り方

がかなり自然に決まってきます。

ただし、UI 起点にしない方がいいケースもある

例外として、次のようなときは ドメインロジック側を起点 に設計した方が安全です。

  • 金額計算や在庫・ポイントなど、整合性が最優先の EC / 業務 UI
  • すでにシステムの業務モデルやテーブル構成がガチガチに決まっているケース
  • 「この値の整合性を絶対崩せない」という強い制約がある場合

このあたりは 「UI優先」か「ドメイン優先」かをプロジェクトごとに決めておく と、チーム内での認識ズレが減ります。

❌ よくある混乱例(UI状態とドメイン状態をごちゃ混ぜ)

const UserForm = () => {
  const [user, setUser] = useState({ name: "", age: 0 });
  const [isLoading, setIsLoading] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);

  // ...
};

一見シンプルですが、

  • user は「入力中の値」なのか
  • バリデーション済みの「ドメインとして正しい値」なのか
  • API レスポンスをそのまま入れているのか

が曖昧で、だんだんと 「何を信じればいい state なのか」 が分からなくなります。

UI 状態と入力状態を分ける

const [name, setName] = useState("");
const [ageInput, setAgeInput] = useState("");
const [status, setStatus] =
  useState<"idle" | "loading" | "success" | "error">("idle");

そして 送信時にだけドメイン型に変換 します。

const handleSubmit = async () => {
  const age = Number(ageInput);
  if (!Number.isInteger(age) || age < 0) {
    setStatus("error");
    return;
  }

  setStatus("loading");

  await updateUser({ name, age });

  setStatus("success");
};
  • 入力値は 「UI が扱いやすい型」(文字列など)で持つ
  • サーバーに渡す直前で 「ドメインとして妥当な型」 に変換する

という流れにしておくと、フォームが複雑になっても破綻しにくくなります。

React 19 時代のフォーム設計のポイント

React 19 では

  • <form>Actions(useActionState / useFormStatus など)
  • ネイティブフォーム送信とサーバーアクション

がかなり強化されています。

つまり、設計の選択肢はだいたいこの 2 つになります。

  • UI 状態として state で全部持つ
  • DOM(フォーム)に任せて、アクションの結果だけを状態として持つ

フォームが増えてきたら、
「これは React の state で持つのか? / DOM に任せるのか?」 を一度立ち止まって考えるだけでも、
useState だらけのフォームから卒業しやすくなります。


2. 状態設計は「3つの軸 + URL 状態 + グローバル状態」でブレなくなる

状態設計で迷子になりがちなポイントを、次の 3 ステップに分解します。

  1. 状態の “オーナー” を決める
  2. 状態の 種類 を分類する
  3. Derived State(派生状態)を増やしすぎない

そこに URL 状態グローバル状態 を足すと、だいぶブレなくなります。

(1) 状態の “オーナー” を決める

まずはシンプルに、この 2 つだけを考えます。

  • その state一番自然に扱うコンポーネント はどこか?
  • その state を使うコンポーネントの 「最小共通親」 はどこか?

この 2 点だけでも、

  • 「とりあえず親に全部置いて props ドリル…」
  • 「どこからでも触りたいからグローバルに…」

といった “逃げの配置” をだいぶ防げます。

(2) 状態の種類で分類する

ざっくりですが、実務ではこのくらい分けておくと頭が整理しやすいです。

種類
UI 状態 モーダル、タブ、ローディング、トースト
サーバー状態 API データ、キャッシュ、再検証
派生状態(Derived) 絞り込み結果、集計、選択中フラグなど
フォーム状態 入力値、エラー、バリデーション
URL / Router 状態 検索条件、ページ番号、ソート条件
グローバル / セッション ログインユーザー、権限、テーマ

Next.js 15(App Router)だと、特に

  • fetch + キャッシュ(revalidate
  • Server Components / Server Actions

が強力なので、「サーバー状態 = DB の値だけでなく、キャッシュを含めた“読み取りの窓口”」 と捉えると設計しやすいです。

(3) Derived State は「目的のない重複」を避ける

// ❌ 目的のない重複
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);

// ...
setFilteredUsers(users.filter((u) => u.active));

こういう「元データの単なるフィルタ結果」を state にすると、

  • usersfilteredUsers のどちらが正か分からなくなる
  • 更新漏れのバグを生みやすい

などの問題を呼び込みがちです。

// 計算だけで十分なら state にしない
const filteredUsers = users.filter((u) => u.active);

で済むなら、まずは state にしない方が安全 です。

ただし、以下のようなときは state にしてしまった方が素直な場合もあります。

  • 計算コストがかなり重い(かつキャッシュしたい)
  • 無限スクロールやページングと強く同期させたい
  • メトリクス計測上、「今の表示件数」を state として持っておきたい

useMemo についても、

  • 「とにかく高速化のために付ける」ではなく
  • 参照の安定性(=== の変化)を保証したいところ にだけ付ける」

と決めておくと、必要以上に増えなくなります。


3. コンポーネント分割は「責務 + 再レンダリング境界」で切る

昔からある Presentational / Container の考え方は、今でもかなり有効です。

// View(UIだけを担当)
export const UserListView = ({ users }: { users: User[] }) => (
  <ul>
    {users.map((u) => (
      <li key={u.id}>{u.name}</li>
    ))}
  </ul>
);

// Logic(データ取得+フィルタリング)
export const UserList = () => {
  const users = useUsers();
  const filtered = useFilteredUsers(users);

  return <UserListView users={filtered} />;
};

ただし「必ず View と Container に分けるべき」という話ではありません。
むしろ実務では、次の 2 軸で判断した方がしっくりきます。

  1. 責務

    • 再利用したい UI → コンポーネントとして切り出す
    • 再利用したいロジック → Hook に切り出す
    • ページ固有ロジック → ページ内に閉じる
    • 業務ロジック → use-case / service 層などに寄せる
  2. 再レンダリング境界

    • 「ここで state を持つと、この下のコンポーネントツリーが全部再レンダリングされるけど OK?」
    • 「ここはあえて分けておいた方が、再レンダリング範囲を狭くできるよね?」

この 2 軸で見ていくと、

  • なんとなくコンポーネントを増やしすぎる
  • 逆に 1 ファイル 500 行の “なんでも屋コンポーネント” になる

という両極端を避けやすくなります。


4. 副作用は「必要なときだけ Hook へ逃がす」

useEffect まわりは、React 19 になっても引き続きハマりポイントです。

ざっくり分けると次のような方針がおすすめです。

  • 再利用したい副作用 → カスタム Hook に切り出す
  • そのページ専用の副作用 → 無理に外へ出さずページ内に置いて OK

ただし、次のような「ちゃんとテストしたい副作用」は Hook に逃がしておいた方が後々ラクです。

  • ログ送信・トラッキング
  • API 呼び出し(ポーリング、Timer、Observer 系)
  • イベント購読(IntersectionObserver / WebSocket / SSE など)

❌ よくある useEffect の“なんでも置き場”化

React 19 ではレンダリングモデルがよりリッチになったぶん、
「とりあえず useEffect の中でやる」 は、以前にも増して危険になっています。

特に避けたいのはこのあたりです。

  • 単なる計算を useEffect に閉じ込める
  • URL(クエリ)を useEffect で別 state に同期させる
  • フォームのローカル値を useEffect で別 state に写し替える

これらは

  • render → effect → state 更新 → render → ...

というループを作りやすく、バグとパフォーマンス問題の温床になりがちです。

副作用じゃなくて、純粋な計算で書けないか?」を一度立ち止まって考える癖を付けておくと、useEffect の出番はかなり減らせます。


5. Next.js のデータ取得は「まず Server Component」で検討(ただし万能ではない)

App Router の世界観では、

  • 初期表示のデータ取得は Server Component でやる
  • その上で、どうしてもクライアントで持ちたいものだけ Client Component に逃がす

という流れが基本になります。

初期表示データは Server Component が強い

// app/users/[id]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params;
  const user = await getUser(id);

  return <UserPage user={user} />;
}

Server Component によるデータ取得のメリットはざっくりこんな感じです。

  • 自動キャッシュ(revalidate 戦略との組み合わせ)
  • 自動再検証(Stale-While-Revalidate)
  • TTFB の改善(クライアントの useEffect fetch が不要)
  • 初期表示の「チラつき」を減らしやすい

⚠ ただし、Server Component だけでは済まないケースも多い

例えば次のようなケースでは、素直に Client 側で fetch した方がよかったりします。

  • 認証 / Cookie / 権限で出し分ける UI が多い
  • 外部 API で、キャッシュ戦略が取りづらい(一度きりのペイロードなど)
  • 画面上でガンガンインタラクションするユーザー固有データ(リアルタイム性が高いもの)

「なんでも Server Component に寄せる」のではなく、

  • “初期表示に必要な最小限” を Server Component
  • その後の細かいインタラクションやポーリングは Client に任せる

くらいのバランスを意識しておくと、設計も読みやすくなります。


6. 更新は Server Actions がかなり強力(ただし制約も理解しておく)

Next.js 15 では Server Actions が安定してきていて、
「フォームから DB 更新までを 1 本の線で書く」 というスタイルがかなり実用的になっています。

// app/users/actions.ts
"use server";

export async function updateUser(prevState: { ok: boolean }, formData: FormData) {
  const name = formData.get("name");
  await db.user.update({ name });

  return { ok: true };
}
// app/users/page.tsx(Client Component 側)
"use client";

import { useActionState } from "react";
import { updateUser } from "./actions";

export function UserForm() {
  const [state, action, isPending] = useActionState(updateUser, { ok: false });

  return (
    <form action={action}>
      <input name="name" />
      <button disabled={isPending}>更新</button>
      {state.ok && <p>更新しました</p>}
    </form>
  );
}

Server Actions のうまみ

  • pending / error / successuseActionState / useFormStatus で扱いやすい
  • Optimistic UI が書きやすい
  • クッキーやセッションを使った mutate が安全に書ける
  • ネイティブフォームとの相性が良く、JavaScript 未読込でも動く(プログレッシブエンハンスメント)

ただし、制約もそれなりにある

  • Action はサーバーバンドルに含まれるので、肥大化しすぎるとビルドサイズに影響 する
  • ランタイムは基本的に Node.js(Edge Runtime での利用は制限・注意点が多い)
  • 呼び出しは基本的に 1 Action = 1 リクエスト として扱われるイメージ
  • モバイルアプリと API をガッツリ共有したい場合は、
    BFF や OpenAPI ベースの API を別で用意した方が設計しやすい

ざっくりした使い分けイメージ

  • Server Actions

    • Web アプリ内のフォーム送信
    • 権限チェックを絡めた安全な mutate
    • 画面に強く紐づいた更新(UI と一体)
  • Route Handlers(BFF 的 API)

    • モバイルアプリや別サービスとの API 共有
    • Web 以外のクライアントからも叩きたい更新系処理
    • SSE / WebSocket などリアルタイム性の高い仕組み
  • Client fetch(クライアントからの fetch / SWR / TanStack Query 等)

    • クライアント側のキャッシュ戦略を細かく制御したいとき
    • 更新頻度が高く、UI がインタラクティブに変化する場面

7. 判断軸を言語化すると迷わなくなる

ここまでの内容を「迷ったときに思い出すチェックリスト」としてまとめると、だいたいこんな感じになります。

  • UI の変化から設計する

    • 「何をしたら、どの UI が、どう変わるか?」を書き出してから state を決める
  • 状態の“オーナー”を決める

    • その state を一番自然に扱うコンポーネントと、最小共通親を意識する
  • 状態の種類で分類する(URL / グローバルも含めて考える)

    • UI / サーバー / Derived / フォーム / URL / グローバル
  • Derived State は安易に重複しない

    • 計算で済むものは state にしない
    • ただし計測やパフォーマンスのための「意図ある冗長性」は許容する
  • コンポーネントは「責務 + 再レンダリング境界」で分ける

    • 再利用したい UI はコンポーネント化
    • 再利用したいロジックは Hook 化
    • ページ固有ロジックはページに閉じる
  • 副作用は必要なときだけ Hook に逃がす

    • 特にテストしたい副作用(ログ・ポーリング・Observer)は Hook に切り出す
  • 初期データは Server Component をまず検討

    • それでも足りないところだけ Client で fetch する
  • mutate は用途ごとに手段を選ぶ

    • Server Actions / Route Handlers / Client fetch を「誰が叩くか」「どこまで共有したいか」で選択する
  • 机上の設計だけで完結させず、「計測」で再評価する

    • LCP / TTFB / INP などのパフォーマンス指標
    • Sentry や各種トラッキングによるエラー・行動ログ

おわりに

2024〜2025 年は、React 19 と Next.js 15 の登場で

  • UI とロジックの責務分離(Server / Client の役割分担)
  • 「初期表示はサーバー主導」がほぼデフォルトになったこと
  • state を必要最小限にして、「ソース・オブ・トゥルースを 1 つに絞る」流れ
  • mutate 戦略(Server Actions / BFF / Client fetch)の選択肢が整理されてきたこと

など、フロントエンドの設計思想がかなりアップデートされた時期でした。

一方で、「正解の設計」自体はプロジェクトごとに違う のも事実です。

だからこそ、

  • UI 起点で状態遷移を整理する
  • 状態のオーナーと種類を言語化する
  • Server / Client の役割分担をチームで共有する
  • 実際の計測結果を見て設計をアップデートする

といった “判断軸” をチームで共有しておくこと が、
結果的にコンポーネント構造やコード品質を安定させてくれます。

誰かが設計で悩んだときに、

「とりあえずこのチェックリストに沿って一緒に整理してみよう」

と言える、共通の土台 を作る上で参考にしてもらえたらうれしいです。

15
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?