結論:Reactの画面は「stateが変わったとき」だけ描き直されます。だから普通の変数を書き換えても表示は変わらず、useState が返す更新関数(setXxx)を呼んで初めて画面が更新されます。配列やオブジェクトのときは、中身を書き換えるのではなく“新しい値を作って渡す”のがルールです。
Reactを触り始めた人がほぼ全員ぶつかるのが「値は確かに変わっているのに、画面の表示が変わらない」現象です。これはバグではなく、Reactの再描画の仕組みをまだ知らないだけです。この記事は state 1点に絞って、その仕組みと正しい書き方をまとめます。
なぜ普通の変数ではダメなのか
たとえば「ボタンを押したら数字が増える」を、素直にこう書いたとします。
function App() {
let count = 0; // ただの変数
return (
<div>
<h1>{count}</h1>
<button onClick={() => { count = count + 1; }}>+1</button>
</div>
);
}
ボタンを押すと count は確かに 1 になります。しかし画面の数字は 0 のままです。理由は2つあります。
- Reactは「再描画のきっかけ」を知らない。 Reactはコンポーネント関数を実行してその戻り値(JSX)から画面を作ります。変数を書き換えても、Reactにとっては「関数をもう一度実行する理由」になりません。だから表示は古いままです。
- 仮に再描画されても値が消える。
let count = 0は関数の中の変数なので、関数がもう一度実行されれば毎回 0 に戻ります。
必要なのは「値を覚えておく」と「変わったらReactに教える」の2つです。この2つをまとめて引き受けるのが useState です。
useState の書き方
const [count, setCount] = React.useState(0);
分解するとこうなります。
React.useState(0)… 初期値0で state を用意するcount… 現在の値(読むだけ)setCount… 値を更新する関数。呼ぶとReactがそのコンポーネントを再描画する
左辺の [ ] は配列の分割代入です。useState は「現在の値」と「更新関数」を2つ組で返すので、それを2つの名前で受け取っています。この書き方に馴染みがなければ、分割代入とスプレッド ― 取り出しとコピー をブラウザで動かして確認しておくと、Reactのコードがぐっと読みやすくなります。
実際に動くカウンターは useStateで状態を持つ ― カウンター にあります。ブラウザ上で本物のReactが描画される回なので、ボタンを押して数字が増えるところまでその場で試せます。初期値を 0 から 100 に変えるとどうなるか、といった実験もエディタでそのままできます。
命名は [値, set値] の形が慣習です(text と setText、items と setItems)。守らなくても動きますが、他人のコードを読むときに効いてきます。
直接書き換えると何も起きない
いちばん多い間違いがこれです。
// ダメな例
count = count + 1; // 変数への代入。Reactは何も気づかない
count++; // 同じ
// 正しい
setCount(count + 1); // 更新関数を通す
setCount を呼ぶと、Reactは「この state が変わった」と知り、そのコンポーネントをもう一度実行して新しいJSXを作り、変わった部分だけを画面に反映します。これが再描画です。用語としての整理は レンダリング を参照してください。
ちなみに const [count, setCount] = ... の count に代入しようとすると、JavaScriptの側でもエラーになります(Assignment to constant variable)。「更新関数を通す」というルールを、const が構文レベルで守らせてくれている形です。
もう一つの落とし穴は、setCount を呼んだ直後の行で count を読むケースです。
setCount(count + 1);
console.log(count); // まだ古い値のまま
count はその回の描画のあいだ固定された値なので、更新関数を呼んでも即座には変わりません。新しい値は「次の描画」で受け取ります。連続して更新したいときは、前の値を受け取る形を使います。
setCount((prev) => prev + 1);
setCount((prev) => prev + 1); // これで確実に2増える
タイマーのように「前の値を元に更新し続ける」場面ではこの形が安全です。実例は useEffectで副作用を書く の1秒カウンターで見られます。
配列やオブジェクトは「新しい値」を作って渡す
数値や文字列なら悩みませんが、配列とオブジェクトで多くの人が詰まります。
// ダメな例:元の配列を書き換えている
items.push("新しい項目");
setItems(items); // 画面が更新されないことがある
// 正しい:新しい配列を作る
setItems([...items, "新しい項目"]);
Reactは「値が変わったか」を、中身を1つずつ比べるのではなく同じものかどうかで判断します。push は同じ配列の中身をいじるだけなので、Reactからは「前と同じ配列」に見えてしまい、再描画されないことがあります。だから、スプレッド構文 [...items, 新しい値] で別の配列を作って渡します。
用途ごとの定石はこうなります。
setItems([...items, newItem]); // 追加
setItems(items.filter((item) => item !== target)); // 削除
setUser({ ...user, name: "新しい名前" }); // 1項目だけ変える
map と filter は元の配列を変えずに新しい配列を返すので、Reactと相性が良いのです。この2つに不安があれば 配列メソッド ― map / filter / find で先に手を動かしておくと、Reactのコードが「JavaScriptそのもの」に見えてきます。
配列を画面に並べる書き方は リストを描画する ― map にあり、追加と表示を組み合わせた形は 小さなTODOアプリ でまとめて動かせます。どちらもブラウザで実行できる回です。
入力欄と state をつなぐ
フォームは state の練習にちょうど良い題材です。
const [text, setText] = React.useState("");
<input value={text} onChange={(e) => setText(e.target.value)} />
value に state を渡し、onChange で state を更新する。この往復で「入力欄の中身=stateの値」が常に一致します。この形を制御コンポーネントと呼びます。動く例は 入力を扱う ― フォームとstate にあります。入力した文字数をその場で表示する、といった実験がすぐできます。
onChange のようなイベントの書き方そのものは イベントに反応する ― onClick が入口です。「押したら何かが起きる」と「stateが変わると画面が変わる」の2つが揃うと、アプリらしい動きが作れるようになります。
state をどこに置くか ― 状態を上げる
慣れてくると次の壁が来ます。「入力欄の値を、別の場所の表示にも使いたい」。しかしReactには、兄弟のコンポーネント同士で直接値を渡す仕組みがありません。
答えは state を、それを使う全員の共通の親に置くことです。値は props で下に流し、変更のきっかけは関数を渡して上に返します。これを状態を上げる(Lifting State Up) と呼びます。
function App() {
const [text, setText] = React.useState(""); // state は親に1つだけ
return (
<div>
<InputBox text={text} onChange={setText} />
<Preview text={text} />
</div>
);
}
この考え方が身につくと、「どのコンポーネントに state を持たせるか」で迷わなくなります。目安はその値を使うコンポーネント全部を含む、いちばん下の親です。上げすぎると無関係な部分まで再描画され、下げすぎると共有できません。
実際に動かせるのが 状態を上げる ― 兄弟コンポーネントでstateを共有する です。props で値を配る書き方に不安があれば、先に propsで値を渡す に戻ると流れがつながります。
親から子、孫、ひ孫へと props を延々と手渡しするのがつらくなってきたら、次の道具があります。
- useContextでバケツリレーを解消する — 離れた場所に値を配る
- useReducerで更新ロジックを1か所にまとめる — 更新の種類が増えたとき
ただし、最初からこれらに手を出す必要はありません。まず useState と「状態を上げる」で足りなくなってからで十分です。
つまずきやすいところ
画面が変わらない。 更新関数を呼んでいるか、配列やオブジェクトを新しく作っているかを順に確認します。この記事の前半2つがほぼ全部の原因です。
Cannot read properties of undefined が出る。 取得前のデータが undefined のまま data.name を読むと起きます。表示側で「まだ無いとき」を分岐させます。エラーの読み方は Cannot read properties of undefined、分岐の書き方は 条件で表示を変える、取得の定石は データ取得のパターン にあります。
入力するたびに全部が再描画されて重い気がする。 ほとんどの画面では問題になりません。実際に遅くなってから 再描画の最適化 ― useMemo・useCallback・React.memo を読むのが正しい順番です。
そもそもJSXの { } が分からない。 state 以前の話なので JSXに値を埋め込む ― 波かっこ に戻ります。JavaScript側が怪しいときは JavaScriptのエラー逆引き から引けます。
学ぶ順番
state を理解するための最短ルートはこの並びです。当サイトの Reactコース は、ブラウザ内で本物のReactが描画される形式で、インストールも登録も不要・無料でその場で試せます(このコースのエディタでは React.useState の形で書きます。自分のプロジェクトでは import { useState } from "react" と書いてから useState を使います)。
- はじめての React ― 画面に文字を出す
- JSXに値を埋め込む ― 波かっこ
- コンポーネントを分ける → propsで値を渡す
- リストを描画する ― map
- イベントに反応する ― onClick
- useStateで状態を持つ ― カウンター ← この記事の中心
- 入力を扱う ― フォームとstate → 条件で表示を変える
- 小さなTODOアプリ → 状態を上げる
6〜8まで来れば、「入力して、増やして、消せる」小さなアプリを自分で組み立てられます。ここがReactの最初の到達点です。
次に読む
- Reactコース — ブラウザで本物のReactを描画。無料・登録不要
- useStateで状態を持つ ― カウンター — この記事の内容をそのまま動かせる回
- JavaScriptコース — 配列メソッドや分割代入の土台
- JavaScriptのエラー逆引き — メッセージから原因を引く
- JavaScript独学ロードマップ — Reactの前に何をやるか
- ReactとAngularどっちがいい — 選び方で迷っているとき