第1回
useStateの完全ガイド:関数型更新・配列・オブジェクトの正しい扱い方
ReactのuseStateを完全解説します。型推論・関数型更新・オブジェクト・配列のイミュータブルな更新方法・複数stateの使い分けまで、実務で使えるパターンを実例付きで学べます。
制作・運営:React Guide 運営者
このシリーズについて
「Hooks完全解説」は、React Hooksを深く理解するための全12回シリーズです。useState・useEffect・useContext・useReducer・useRef・useMemo・useCallback・カスタム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のルールとよくある間違い |
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[]>([]);関数型更新
前の値をもとに更新するときは関数型更新が安全です。
// 1回のクリックで1増やすなら有効。同じイベント内の連続更新では同じcountを読む
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)
初期値の計算コストが高いとき、関数を渡すと通常の再レンダーでは再計算しません。開発時のStrictModeでは純粋性の確認のため追加で呼ぶことがあるので、副作用は入れません。
// ❌ 毎回レンダリングのたびに parseJSON が実行される
const [data, setData] = useState(parseJSON(localStorage.getItem('data')));
// ✅ 初期化のために計算する(StrictModeでは追加実行の場合あり)
const [data, setData] = useState(() => parseJSON(localStorage.getItem('data')));まとめ
- 前の値をもとに更新するときは
setState(prev => ...)の関数型更新を使う - 配列・オブジェクトは直接変更せず、新しい値を作って渡す(イミュータブル)
- 独立した値は分けて管理、常に一緒に変わる値はまとめて管理
- 初期値の計算が重いときは
useState(() => ...)で遅延初期化
次の第2回では、データ取得などの副作用を扱う useEffect の基本を学びます。
確認してみる
同じイベントで2回増やしたいときは 更新実験を使う。配列更新では元の配列も残して、元が変わっていないか確認する。
同じテーマの教材
useState・useRef・useReducerの型定義:TypeScriptで正しく型をつける
TypeScriptでuseState・useRef・useReducerに型をつける方法を解説。型推論が効くケースと明示が必要なケース、useRefのnull初期値問題まで実例付きで説明します。
stateとuseState:クリックで選択キャラを切り替える
useStateでキャラクター選択状態を管理する実装を通じて、stateの概念・更新関数・再レンダリングの仕組みを対話形式で解説。
【解決】useEffectでasync/awaitを使う正しい方法と注意点
useEffectに直接asyncを付けてはいけない理由と、正しいasync/awaitの書き方を解説。内部関数として定義する方法・クリーンアップとの組み合わせ・競合状態(race condition)の防ぎ方まで実例付きで説明します。