React基礎講座 #5 StateとuseState

読了 8分

前回はコンポーネントとpropsを学びました。ところが私たちが作ったコンポーネントはすべて静的でした。一度画面に描画されると二度と姿が変わりませんでした。実際のアプリはユーザーがボタンを押したり、入力をしたり、データが届くと画面が更新されなければなりません。今回はコンポーネントが変わりうるデータを扱う方法であるstateを学んでいきます。

なぜ普通の変数ではダメなのか? #

最初に思い浮かぶ考えは「普通に変数の値を変えればいいんじゃないか?」でしょう。一度試してみましょうか?

src/App.jsx
function App() {
  let count = 0;

  function handleClick() {
    count = count + 1;
    console.log('count:', count);
  }

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

export default App;

ボタンをクリックするとコンソールにはcount: 1count: 2count: 3のように値はちゃんと増えていきます。ところが画面の数字はそのまま0です。なぜでしょうか?

Reactは画面を描画するときコンポーネント関数を1回実行し、その結果を画面に反映します。通常の変数の値を変えてもReactは「再描画する必要がある」というシグナルを受け取らないので画面を更新しません。さらにコンポーネント関数は再度呼び出されるたびにlet count = 0が最初から再実行されるので、変更された値は次のレンダリングに残れもしません。

Reactに画面を再描画させながら、その値が次のレンダリングでも維持されるようにするにはstateという特別な保管場所を使わなければなりません。

useStateフック #

stateはuseStateという関数を呼び出して作ります。この関数はReactが提供するフック(Hook)の1つです。

注記
フック(Hook)は関数コンポーネントの中でReactの機能を使えるようにしてくれる特殊な関数です。名前がすべてuseで始まります(useStateuseEffectuseContext…)。フックに関する詳しい内容はシリーズ後半で扱いますが、今は「Reactが提供する特別な関数」ぐらいに理解していただければよいです。

useStateを使ってカウンターを再度作ってみます。

src/App.jsx
import { useState } from 'react';

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

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

export default App;

今回はボタンを押すと画面の数字が本当に増えます。

useStateを覗いてみる #

上のコードを1行ずつ見ていきます。

import { useState } from 'react';

reactパッケージからuseStateを取り込みます。フックを使うには常にimportしなければなりません。

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

useState(0)を呼び出すと、長さ2の配列が返されます。JavaScriptの分割代入で2つの値を一度に受け取ります。

  • 最初のcount: 現在のstate値。最初はuseStateに入れた初期値(0)です。
  • 2番目のsetCount: stateを変更する関数。この関数を呼び出さないとReactは画面を再描画しません。

名前は自由に付けてもよいですが、慣例的に[値, set値]パターンで付けます。nameなら[name, setName]isOpenなら[isOpen, setIsOpen]のように。

setCount(count + 1);

setCountに新しい値を入れて呼び出すと、Reactは(1)stateを更新し(2)コンポーネントを再レンダリングします。再レンダリングされるとコンポーネント関数が最初から再実行され、今度はuseState(0)が新しく更新された値(1)を返します。

stateが変わると何が起きるのか #

この絵を頭の中によく描いておくと、今後Reactのコードがはるかによく読めるようになります。

  1. ユーザーがボタンをクリック
  2. handleClick関数が実行される → setCount(1)を呼び出し
  3. Reactがstateを1に更新してコンポーネントを再レンダリング
  4. App関数が最初から再実行される
  5. useState(0)が今度は[1, setCount]を返す
  6. 新しいJSX(<p>現在のカウント: 1</p>)が作られる
  7. Reactが以前の画面と比較して変更された部分だけを実際のDOMに反映

stateが変わるたびにコンポーネント関数全体が再実行されるという点が重要です。だからlet count = 0のようにコンポーネント内で宣言した通常の変数は毎回初期化されて値が維持されないのです。stateはReact内部のどこかに別途保管され、次のレンダリングでも生き残ります。

様々なタイプのstate #

state値は数値だけでなくどんなJavaScript値でもかまいません。

様々なstateの例
const [name, setName] = useState('');                  // 文字列
const [isOpen, setIsOpen] = useState(false);           // ブーリアン
const [items, setItems] = useState([]);                // 配列
const [user, setUser] = useState({ name: '', age: 0 }); // オブジェクト
const [selected, setSelected] = useState(null);       // null

stateは絶対に直接修正しないでください #

もっともよくやるミスです。次のコードは動作しません

間違った例
const [count, setCount] = useState(0);

function handleClick() {
  count = count + 1;  // 🚫 直接修正
}
間違った例 (配列)
const [items, setItems] = useState(['りんご']);

function addItem() {
  items.push('バナナ');  // 🚫 配列を直接変更
  setItems(items);
}

stateは必ずset関数を通じて新しい値を渡して変更しなければなりません。配列やオブジェクトの場合は新しい配列/新しいオブジェクトを作って渡すのが原則です。

正しい例 (配列追加)
const [items, setItems] = useState(['りんご']);

function addItem() {
  setItems([...items, 'バナナ']);  // 新しい配列を作って渡す
}
正しい例 (オブジェクトの一部修正)
const [user, setUser] = useState({ name: 'チョルス', age: 30 });

function birthday() {
  setUser({ ...user, age: user.age + 1 });  // 新しいオブジェクトを作って渡す
}

スプレッド演算子(...)で既存の値を展開した後、変更する部分だけを上書きするパターンがもっとも一般的です。

注記
「なぜわざわざ新しい配列/オブジェクトを作らないといけないのですか?」Reactはstateが変わったかを判断するとき参照(reference)が異なるかを確認します。items.push(...)は同じ配列の内容だけを変えるだけで参照はそのままなので、Reactは変化がないと判断して再レンダリングしません。新しい配列/オブジェクトを作って渡すと、Reactが「あ、別の値だ」と判断して画面を更新します。

関数型アップデート #

state値を以前の値を基にアップデートするときはset関数に関数を渡すことができます。

src/App.jsx
function handleClick() {
  setCount(prev => prev + 1);
}

setCount(count + 1)とほぼ同じですが、以前の値を安全に受け取るので何度も連続で呼び出さなければならないときに正確に動作します。

問題となるパターン
function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

このコードは1回のクリックでカウントを3増やしそうですが、実際には1しか増えません。3つの呼び出しすべてが同じcount値を見ているため、すべてがcount + 1を試みるからです。

関数型アップデートで安全に
function handleClick() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
}

このように書くと各呼び出しが直前の結果を受け取って処理するので、カウントが正確に3増えます。普段はsetCount(count + 1)でも十分ですが、「以前の値を基準にアップデートする」という意味が明確な関数型アップデートの方がより安全な基本パターンです。

複数のstate #

useStateは1つのコンポーネントの中で何度でも呼び出せます。

src/LoginForm.jsx
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  // ... 入力処理ロジック ...
}

性格の異なる値はそれぞれ別のstateで管理するのが一般的です。1つのオブジェクトにまとめることもできますが、変更するたびにスプレッドで展開しなければならずコードが長くなるので、単純な値は別々に置く方が便利です。

自分でやってみる #

カウンターコンポーネントを作ってみます。+1-1リセットボタンがあるコンポーネントです。

src/Counter.jsxを新しく作ります。

src/Counter.jsx
import { useState } from 'react';

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

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <h2>カウント: {count}</h2>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  );
}

export default Counter;

src/App.jsxから取り込んで使います。

src/App.jsx
import Counter from './Counter';

function App() {
  return (
    <>
      <h1>カウンターデモ</h1>
      <Counter />
      <Counter />
    </>
  );
}

export default App;

興味深い点が1つあります。<Counter />を2回使ったのですが、それぞれのカウンターが自分だけのカウントを持って独立して動作します。一方で+1を押しても他方の数字はそのままです。stateはコンポーネントのインスタンス(instance)ごとに別々に保管されるからです。

おわりに #

今回の記事ではコンポーネントが変わるデータを扱うツールであるstateuseStateフックを学びました。要点をまとめると:

  • 通常の変数では画面を更新できない → useStateを使う
  • const [値, set値] = useState(初期値)パターン
  • stateは直接修正せず必ずset関数で変更する
  • 配列/オブジェクトは新しい値を作って渡す([...arr, x]{ ...obj, k: v })
  • 以前の値を基に更新するときは関数型アップデート(setX(prev => ...))が安全

これまで使ってきたonClickのようなイベントハンドラはそのまま持ってきて使っただけで詳しく扱いませんでした。次の記事「React基礎講座 #6 イベントハンドリング」では、Reactのイベント処理方式を本格的に見ていき、イベントオブジェクトから情報を取り出して使う方法まで見ていくことにしましょう。

X