Chapter 04

State と useState

コンポーネントの「記憶」であるStateを学びます。ボタンのクリック数やフォームの入力値など、時間とともに変化するデータを管理できるようになります。

45 min

このチャプターで学ぶこと

  • なぜプレーンな変数ではなくStateが必要なのかを説明できる
  • useStateの構文を理解して正しく使える
  • Stateを更新してコンポーネントを再レンダリングさせられる
  • 同じコンポーネントを複数使ったとき、それぞれが独立したStateを持つことを理解できる
  • いいねカウンターボタンを自力で作れる

State と useState

Propsを使えばコンポーネントに外からデータを渡せるようになりました。でも、「ボタンをクリックしたらカウントが増える」「テキストを入力したら画面に反映される」といった時間とともに変化するデータはどう扱えばいいでしょうか?

そこで登場するのが State(ステート) です。

プレーンな変数では動かない

まず、なぜ普通のJavaScript変数じゃダメなのかを確認しましょう。

// これは動かない!
function Counter() {
  // プレーンな変数
  let count = 0;

  function handleClick() {
    count = count + 1; // 変数は変わるが...
    console.log(count); // コンソールには表示される
  }

  return (
    <div>
      {/* でも画面は更新されない */}
      <p>カウント: {count}</p>
      <button onClick={handleClick}>増やす</button>
    </div>
  );
}

試してみると、コンソールには 1, 2, 3… と表示されます。でも画面上の数字は 0 のまま動きません。

なぜか? Reactは「再レンダリング(re-render)」するきっかけがないと、画面を更新しないからです。変数を書き換えただけでは、Reactは「あ、画面を更新しなきゃ」と気づいてくれません。

📝 Laravelで例えると

バックエンドで例えると、PHPのローカル変数を変更しても、ブラウザ側のHTMLは更新されませんよね。画面を更新するには新しいHTTPレスポンスを返す必要があります。

Reactも同様で、画面を更新するには「再レンダリング」が必要です。Stateを更新することが、そのトリガーになります。Stateはいわば「セッション変数」のようなもので、値が変わると自動的にページ(コンポーネント)が再描画されます。

useState の基本構文

useState は React が提供する フック(Hook) のひとつです。フックとは use で始まる特別な関数で、コンポーネントに機能を追加します。

import { useState } from 'react';

function Counter() {
  // useState(初期値) を呼び出す
  const [count, setCount] = useState(0);

  //   ↑        ↑                  ↑
  // 現在の値  更新する関数       初期値
}

useState配列を返します。その配列を分割代入(デストラクチャリング)で受け取っています。

配列の分割代入について

初めて見ると少し戸惑うかもしれません。これは JavaScript の記法です。

// useState が返す配列のイメージ
// [現在の値, 更新する関数]
const result = useState(0);
const count = result[0];    // 現在の値
const setCount = result[1]; // 更新する関数

// 上記を1行で書くのが分割代入
const [count, setCount] = useState(0);

名前は自由につけられます。慣習として [xxx, setXxx] という形が使われます。

const [isOpen, setIsOpen] = useState(false);
const [username, setUsername] = useState('');
const [score, setScore] = useState(100);

State を使ったカウンター

正しく動くカウンターを作りましょう。

import { useState } from 'react';

function Counter() {
  // count の初期値は 0
  const [count, setCount] = useState(0);

  function handleClick() {
    // setCount を呼ぶと React が再レンダリングしてくれる
    setCount(count + 1);
  }

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleClick}>増やす</button>
    </div>
  );
}
localhost:5173
カウンターの初期状態
最初は 0 が表示されている

ボタンをクリックすると数字が増えます。

localhost:5173
ボタンをクリックした後のカウンター
クリックするたびにカウントアップ

何が起きているか

  1. ボタンがクリックされる
  2. handleClick 関数が呼ばれる
  3. setCount(count + 1) が実行される
  4. React が「Stateが変わった!再レンダリングしなきゃ」と認識する
  5. Counter 関数が再度実行され、新しい count の値で画面が更新される

このサイクルが Reactの基本的な動き方です。

💡 setXxx は直接代入ではない

count = count + 1 ではなく setCount(count + 1) と書く必要があります。Stateは直接書き換えず、必ず更新関数(setter)を使うのがルールです。直接書き換えても再レンダリングが走らず、画面が更新されません。

State の初期値にいろいろな型を使う

useState の初期値は数値に限りません。

// 文字列
const [name, setName] = useState('ゲスト');

// 真偽値
const [isVisible, setIsVisible] = useState(true);

// 配列
const [items, setItems] = useState([]);

// オブジェクト
const [user, setUser] = useState({ name: '', email: '' });

State はコンポーネントインスタンスごとに独立している

同じコンポーネントを複数レンダリングしても、それぞれが独立した State を持ちます。

import { useState } from 'react';

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

  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増やす</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>カウンターたち</h1>
      {/* 3つの Counter はそれぞれ独立した count を持つ */}
      <Counter />
      <Counter />
      <Counter />
    </div>
  );
}

左のカウンターを3回クリックしても、右のカウンターは0のまま。それぞれが独自の「記憶」を持っています。

📝 Laravelで例えると

クラスのインスタンスを複数 new すると、それぞれが独自のプロパティを持つのと同じイメージです。Counter コンポーネントが3つあれば、3つの別々な count が存在します。

State の更新は非同期(バッチ処理)

ひとつ注意点があります。setCount を呼んでも、すぐに count の値が変わるわけではありません

function handleClick() {
  setCount(count + 1);
  console.log(count); // まだ古い値が出る!
  // React は次のレンダリングで count を更新する
}

React は複数の State 更新をまとめて処理する「バッチ処理」を行うため、更新は非同期的に行われます。「更新をお願いする」というイメージです。実際の値は次のレンダリング(関数の再実行)時に反映されます。

⚠️ 前の State を元に更新するとき

現在の State を元に新しい値を計算するときは、関数形式で書くのが安全です。

// 危険(古い値を参照する可能性がある)
setCount(count + 1);

// 安全(必ず最新の値を使える)
setCount(prevCount => prevCount + 1);

特に連続して複数回 setCount を呼ぶ場合は関数形式を使いましょう。

複数の State を持てる

ひとつのコンポーネントが複数の useState を持っても問題ありません。

import { useState } from 'react';

function UserForm() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [age, setAge] = useState(0);

  return (
    <div>
      <input
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
        placeholder="名前"
      />
      <input
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
        placeholder="苗字"
      />
      <p>こんにちは、{lastName} {firstName} さん!</p>
    </div>
  );
}

onChange の詳細は次のチャプター(イベントハンドリング)で詳しく扱います。

✍ やってみよう

演習: いいねカウンターを作ろう

SNSでよく見る「いいね」ボタンを作りましょう。

要件:

  • ハートボタン(❤️)をクリックするたびにいいね数が増える
  • 現在のいいね数を表示する
  • ボタンにいいね数も表示する(例:「❤️ 42」)

ヒント:

import { useState } from 'react';

function LikeButton() {
  // ここに State を追加しよう
  const [likes, setLikes] = useState(0);

  return (
    <div>
      {/* いいね数を表示 */}
      <p>{likes} 件のいいね</p>
      {/* クリックで増やすボタン */}
      <button onClick={/* ここを埋めよう */}>
        ❤️ {likes}
      </button>
    </div>
  );
}

チャレンジ:

  • いいね済みかどうかを boolean の State で管理して、ボタンの色を変えてみよう(いいね済み: 赤、未いいね: グレー)
  • いいね済みの場合はもう一度クリックするといいねを取り消せるようにしよう

まとめ

次のチャプターでは、クリック・入力・送信などのイベントハンドリングを詳しく学びます。