Reactチートシート

中級 | 15分 read | 2025.01.10

基本的な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]);

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} />

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();
  }}
/>

フォーム

制御コンポーネント

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>
  );
}

カスタム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;
}

パフォーマンス最適化

// 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>

よく使うパターン

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

関連記事

← Back to list