第1回

useStateの完全ガイド:関数型更新・配列・オブジェクトの正しい扱い方

ReactのuseStateを完全解説します。型推論・関数型更新・オブジェクト・配列のイミュータブルな更新方法・複数stateの使い分けまで、実務で使えるパターンを実例付きで学べます。

·14分で読める

このシリーズについて

「Hooks完全解説」は、React Hooksを深く理解するための全12回シリーズです。useStateuseEffectuseContextuseReduceruseRefuseMemouseCallback・カスタムHooks・React 18の新Hooksまでを体系的に解説します。

Hook
第1回(この記事) useState — 完全ガイド
第2〜4回 useEffect — 基本・依存配列・クリーンアップ
第5回 useContext
第6回 useReducer
第7回 useRef
第8回 useMemo
第9回 useCallback
第10回 カスタムHooks
第11回 useTransition / useDeferredValue
第12回 Hooksのルールとよくある間違い
たける
たける React基礎編でuseStateは一通りやりましたけど、「完全ガイド」って何が違うんですか?
りこ
りこ 基礎編では「stateを宣言して更新する」を学んだ。ここでは「正しく更新する」を学ぶ。配列・オブジェクトの更新、連続した更新のバグ、初期化の最適化──実務でハマるポイントが全部ここにある。

useStateの基本構文

const [state, setState] = useState(initialValue);
  • state — 現在の値
  • setState — 値を更新する関数(呼ぶと再レンダリングが起きる)
  • initialValue — 初期値(最初のレンダリング時だけ使われる)

TypeScriptでの型定義

型は初期値から自動推論されます。

const [count, setCount] = useState(0);       // number
const [name, setName] = useState('');         // string
const [isOpen, setIsOpen] = useState(false);  // boolean

型が複雑な場合は明示的に指定します。

type User = { id: number; name: string };

// nullを許容する
const [user, setUser] = useState<User | null>(null);

// 文字列リテラルの共用型
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');

// 配列
const [tags, setTags] = useState<string[]>([]);

関数型更新

前の値をもとに更新するときは関数型更新が安全です。

// ❌ 古い値を参照してしまうことがある
setCount(count + 1);

// ✅ 最新の値を受け取って更新
setCount((prev) => prev + 1);

特に連続した更新では違いが明確になります。

function TripleIncrement() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // ❌ 3回呼んでも1しか増えない(count は同じ値)
    // setCount(count + 1);
    // setCount(count + 1);
    // setCount(count + 1);

    // ✅ 確実に3増える
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
  };

  return <button onClick={handleClick}>{count}</button>;
}

配列のstate

配列を直接変更しないのが鉄則です。元の配列を変えず、新しい配列を作って渡します。

function TagManager() {
  const [tags, setTags] = useState<string[]>(['react', 'typescript']);

  // 追加
  const addTag = (tag: string) => {
    setTags((prev) => [...prev, tag]);
  };

  // 削除
  const removeTag = (tag: string) => {
    setTags((prev) => prev.filter((t) => t !== tag));
  };

  // 更新(特定の要素を変える)
  const updateTag = (index: number, newTag: string) => {
    setTags((prev) => prev.map((t, i) => (i === index ? newTag : t)));
  };

  // 並び替え
  const moveUp = (index: number) => {
    if (index === 0) return;
    setTags((prev) => {
      const next = [...prev];
      [next[index - 1], next[index]] = [next[index], next[index - 1]];
      return next;
    });
  };

  return (
    <ul>
      {tags.map((tag, i) => (
        <li key={tag}>
          {tag}
          <button onClick={() => moveUp(i)}>↑</button>
          <button onClick={() => removeTag(tag)}>削除</button>
        </li>
      ))}
    </ul>
  );
}

オブジェクトのstate

オブジェクトも直接変更しない。スプレッド演算子で一部だけ更新した新しいオブジェクトを作ります。

type Profile = {
  name: string;
  email: string;
  bio: string;
  isPublic: boolean;
};

function ProfileEditor() {
  const [profile, setProfile] = useState<Profile>({
    name: '山田 太郎',
    email: 'yamada@example.com',
    bio: '',
    isPublic: true,
  });

  // フィールドを個別に更新するユーティリティ
  const updateField = <K extends keyof Profile>(key: K, value: Profile[K]) => {
    setProfile((prev) => ({ ...prev, [key]: value }));
  };

  return (
    <form>
      <input
        value={profile.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input
        value={profile.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
      <textarea
        value={profile.bio}
        onChange={(e) => updateField('bio', e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={profile.isPublic}
          onChange={(e) => updateField('isPublic', e.target.checked)}
        />
        公開する
      </label>
    </form>
  );
}

複数のstateをまとめるか分けるか

分けるべきケース:独立した値

// ✅ 別々の関心事は分けたほうが明確
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<Data | null>(null);

まとめるべきケース:常に一緒に変わる値

// ✅ いつも同時に更新する値はまとめる
const [position, setPosition] = useState({ x: 0, y: 0 });

const handleMouseMove = (e: MouseEvent) => {
  setPosition({ x: e.clientX, y: e.clientY });
};

初期化を遅延させる(lazy initialization)

初期値の計算コストが高いとき、関数を渡すと最初のレンダリング時だけ実行されます。

// ❌ 毎回レンダリングのたびに parseJSON が実行される
const [data, setData] = useState(parseJSON(localStorage.getItem('data')));

// ✅ 初回だけ実行される(lazy initialization)
const [data, setData] = useState(() => parseJSON(localStorage.getItem('data')));
ユナ
ユナ 配列・オブジェクトを直接変更しないイミュータブルな更新は、バックエンドでもReduxやEventSourcedなアーキテクチャで同じ原則を使う。「状態を変えるのではなく、新しい状態を作る」という思想は一貫している。
たける
たける `[...prev, newItem]` ってスプレッドで新しい配列を作るのが癖になりそうです。`push` より書き方が多少長くなるけど、これが正しいんですね。
りこ
りこ `push` は元の配列を変えてしまう。Reactは参照が同じだと「変わっていない」と判断して再レンダリングしない。スプレッドで新しい配列を返すのは、Reactに「変わったよ」と伝えるための合図。

まとめ

  • 前の値をもとに更新するときは setState(prev => ...)関数型更新を使う
  • 配列・オブジェクトは直接変更せず、新しい値を作って渡す(イミュータブル)
  • 独立した値は分けて管理、常に一緒に変わる値はまとめて管理
  • 初期値の計算が重いときは useState(() => ...) で遅延初期化

次の第2回では、データ取得などの副作用を扱う useEffect の基本を学びます。