1. はじめに
-
筆者: Vue (v2/v3) で業務経験あり、転職先が React + TypeScript
-
目的: 「そもそも React って何者?」を Vue の視点 で整理
-
対象:
- Vue で開発してきたけど React にジョブチェンジしたい
- Hooks が多すぎて「何から手を付ける?」となっている人
TL;DR Vue→React へ乗り換えるなら まず 3 つの Hook —
useState
/useEffect
/useReducer
を押さえれば、業務コードの 8 割は読めるようになる。
2. ざっくり比較 – データの流れ編
観点 | Vue | React |
---|---|---|
基本構造 | MVVM / SFC | “UI ライブラリ” (JSX) |
データ → UI | 自動リアクティブ (data ) |
片方向: 親→子 (props) |
算出 / キャッシュ | computed |
useMemo |
副作用 |
watch , mounted
|
useEffect |
複雑な状態管理 | Vuex / Pinia |
useReducer , Context, Redux など |
要点
React は「親 → 子に一方通行」。子→親は「コールバックで通知→親が state 更新」のみ。双方向バインディングは存在しないので、フォームは Controlled Component として書く。
3. useState
— "変わる値" をコンポーネント内で持つ
何者? コンポーネント内に "状態" を作り、値が変わった瞬間に 再レンダリング を発生させる Hook。
いつ使う?
- カウンターやフォーム入力のように ユーザー操作で変わる 値
- タブ開閉、モーダル表示など UI の ON/OFF
- 外部 API 取得結果を一時的に保持するとき(グローバル共有が不要な場合)
使わない例
- コンポーネント外 (URL, ローカルストレージなど) にすでに ソースオブトゥルース※ が存在する値 ←
useState
は不要で props で受け取れば OK
※ソースオブトゥルース (Single Source of Truth):** アプリ全体で「この値はここだけが正なのだ」と決めた唯一の保管場所**。React では state を増やす前に「その値の元データはどこか?」を必ず確認する。
3‑1. シグネチャ
const [state, setState] = useState<型>(初期値);
3‑2. 更新方法 2 パターン
書き方 | いつ使う? | コード例 |
---|---|---|
直接値 setState(newValue)
|
前回の値を参照しない | setName("Ken") |
関数型更新 setState(prev => prev + 1)
|
前回値が必要(連打・非同期) | setCount(prev => prev + 1) |
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(prev => prev + 1)}>
{count} clicks
</button>
);
}
3‑3. オブジェクト State とスプレッド構文(...
)
React は 置き換え更新―すなわち “新しいオブジェクトで丸ごと差し替える” ことで変化を検知します。
スプレッド構文とは?
const user = { name: "A", age: 20 };
const updated = { ...user, age: 21 };
// => { name: "A", age: 21 }
...foo
でオブジェクト(または配列)の中身を“展開”しコピーできます。
Vue と React の違い(リアクティブ更新 vs 置き換え更新)
フレームワーク | 更新例 | 画面に反映? |
---|---|---|
Vue | this.user.age = 21 |
◎ 自動リアクティブ |
React (NG) | user.age = 21; setUser(user); |
× 参照が不変で検知できない |
React (OK) | setUser(prev => ({ ...prev, age: 21 })); |
◎ 新オブジェクトなので再レンダリング |
既存オブジェクトを 直接 変更すると参照が変わらず React は「未変更」と判断します。
実戦パターン
const [user, setUser] = useState({
name: "田中",
age: 25,
email: "tanaka@example.com"
});
const updateAge = () => {
setUser(prev => ({ ...prev, age: prev.age + 1 }));
};
-
...prev
で 既存キーをコピー - 更新したいキーだけあとから上書き
4. useEffect
— 副作用 (fetch / 監視) を 1 か所に集約
何者? DOM へのアクセス / API 通信 / イベント購読など、レンダリング以外の処理(=副作用) を安全に書くための Hook。
いつ使う?
- データ取得 :
fetch
,axios
, GraphQL などの非同期リクエスト- サブスクリプション :
addEventListener
, WebSocket, setInterval など- 外部ライブラリとの連携 : Chart.js や Map SDK の初期化
- DOM 操作 :
document.title
変更、スクロール位置調整使わない例
- 純粋な計算やフィルタリング →
useMemo
/ 通常関数で十分
4‑1. シグネチャ
useEffect(effectFn, deps?);
引数 | 意味 |
---|---|
effectFn |
実行したい副作用。戻り値に “クリーンアップ関数” を返せる |
deps |
依存配列。依存が変わるたび effect を再実行 |
クリーンアップ関数とは?
effectFn
が return した無名関数。コンポーネントが 消えるとき または 次のuseEffect
が走る直前 に React が自動実行します。書き方の型
useEffect(() => { /* 副作用の開始 */ return () => { /* クリーンアップ処理 (後片付け) */ }; }, deps);
何を片付ける?
addEventListener
→removeEventListener
setInterval / setTimeout
→clearInterval / clearTimeout
- WebSocket / SSE →
socket.close()
- 進行中の fetch →
AbortController.abort()
忘れると起こること
- イベントハンドラが重複してパフォーマンス低下
- タイマーが増殖してメモリリーク
- WebSocket が残りっぱなしで接続数上限に達する
借りたものは返す・使ったものは片付ける — React の副作用も同じ発想です。
useEffect(() => { const id = setInterval(log, 1000); // ① 副作用を開始 return () => clearInterval(id); // ② クリーンアップ }, []);
上の例では ❶ マウント時に
setInterval
が走り、❷ アンマウント時にclearInterval
で後始末。依存配列[foo]
がある場合は「foo
が変わる→旧タイマー停止→新タイマー開始」の順に実行される。依存が変わるたび effect を再実行 |
4‑2. 実行タイミングとクリーンアップ
-
初回マウント後(DOM が描画された直後)
-
deps
のいずれかが変わるたび -
Strict Mode(開発時のみ)で 意図的に 2 回 実行
-
なぜ? 予期せぬ副作用(破壊的 API 呼び出し・二重購読など)を検出するため、React 18 以降は 開発モード で
useEffect
を mount → unmount → mount という擬似サイクルでテストする。 - 本番ビルドでは 1 回 だけ。パフォーマンスへの影響はなし。
-
なぜ? 予期せぬ副作用(破壊的 API 呼び出し・二重購読など)を検出するため、React 18 以降は 開発モード で
-
次回実行前 or アンマウント時に クリーンアップ関数 が呼ばれる
よくあるパターン
① データフェッチ
useEffect(() => {
let canceled = false;
(async () => {
const res = await fetch("/api/user");
if (!canceled) setUser(await res.json());
})();
return () => { canceled = true; };
}, []); // マウント時 1 回
② イベント登録 & クリーンアップ
useEffect(() => {
const handler = () => setX(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
4‑3. ESLint exhaustive-deps
で事故防止
- 依存配列に入れ忘れた変数を自動検知
- 無視する場合はコメントより ロジックの分割 を検討
5. useReducer
— 小さな Redux / フォームや複雑ロジックに最適
何者? 複数の
useState
が絡み合って読みにくくなったとき、更新ロジックを 1 箇所(=reducer) に集約できる Hook。いつ使う?
- フォーム入力のステップごとに多くのフィールドを更新
- Undo / Redo のように「変更履歴」を追いたい
setState
連打で "○○ が先に走ったせいで△△が上書き" を防ぎたいポイント: Action を Union 型 で列挙 → コンパイル時にカバー漏れを検出できる。
5‑1. シグネチャ
const [state, dispatch] = useReducer(reducerFn, initialArg, initFn?);
引数 | 意味 |
---|---|
reducerFn |
(state, action) => newState 純粋関数 |
initialArg |
初期 state |
initFn (省略可)
|
遅延初期化したいとき (initialArg)=>initialState
|
reducerFn は「純粋関数 (Pure Function)」
複数のコンポーネントから呼ばれても 同じ入力 (state, action)
に対して常に同じ newState
を返し、副作用を起こさない 関数です。
ワード | 意味 | 例 |
---|---|---|
state | いま画面が持っている値 | { count: 3 } |
action | “こう変えてほしい” 命令オブジェクト | { type: "inc" } |
newState | action 適用後の次の状態 | { count: 4 } |
// 純粋関数のイメージ
const reducer = (state, action) => {
switch (action.type) {
case "inc": return { count: state.count + 1 };
case "dec": return { count: state.count - 1 };
default: return state; // 未知の action は現状維持
}
};
- 副作用ゼロ: fetch/console.log は書かない。
- 入力=出力が決定的 だから タイムトラベル、ユニットテスト が楽。
5‑2. サンプル: 多段フォーム
type State = {
page: 1 | 2 | 3;
form: { name: string; age: number };
};
type Action =
| { type: "next" }
| { type: "prev" }
| { type: "update"; payload: Partial<State["form"]> };
const reducer = (s: State, a: Action): State => {
switch (a.type) {
case "next": return { ...s, page: (s.page + 1) as State["page"] };
case "prev": return { ...s, page: (s.page - 1) as State["page"] };
case "update": return { ...s, form: { ...s.form, ...a.payload } };
}
};
5‑3. DevTools 連携
-
Redux DevTools を
useReducer
でも利用可能:import reduxDevTools from "redux-devtools-extension"
等 - Action 履歴をタイムトラベルで確認=バグ調査が容易
5‑4. Undo / Redo を実装できるワケ
useReducer
は 「状態をどう変換するか」を Action + 純粋関数で記録 するため、
過去・現在・未来のスナップショットを 1 か所 で管理できます。
// ⬇︎ 3 つのスロットに分割
// past : 巻き戻し用スタック
// present: 画面に表示する現在
// future: 先読み(redo)キュー
const initial = { past: [], present: 0, future: [] };
type A = { type: "inc" } | { type: "dec" } | { type: "undo" } | { type: "redo" };
const reducer = (s: typeof initial, a: A) => {
switch (a.type) {
case "inc":
case "dec": {
const next = a.type === "inc" ? s.present + 1 : s.present - 1;
return { past: [...s.past, s.present], present: next, future: [] };
}
case "undo": {
if (!s.past.length) return s;
const previous = s.past[s.past.length - 1];
return { past: s.past.slice(0, -1), present: previous, future: [s.present, ...s.future] };
}
case "redo": {
if (!s.future.length) return s;
const next = s.future[0];
return { past: [...s.past, s.present], present: next, future: s.future.slice(1) };
}
}
};
-
状態遷移を reducer 内だけで完結 → UI 側は
dispatch({ type: "undo" })
するだけ - 過去・未来の履歴 をオブジェクトに保持できるのは「state を直接 mutate しない=不変更新」だからこそ
- DevTools と組み合わせれば タイムトラベルデバッグ が実質 “Undo / Redo” と同義
6. 依存配列チートシート(Vue 対比版)
React – useEffect
|
Vue 等価処理 | 実行タイミング |
---|---|---|
useEffect(() => {}, []) |
mounted() |
マウント時に 1 回だけ |
useEffect(() => {}, [foo]) |
watch: { foo() { … } } |
foo が変わるたび |
useEffect(() => {}) |
updated() |
毎レンダリング(依存配列を省略すると全再実行) |
メモ
- 依存配列を省略したパターンはパフォーマンスとバグの温床。ほぼ使わない。
- Vue の
watch
に{ immediate: true }
を付けた挙動にしたい場合は、useEffect
で初回も走るので追加設定不要。
7. デバッグ&安全運用 Tips デバッグ&安全運用 Tips
- React DevTools: state/props をリアルタイム確認
- Strict Mode: 副作用の二重実行で非同期バグを早期発見
-
ESLint
exhaustive-deps
: 依存配列漏れを自動警告 - カスタム Hook: 重複ロジックを再利用・テスト容易に
8. 2025 年版スターター – Vite 一択
npm create vite@latest my-app -- --template react-ts
cd my-app
npm i
npm run dev
Create React App はメンテモード。Vite or Next.js を推奨。
9. まとめ
- Vue → React の最大ギャップは 双方向バインディングが無いこと。
- まず
useState / useEffect / useReducer
を理解すれば業務コードの大半は読める。 - Strict Mode + ESLint で副作用の落とし穴を防ぐ。
- 物足りなくなったら Context → Redux / Zustand へスケール。
React は関数ファースト。 最初は冗長でも、可読性とテストしやすさで後々効いてきます。
コメント・質問歓迎!
「ここが React で困った」「こう解決した」など気軽にコメントください!