この記事の要点
• useStateで状態管理、useEffectで副作用処理が基本
• useMemo/useCallbackでパフォーマンス最適化
• カスタムHooksでロジックを再利用可能にする
基本的なHooks
useState
// 基本
const [count, setCount] = useState(0);
// オブジェクト
const [user, setUser] = useState({ name: "", email: "" });
setUser(prev => ({ ...prev, name: "Alice" }));
// 関数で初期化(重い計算)
const [data, setData] = useState(() => expensiveComputation());
// 型指定
const [items, setItems] = useState<string[]>([]);
useEffect
// マウント時のみ
useEffect(() => {
console.log("Mounted");
}, []);
// 依存配列の値が変わるたび
useEffect(() => {
console.log("Count changed:", count);
}, [count]);
// クリーンアップ
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
// 非同期処理
useEffect(() => {
const fetchData = async () => {
const data = await fetch("/api/data");
setData(await data.json());
};
fetchData();
}, []);
useRef
// DOM参照
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
// 値の保持(再レンダリングしない)
const countRef = useRef(0);
countRef.current += 1;
// 前の値を保持
const prevValueRef = useRef(value);
useEffect(() => {
prevValueRef.current = value;
}, [value]);
注意: useEffectの依存配列を空([])にするとマウント時のみ実行。依存配列を忘れると毎レンダリングで実行されるので注意しましょう。
useMemo / useCallback
// 計算結果のメモ化
const expensiveValue = useMemo(() => {
return items.filter(item => item.active).map(item => item.value);
}, [items]);
// 関数のメモ化
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
// 子コンポーネントへの関数渡し
<ChildComponent onClick={handleClick} />
ポイント: useMemoは計算結果のメモ化、useCallbackは関数のメモ化。子コンポーネントへ関数を渡す場合はuseCallbackで不要な再レンダリングを防ぎます。
useContext
// コンテキスト作成
const ThemeContext = createContext<Theme>("light");
// プロバイダー
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
// 使用
const theme = useContext(ThemeContext);
useReducer
type State = { count: number };
type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "increment" });
コンポーネントパターン
Props
// 基本
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// children
interface CardProps {
children: React.ReactNode;
title?: string;
}
function Card({ children, title }: CardProps) {
return (
<div className="card">
{title && <h2>{title}</h2>}
{children}
</div>
);
}
条件付きレンダリング
| パターン | コード |
|---|---|
| &&演算子 | {isLoggedIn && <UserMenu />} |
| 三項演算子 | {isLoading ? <Spinner /> : <Content />} |
| 早期リターン | if (isLoading) return <Spinner />;if (error) return <Error message={error} />;return <Content data={data} />; |
リスト
| パターン | コード |
|---|---|
| map | {items.map(item => (<ListItem key={item.id} item={item} />))} |
| インデックスをキー(非推奨) | {items.map((item, index) => (<ListItem key={index} item={item} />))} |
| フラグメント | {items.map(item => (<Fragment key={item.id}><dt>{item.term}</dt><dd>{item.description}</dd></Fragment>))} |
イベントハンドリング
| イベント | コード |
|---|---|
| クリック | <button onClick={(e) => handleClick(e)}>Click</button> |
| フォーム | <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}> |
| 入力 | <input value={value} onChange={(e) => setValue(e.target.value)} /> |
| キーボード | <input onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }} /> |
実践メモ: リストのレンダリングではkeyに一意なIDを使いましょう。インデックスをkeyに使うと、要素の追加・削除で不要な再レンダリングが発生します。
フォーム
制御コンポーネント
function Form() {
const [formData, setFormData] = useState({
name: "",
email: "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">送信</button>
</form>
);
}
React Hook Form
import { useForm } from "react-hook-form";
interface FormData {
name: string;
email: string;
}
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: "名前は必須です" })} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register("email", {
required: "メールは必須です",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "無効なメールアドレスです"
}
})} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">送信</button>
</form>
);
}
注意: オブジェクトのstate更新はスプレッド構文でイミュータブルに。setUser(prev => ({...prev, name: "Alice"}))のように、直接変更(ミューテーション)は避けましょう。
カスタムHooks
// フェッチ
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// ローカルストレージ
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// デバウンス
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
ポイント: ロジックの再利用はカスタムHooksで実現。useで始まる関数名にして、useStateやuseEffectを中で使います。
パフォーマンス最適化
// React.memo
const MemoizedComponent = React.memo(function Component({ value }: Props) {
return <div>{value}</div>;
});
// カスタム比較関数
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
return prevProps.id === nextProps.id;
});
// lazy loading
const LazyComponent = React.lazy(() => import("./HeavyComponent"));
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
実践メモ: React.lazyとSuspenseでコード分割。初期バンドルサイズを削減し、表示速度を改善できます。
よく使うパターン
| パターン | コード |
|---|---|
| ローディング状態 | {loading && <Spinner />}{error && <ErrorMessage error={error} />}{data && <Content data={data} />} |
| 空状態 | {items.length === 0 ? (<EmptyState message="アイテムがありません" />) : (<ItemList items={items} />)} |
| 条件付きクラス | <div className={`card ${isActive ? "active" : ""}`} /><div className={clsx("card", { active: isActive })} /> |
参考リソース
- React Reference - React 公式 API リファレンス
- React Hooks Reference - 全Hooks リファレンス
- Learn React - React 公式チュートリアル
- React DOM API - React DOM リファレンス
関連記事
- React Hooks実践 - Hooks詳細
- React Server Components - RSC
- 状態管理パターン - 状態管理