Reactチートシート

中級 | 15分 で読める | 2025.01.10

公式ドキュメント

この記事の要点

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で始まる関数名にして、useStateuseEffectを中で使います。

パフォーマンス最適化

// 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.lazySuspenseコード分割。初期バンドルサイズを削減し、表示速度を改善できます。

よく使うパターン

パターンコード
ローディング状態{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 })} />

参考リソース

関連記事

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る