本文へスキップ
BecomeCoder
ブログ一覧Wikiコース一覧

ReactのstateとuseStateの使い方|値を変えたのに画面が変わらない理由

#React#useState#state#フロントエンド#初心者

結論: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つあります。

  1. Reactは「再描画のきっかけ」を知らない。 Reactはコンポーネント関数を実行してその戻り値(JSX)から画面を作ります。変数を書き換えても、Reactにとっては「関数をもう一度実行する理由」になりません。だから表示は古いままです。
  2. 仮に再描画されても値が消える。 let count = 0 は関数の中の変数なので、関数がもう一度実行されれば毎回 0 に戻ります。

必要なのは「値を覚えておく」と「変わったらReactに教える」の2つです。この2つをまとめて引き受けるのが useState です。

useState の書き方

const [count, setCount] = React.useState(0);

分解するとこうなります。

左辺の [ ] は配列の分割代入です。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 を延々と手渡しするのがつらくなってきたら、次の道具があります。

ただし、最初からこれらに手を出す必要はありません。まず 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 を使います)。

  1. はじめての React ― 画面に文字を出す
  2. JSXに値を埋め込む ― 波かっこ
  3. コンポーネントを分ける → propsで値を渡す
  4. リストを描画する ― map
  5. イベントに反応する ― onClick
  6. useStateで状態を持つ ― カウンター ← この記事の中心
  7. 入力を扱う ― フォームとstate → 条件で表示を変える
  8. 小さなTODOアプリ → 状態を上げる

6〜8まで来れば、「入力して、増やして、消せる」小さなアプリを自分で組み立てられます。ここがReactの最初の到達点です。

次に読む

← ブログ一覧に戻る