関数コンポーネント — Reactの基本単位
Reactにおける関数コンポーネントは、Propsを受け取りJSXを返す純粋なJavaScript関数です。 React 16.8でHooksが導入されて以降、クラスコンポーネントに代わり関数コンポーネントが標準的な記述方法となりました。
基本構文
関数コンポーネントの最もシンプルな形は、Propsを引数として受け取り、UIを記述するJSXを返す関数です。
// 関数コンポーネントの基本形
function Greeting({ name }: { name: string }) {
return <h1>こんにちは、{name}さん!</h1>;
}
// アロー関数でも記述可能
const Greeting = ({ name }: { name: string }) => (
<h1>こんにちは、{name}さん!</h1>
); Props: 読み取り専用の一方向データフロー
Propsは親コンポーネントから子コンポーネントへ渡される読み取り専用のデータです。 子が親のPropsを変更することはできません。これがReactの「一方向データフロー」を支える仕組みです。
// Propsの型定義と分割代入
interface UserCardProps {
name: string;
role: string;
avatarUrl?: string; // オプショナル
}
function UserCard({ name, role, avatarUrl }: UserCardProps) {
return (
<div className="user-card">
{avatarUrl && <img src={avatarUrl} alt={name} />}
<h2>{name}</h2>
<p>{role}</p>
</div>
);
} children prop
childrenはコンポーネントのタグで囲んだ中身を受け取る特別なPropです。
レイアウトコンポーネントやラッパーコンポーネントの設計に欠かせません。
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// 使用例
<Card title="お知らせ">
<p>この中身がchildrenとして渡されます。</p>
</Card> State Hooks — コンポーネントの記憶
コンポーネントが「記憶」を持つための仕組みがState Hooksです。 レンダリングをまたいで値を保持し、更新時にコンポーネントを再レンダリングします。
useState: 最も基本的なHook
useStateは値とその更新関数のペアを返します。
更新関数が呼ばれると、Reactはそのコンポーネントを再レンダリングします。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 前の値を元に更新する場合は関数形式を使う */}
<button onClick={() => setCount(prev => prev + 1)}>+1 (関数形式)</button>
</div>
);
} useReducer: 複雑なState遷移
複数の値が相互に依存するStateや、遷移ロジックが複雑な場合はuseReducerが適しています。
Reduxと同様のdispatch/actionパターンで、State遷移を明示的に管理できます。
import { useReducer } from 'react';
type State = { count: number; step: number };
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
}
}
function StepCounter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>カウント: {state.count}(ステップ: {state.step})</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
} Effect Hooks — 外部世界との同期
Reactコンポーネントは「レンダリング」という純粋な計算を行いますが、
API呼び出し、DOM操作、タイマーといった副作用(Side Effect)は
useEffectで管理します。
useEffect: 外部システムとの同期
import { useState, useEffect } from 'react';
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
// セットアップ: 接続を確立
const connection = createConnection(roomId);
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
// クリーンアップ: 接続を切断
return () => {
connection.disconnect();
};
}, [roomId]); // roomIdが変わったら再接続
return <ul>{messages.map((m, i) => <li key={i}>{m}</li>)}</ul>;
} 依存配列のルール
flowchart TD
A["useEffect(fn, deps)"] --> B{deps の指定は?}
B -->|"[a, b]"| C["a または b が変わった時に実行"]
B -->|"[]"| D["マウント時に1回だけ実行"]
B -->|省略| E["毎レンダリング後に実行"]
C --> F["クリーンアップ → 再実行"]
D --> G["アンマウント時にクリーンアップ"]
E --> H["毎回クリーンアップ → 再実行"]「useEffectは不要かもしれない」パターン
React公式ドキュメントが強調するように、多くの場面でuseEffectは不要です。 以下のケースではuseEffectを使わないほうがパフォーマンスもコードの可読性も向上します。
- 派生データの計算: Stateやpropsから計算できる値にuseEffectは不要。レンダリング中に直接計算する
- イベントへの応答: ボタンクリック等はイベントハンドラ内で直接処理する
- データ変換: propsの加工は関数の本体で行う
// NG: useEffectで派生データを計算
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(item => item.active));
}, [items]);
// OK: レンダリング中に直接計算(useEffect不要)
const [items, setItems] = useState([]);
const filteredItems = items.filter(item => item.active); 全Hooks一覧
React 19時点で提供されるすべての組み込みHooksを用途別に整理します。
| カテゴリ | Hook | 用途 |
|---|---|---|
| State | useState | 基本的な状態管理 |
| State | useReducer | 複雑な状態遷移ロジック |
| Context | useContext | コンテキストの値を読み取る |
| Ref | useRef | レンダリングに不要な値の保持・DOM参照 |
| Ref | useImperativeHandle | 親に公開するref handleのカスタマイズ |
| Effect | useEffect | 外部システムとの同期 |
| Effect | useLayoutEffect | ブラウザ描画前に同期的に実行 |
| Effect | useInsertionEffect | CSS-in-JSライブラリ用(DOM挿入前) |
| Performance | useMemo | 計算結果のメモ化 |
| Performance | useCallback | 関数参照のメモ化 |
| Performance | useTransition | UI更新の優先度を下げる |
| Performance | useDeferredValue | 値の更新を遅延させる |
| その他 | useId | SSR安全なユニークID生成 |
| その他 | useSyncExternalStore | 外部ストアの購読 |
| その他 | useDebugValue | DevToolsでのカスタムHookラベル |
| React 19 | use | PromiseやContextの値を読み取る |
| React 19 | useActionState | フォームActionの状態管理 |
| React 19 | useFormStatus | 親フォームの送信状態を取得 |
| React 19 | useOptimistic | 楽観的UI更新 |
カスタムHook — ロジックの再利用
カスタムHookはuseプレフィックスで始まる関数で、
複数の組み込みHooksを組み合わせてロジックを再利用可能にします。
UIを返すのではなく、ステートフルなロジックを共有する仕組みです。
実例: useLocalStorage
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
// 初期値をlocalStorageから読み取り
const [value, setValue] = useState<T>(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
// 値が変わるたびにlocalStorageへ保存
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// localStorageが使えない場合は無視
}
}, [key, value]);
return [value, setValue] as const;
}
// 使用例
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'dark');
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
現在: {theme}
</button>
);
} 初心者がハマるポイント
Stale Closure(古いクロージャ)
useEffectやイベントハンドラ内のコールバックは、定義時のStateの値をキャプチャします。 タイマーやサブスクリプション内で最新の値が取得できない問題が「Stale Closure」です。
// NG: 3秒後に表示されるのは常にクリック時のcountの値
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
// このcountはクリック時の値(古い値)
alert('count: ' + count);
}, 3000);
};
return <button onClick={handleClick}>count: {count}</button>;
}
// OK: useRefで最新値を参照する
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count; // 毎レンダリングで最新値を反映
const handleClick = () => {
setTimeout(() => {
alert('count: ' + countRef.current); // 常に最新値
}, 3000);
};
return <button onClick={handleClick}>count: {count}</button>;
} useEffectの無限ループ
useEffect内でStateを更新し、そのStateが依存配列に含まれていると無限ループが発生します。 また、オブジェクトや配列を依存配列に入れると、参照が毎レンダリングで変わるため意図せずループします。
// NG: 無限ループ
useEffect(() => {
setCount(count + 1); // State更新 → 再レンダリング → Effect再実行 → ...
}, [count]);
// NG: オブジェクトを依存配列に(毎回新しい参照)
const options = { method: 'GET' };
useEffect(() => {
fetch('/api', options);
}, [options]); // optionsは毎レンダリングで新しいオブジェクト
// OK: useMemoで参照を安定化
const options = useMemo(() => ({ method: 'GET' }), []);
useEffect(() => {
fetch('/api', options);
}, [options]); Strict Modeの二重実行
開発環境ではReactのStrict Modeにより、useEffectが2回実行されます。
これはクリーンアップ関数が正しく実装されているかを検証するための意図的な動作です。
本番ビルドでは1回のみ実行されるため、クリーンアップを適切に書くことが解決策です。
理解度チェック
useEffectの依存配列を空配列 [] にした場合、Effectはいつ実行されますか?
キーボード: 1〜4 で選択、Enter で回答