目次
5 章

State と useState

React が再描画する単位としての state。useState の正確なモデルと関数型更新、オブジェクト state の更新パターンを見ていきます。

4 章でコンポーネントと props を学びました。ところが作ったコンポーネントはすべて静的でした。一度描かれたら姿を変えませんでした。実際のアプリはユーザがボタンを押したり入力したり、データが到着したりすると画面が更新される必要があります。本章ではコンポーネントが変わりうるデータを扱う方法である state を学びます。

useState は本書全体で最もよく出会う道具です。18 章(hooks の型付け)で TypeScript によってもう一度整え、24 章(Server vs Client Components)では、どのコンポーネントで useState が使えてどこでは使えないのかの境界をもう一度押さえます。

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

最初に思いつくのは「普通に変数の値を変えればいいのでは?」でしょう。試してみましょうか。

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

React に画面を再描画させながら、その値が次のレンダリングでも保たれるようにするには、state という特別な保管庫を使う必要があります。

useState フック #

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

注記
フック(Hook) は関数コンポーネントの中で React の機能を使えるようにする特殊な関数です。名前がすべて use で始まります(useStateuseEffectuseContext など)。フックの深い話は 13 章(カスタムフック)で扱うので、いまは「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 番目の setCountstate を変更する関数。この関数を呼ばないと 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

18 章(hooks の型付け)で見ますが、TypeScript では初期値から型が自動的に推論されます。useState('') なら stringuseState(0) なら number が推論されます。

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 つのオブジェクトにまとめることもできますが、変更のたびにスプレッドで展開する必要があってコードが長くなるので、単純な値は別々に置く方が楽です。9 章(フォームの扱い)で両方を比較してみます。

自分でやってみる #

カウンタコンポーネントを作ってみます。+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)単位で別々に保管されるからです。

練習問題 #

  1. 上の Counter に新しいボタン +10 を追加して、1 回押すと一度に 10 増えるようにしてみてください。関数型更新(setCount(prev => prev + 10))のパターンを使います。
  2. Counterminmax の prop を追加し、カウントが min より下に下がったり max より上に上がったりしないように防いでみてください。<Counter min={0} max={10} /> のように呼び出せば、0 未満や 10 超過にならないようにします。関数型更新の中で Math.maxMath.min で処理します。
  3. オブジェクト state を扱う練習。User コンポーネントを作り、useState({ name: '太郎', age: 30 }) で初期値を設定してください。「名前を変える」ボタンは prev => ({ ...prev, name: '花子' })、「年齢 +1」ボタンは prev => ({ ...prev, age: prev.age + 1 }) で処理します。スプレッドで新しいオブジェクトを作るパターンが手に馴染むまで繰り返してみてください。

一行まとめ: 普通の変数では画面を更新できない。useState[値, set値] を受け取り、set 関数でのみ変更する。配列・オブジェクトは新しい値を作って渡す([...arr, x]{ ...obj, k: v })。以前の値を基に更新するときは関数型更新(setX(prev => ...))が安全な既定だ。

次の章 #

ここまで使ってきた onClick のようなイベントハンドラは、ただ持ってきて使っただけで詳しくは扱いませんでした。次の第 6 章 イベントハンドリングでは React のイベント処理方式を本格的に見て、イベントオブジェクトから情報を取り出す方法、そして 19 章(イベントとフォームの型付け)への橋渡しまで押さえます。

X