React開発の基礎を地図にする:コンポーネント設計・フック・状態の置き場で迷わない
Reactでどこにstateを置くか、useEffectをいつ使うか、コンポーネントをどう切るか。設計の判断軸を、コピペで動くコードとClaude Codeでの作り方つきで一本にまとめました。
「Reactって、結局どこに何を書けばいいの?」
これ、僕が一番たくさん聞かれる質問です。チュートリアルは終わった。useStateもuseEffectも名前は知っている。なのに、いざ画面を作り始めると手が止まる。stateをこの部品に置くべきか、親に上げるべきか。useEffectで取ってくるのか、そうじゃないのか。コンポーネントはどこで切るのか。
そして悩んだ末に、全部1つの巨大なコンポーネントに詰め込む。動くけど、3日後の自分が読めない。僕も最初はそうでした。
Reactで詰まる理由のほとんどは、文法じゃなくて設計の判断軸を持っていないことです。この記事は、その判断軸を1枚の地図にします。各論(フックの細かい話、状態管理ライブラリの選び方、テーブルやフォームの実装)は途中でリンクに送るので、まずはここで全体像をつかんでください。
この記事の要点
- Reactの設計は「stateの置き場所」と「コンポーネントの境界」の2つを決めるだけで、9割すっきりする。
useEffectは「外の世界と同期する」ためのもの。propsとstateから計算できる値にEffectは要らない。- 再レンダリングは悪じゃない。遅いと体感したときだけ
useMemoやmemoを足す。先回りしない。 - Claude Codeに頼むときは「作って」より先に、境界・状態・データの形・検証方法の4点を渡すと出力が安定する。
- ライブラリ(Zustand、TanStack Query、React Hook Form)は、まず素のReactで意図を示してから選ぶと事故らない。
まず、Reactの考え方を一行で
Reactは「状態(state)が変わったら、画面をその状態に合わせて描き直す」だけの仕組みです。
ここが他のやり方と一番違うところです。昔のjQueryなら「ボタンが押されたら、この要素のテキストを書き換える」と手で命令しました。Reactは逆で、「いまの状態だと画面はこう見えるはず」という結果を書きます。状態が変わると、Reactが差分を計算して、必要なところだけ描き直してくれる。
だから設計の主役は、見た目(HTML)じゃなくて状態です。「この画面が覚えておくべき値は何か」「それをどこに置くか」をまず考える。これがReact開発の出発点で、ここがズレると後で全部つらくなります。
設計の9割:stateをどこに置くか
stateの置き場所には、だいたい4つの選択肢があります。上から順に検討して、足りなければ下げていくのがコツです。
| 置き場所 | 向いている値 | 具体例 |
|---|---|---|
| 置かない(計算で出す) | propsやstateから導ける値 | フィルタ後の配列、合計金額、表示ラベル |
| 1つの部品の中(ローカル) | その部品しか使わない値 | 開閉フラグ、入力途中のテキスト |
| 親に上げる(リフトアップ) | 兄弟の部品が共有する値 | 選択中のタブ、フィルタ条件 |
| URL / サーバー側 | 共有・再現したい状態 | 検索クエリ、ページ番号、APIのデータ |
一番やりがちな失敗は、1番目をすっ飛ばすことです。「フィルタ後のユーザー一覧」をわざわざuseStateに入れて、useEffectで計算し直す。これ、要りません。元の配列とフィルタ条件があれば、レンダリングのたびに計算すればいいだけです。stateを増やすほど「同期が取れているか」を気にする箇所が増えて、バグの温床になります。
逆に「兄弟どうしで同じ値を見たい」なら、迷わず共通の親に上げる。Reactの公式が「Thinking in React」で説明している考え方そのままで、まず状態の最小単位を探し、それを必要な部品が共有できる一番低い位置に置く。これだけです。
状態が画面をまたいで複雑になってきたら、ライブラリの出番です。ただ「とりあえずRedux」みたいに飛びつくと重くなります。規模と種類で選ぶ判断軸は状態管理ライブラリの使い分けに分けて書いたので、useStateでは苦しいと感じてから読んでください。
コンポーネントはどこで切るか
もう1つの軸が境界です。目安はシンプルで、「propsを一言で説明できるか」。
たとえばUserStatusBadgeは「statusを受け取って色付きラベルを出す」と説明できる。これはいい部品です。一方、UserTableの中に検索ロジック、API呼び出し、編集モーダルの開閉、保存処理まで入っていたら、もう説明が一言で終わりません。それは切りどきのサインです。
切るときは、役割を3つに分けると見通しがよくなります。
- 見た目だけの部品(UI):propsを受け取って表示するだけ。状態を持たない。
- 状態とロジックを持つ部品(コンテナ):データを取り、子に配る。
- 再利用するロジック(カスタムフック):状態や副作用を関数に逃がす。
注意したいのは、切りすぎも失敗だということ。UserNameText、UserEmailText、UserRoleTextみたいに1行ずつ部品化すると、ファイル数だけ増えて責務は何も整理されません。分割は「再利用する」「状態を分けたい」「テストしやすくしたい」のどれかに効くときだけで十分です。「2か所目で使うまで汎用化しない」を口ぐせにしておくと、過剰設計をだいぶ防げます。
useEffectの正しい居場所
ここでフックの話を、地図の範囲だけ整理します。
useStateは「画面が覚えておく値」。useEffectは「Reactの外の世界と同期する」ためのもの。この区別がつくと、フックの9割は迷わなくなります。
「外の世界」というのは、API通信、localStorage、タイマー、ブラウザのイベント購読など、画面を描くこと以外で外部に触る処理のことです。逆に言うと、propsとstateから計算できる値にuseEffectは要りません。これがReactで一番多い間違いで、公式も「You Might Not Need an Effect」というページをわざわざ用意しているくらいです。
迷ったら、この順で考えてみてください。
- propsかstateから計算できる? → レンダリング中にそのまま計算する(Effectなし)。
- ユーザー操作がきっかけ? → イベントハンドラに書く(クリック時にfetchする等)。
- 画面の表示そのものが外部と同期する必要がある? → ここで初めて
useEffect。
3に当てはまるのは、たとえば「この画面を開いている間だけ、サーバーのデータを取って表示する」ようなケースです。下のカスタムフックがその実例になっています。
コピペで動く:取得ロジックをカスタムフックに逃がす
地図だけだと退屈なので、手を動かせる一本を置いておきます。Vite + React + TypeScriptでそのまま動く、データ取得のカスタムフックです。useEffectの「正しい使いどころ」と、後片付け(クリーンアップ)の書き方が一度に分かります。
ポイントは2つ。AbortControllerで古いリクエストを止めること。そしてアンマウント後にstateを触らないこと。この2つを忘れると、画面を素早く切り替えたときに「古い結果が新しい画面を上書きする」地味なバグが出ます。
import { useEffect, useState } from "react";
// 取得状態を1つの値で表す(loading / success / error を取り違えないため)
type FetchState<T> =
| { status: "loading"; data: null; error: null }
| { status: "success"; data: T; error: null }
| { status: "error"; data: null; error: string };
export function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
status: "loading",
data: null,
error: null,
});
useEffect(() => {
// 古いリクエストを途中で打ち切るための「中止ボタン」
const controller = new AbortController();
setState({ status: "loading", data: null, error: null });
async function run() {
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = (await res.json()) as T;
setState({ status: "success", data: json, error: null });
} catch (err) {
// 中止は「失敗」ではないので、何もしないで抜ける
if (err instanceof DOMException && err.name === "AbortError") return;
setState({
status: "error",
data: null,
error: err instanceof Error ? err.message : "不明なエラー",
});
}
}
void run();
// urlが変わる前・画面が消える前に、走っている通信を止める
return () => controller.abort();
}, [url]);
return state;
}
使う側はこうなります。statusで分岐するので、「データはあるのにローディング中」みたいな矛盾した状態が作れません。
type User = { id: string; name: string };
export function UserList() {
const result = useFetch<User[]>("/api/users");
if (result.status === "loading") return <p>読み込み中…</p>;
if (result.status === "error") return <p role="alert">失敗しました:{result.error}</p>;
return (
<ul>
{result.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
これが「Effectの正しい使い方」の型です。実務ではキャッシュや再取得が欲しくなるので、その段階になったらTanStack Queryのキャッシュ設計に乗り換える。自前で書くのは、あくまで「UIと通信を分ける」感覚を体に入れるためです。
再レンダリングは怖くない
「Reactは再レンダリングが多くて遅い」という話をよく聞きますが、最初に言っておくと、再レンダリング自体はほとんどの場合タダ同然です。Reactは差分だけをDOMに反映するので、関数が再実行されても画面がチカチカするわけじゃありません。
なのに先回りでuseMemoやReact.memoを貼りまくると、コードが読みにくくなるだけで、たいてい速くなりません。順番が逆なんです。正しいのはこの順番。
- まずstateを必要な場所に置く(関係ない部品まで巻き込んで再描画しない)。
- リストの
keyを安定したIDにする(indexをkeyにしない)。 - 重い計算や巨大な配列の処理は、本当に遅いと体感してから
useMemoで包む。 - 入力中に重い処理が走るなら、デバウンス(間引き)を入れる。
「なぜ再レンダリングが起きているか」を説明できないままmemoを足すのは、痛くない場所に湿布を貼るようなものです。計測してボトルネックを特定してから直す。この考え方はWebパフォーマンス改善の優先順位に詳しくまとめました。
Claude Codeでの効率的な作り方
ここまでの判断軸を、Claude Codeに丸投げせずに「渡す」のがコツです。「いい感じの管理画面を作って」だけだと、見た目は整っていても状態が散らかったコードが出てきます。僕がやらかしたのもこれで、出てきた巨大コンポーネントを結局ほぼ書き直しました。
いまは依頼の前に、必ず次の4点を文章で渡しています。
| 渡す情報 | 書き方の例 |
|---|---|
| コンポーネントの責務 | 「一覧・検索・編集ダイアログを別部品に分けて」 |
| stateの置き場所 | 「フィルタ条件は親に持たせ、行の開閉だけローカルに」 |
| データの形 | 「User型はこれ。APIレスポンスの形は変えないで」 |
| 検証方法 | 「Testing Libraryでroleとlabelから取得するテストも書いて」 |
そして個人的に一番効いたのが、**「新規作成」より「レビュー依頼」**にすること。既存の差分を読ませて、「コンポーネントが大きすぎないか」「不要なuseEffectはないか」「stateとサーバーのデータが混ざっていないか」を指摘させる。新しいファイルを勝手に増やされにくく、修正範囲が小さく収まります。
チームでやるなら、この4点と禁止事項(過剰な汎用化、placeholderだけのフォーム、indexをkeyにする等)をCLAUDE.mdに書いておくと、毎回同じ説明をせずに済みます。型まわりの土台はTypeScript開発の実践Tips、テストの広げ方はテスト戦略の優先順位が地続きです。
よくある質問
Q. useStateとuseRef、どう使い分けますか?
A. 画面に反映したい値はuseState、反映しなくていい値はuseRefです。たとえば「入力欄のDOM参照」や「前回の値の保持」は再描画が要らないのでuseRef。値を変えたら画面も変わってほしいならuseState。useRefの値を変えても再レンダリングは起きません。
Q. propsをいくつ超えたらコンポーネントを分けるべき? A. 数より「説明できるか」で判断します。propsが3つでも役割がバラバラなら大きすぎだし、6つでも全部が同じ目的なら問題ありません。propsが増えてきたら、まず「オブジェクト1つにまとめられないか」「実は2つの部品が混ざっていないか」を疑ってください。
Q. useEffectの中で取得したデータが、画面切り替えで一瞬古く見えます。
A. クリーンアップ(後片付け)が抜けています。上のコード例のようにAbortControllerで前の通信を止め、return () => controller.abort()を必ず書いてください。これで「古い結果が新しい画面を上書きする」競合が消えます。
Q. 状態管理ライブラリは最初から入れるべき?
A. いいえ。まずuseStateと「親に上げる」で進めて、画面をまたいで状態がこんがらがってから検討すれば十分です。サーバーのデータはそもそもUIの状態と別物なので、状態管理の使い分けで種類を切り分けてから選んでください。
Q. Claude Codeが立派すぎる汎用コンポーネントを作ってしまいます。 A. 「いま必要な抽象化だけ」「2か所目で使うまで汎用化しない」と最初に伝えてください。汎用Table・汎用Modal・汎用FilterEngineを1画面のために作られると、将来の変更が一気に重くなります。
まとめ
Reactの設計は、突き詰めると2つの問いに答えるだけです。「この値はどこに置く?」と「この部品の責務は一言で言える?」。stateの置き場所を上から検討し、計算で出せる値にはEffectを使わず、再レンダリングは遅いと感じてから手を入れる。これだけで、読めるコードがちゃんと書けます。
Claude Codeはその判断軸を渡してあげると一気に頼れる相棒になります。逆に判断軸ごと丸投げすると、立派に見えて運用に弱いコードが返ってきます。まずは小さな1画面で、境界・状態・データの形・検証方法の4点を渡すところから試してみてください。
各論をもっと深掘りしたくなったら、状態管理は状態管理の使い分け、データ取得はTanStack Queryのキャッシュ設計、フォームはreact-hook-form × zodの検証、アクセシビリティはa11y実装の優先順位へ。手を動かす教材や相談先がほしい人は教材一覧も見てみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。