こんにちは、かつコーチです。
前回はPropsを使って、親から子へ値を渡す方法を解説しました。
ただ、Propsだけでは「コンポーネント自身が持つ、変化する値」を扱うことはできません。
たとえばボタンを押すたびに数字が増えるカウンターのような機能です。
今回は、そのための仕組みである useState を基礎から見ていきます。
useStateとは?
コンポーネントに「状態」を持たせるHook
useState は、コンポーネントの中に状態(State)を持たせるためのReactの機能です。
React用語でこういった機能は「Hook(フック)」と呼ばれ、関数コンポーネントに特別な能力を「引っかける(hookする)」ものだとイメージすると分かりやすいです。
状態とは、時間の経過やユーザー操作によって変化する値のことです。
カウンターの数字、フォームの入力内容、モーダルの表示・非表示などが代表例です。
なぜuseStateが必要なのか
普通のJavaScriptの変数を使って画面を更新しようとしても、Reactは変数の変化を検知できません。
function Counter() {
let count = 0;
const increment = () => {
count = count + 1; // 変数は変わるが、画面は再描画されない
};
return <button onClick={increment}>{count}</button>;
}
このコードは、ボタンを押すと count の値自体は増えているのに、画面上の表示は0のまま変わりません。
Reactは「状態が変わったら画面を再レンダリングする」という仕組みで動いているため、Reactが認識できる形で値を管理する必要があります。
その役割を担うのが useState です。
基本の書き方
状態を宣言する
useState は、初期値を渡して呼び出すと「現在の値」と「値を更新する関数」の2つを配列で返します。
import { useState } from "react";
function Counter() {
const [count, setCount] = useState<number>(0);
return (
<div>
<p>現在のカウント:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
const [count, setCount] = useState(0) の部分がポイントです。
count:現在の状態の値setCount:状態を更新するための関数useState(0):初期値を0に設定
配列の分割代入を使って、この2つを同時に受け取っています。
更新関数を呼んで再レンダリングを起こす
setCount(count + 1) のように更新関数を呼ぶと、Reactは新しい値でコンポーネントを再レンダリングします。
先ほどの「変わらないカウンター」と違い、ボタンを押すたびに画面上の数字がきちんと更新されます。
setCount を呼ぶことが、Reactに対して「状態が変わったので画面を描き直してください」と伝える合図になっているイメージです。
型を明示する
TypeScriptでは useState<number>(0) のようにジェネリクスで型を指定できます。
初期値から型を推論してくれるケースも多いですが、初期値が null や undefined になりうる場合は明示しておくと安全です。
const [user, setUser] = useState<{ name: string } | null>(null);
よくあるつまずきポイント
状態を直接書き換えてしまう
私が useState を使い始めた頃、配列の状態を「直接書き換えて」からrenderされずに悩んだことがあります。
❌ Before:状態を直接ミューテートする
function TodoList() {
const [todos, setTodos] = useState<string[]>(["買い物"]);
const addTodo = () => {
todos.push("掃除"); // 配列を直接書き換えている
// setTodosを呼んでいないので画面は更新されない
};
return (
<div>
<button onClick={addTodo}>追加</button>
<ul>
{todos.map((todo) => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
todos.push() で配列自体は変化しているのですが、setTodos を呼んでいないため、Reactは変化に気づかず画面が更新されません。
さらに、Reactは状態を「不変(イミュータブル)」に扱うことを前提にしているため、元の配列を直接書き換える設計自体がバグの温床になります。
✅ After:新しい配列を作ってsetStateに渡す
function TodoList() {
const [todos, setTodos] = useState<string[]>(["買い物"]);
const addTodo = () => {
setTodos([...todos, "掃除"]); // 新しい配列を作って渡す
};
return (
<div>
<button onClick={addTodo}>追加</button>
<ul>
{todos.map((todo) => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
スプレッド構文 [...todos, "掃除"] を使って、元の配列はそのままに、新しい配列を作って setTodos に渡すのがポイントです。
「状態は直接書き換えず、常に新しい値を作ってセットする」というルールを覚えておくと、この種のバグはほぼ防げます。
応用:一歩先の使い方
オブジェクトの状態を部分的に更新する
オブジェクトを状態として持つ場合も、考え方は配列と同じです。
interface Profile {
name: string;
age: number;
}
function ProfileForm() {
const [profile, setProfile] = useState<Profile>({ name: "", age: 0 });
const updateName = (newName: string) => {
setProfile({ ...profile, name: newName });
};
return <input onChange={(e) => updateName(e.target.value)} />;
}
{ ...profile, name: newName } のように、元のオブジェクトをスプレッド構文で展開しつつ、変更したいプロパティだけを上書きします。
直前の状態をもとに更新する(関数型更新)
現在の値を使って更新する場合、更新関数に値ではなく関数を渡す書き方もあります。
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
短時間に何度も更新が走る場面では、この「関数型更新」を使う方が値のズレを防ぎやすく、より安全です。
まとめ
この記事のポイント
useStateはコンポーネントに変化する値(状態)を持たせるためのHook- 状態と更新関数はセットで返され、更新関数を呼ぶことで画面が再レンダリングされる
- 配列やオブジェクトの状態は直接書き換えず、スプレッド構文で新しい値を作って更新する
useState((prev) => ...)の関数型更新を使うと、直前の値をもとに安全に更新できる
次に読むべき記事
状態が更新できるようになったら、次はユーザー操作に反応してその状態を変える方法を学びます。
次回は、onClick をはじめとしたイベントハンドリングの書き方を解説します。
→ 次の記事:イベントハンドリングの書き方(onClick等)