React Stateとは?動的なWebアプリを作る魔法の仕組み

プログラミング・IT

「Reactを勉強し始めたけど、stateって何?」
「なんでstateを使わないと画面が更新されないの?」
「useStateの使い方がいまいち分からない…」

Reactでアプリを作り始めると、必ず出会うのが「state(ステート)」という概念です。

実は、stateはReactアプリの心臓部。この仕組みを理解すると、ボタンをクリックしたら画面が変わる、フォームに入力したら表示が変わる、といった「動き」のあるアプリが作れるようになります。

この記事では、React Stateの基本から実践的な使い方まで、初心者の方にも分かりやすく解説していきますね!


スポンサーリンク

React Stateとは?コンポーネントの「記憶」

Stateとは、Reactコンポーネントが持つ内部データのこと。日本語では「状態」と訳されます。

コンポーネントの「記憶」や「変数」のようなものだと考えてください。

コンポーネントって何?

コンポーネントとは、UIの部品のこと。ボタン、カード、フォームなど、再利用可能なパーツを指します。

実例:

function Counter() {
  return <div>カウンター</div>;
}

これが最もシンプルなコンポーネントです。

なぜStateが必要なの?

普通の変数では、画面の更新が反映されないからです。

ダメな例(普通の変数):

function Counter() {
  let count = 0;

  const increment = () => {
    count = count + 1;  // 値は変わるけど...
    console.log(count); // コンソールには表示される
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={increment}>増やす</button>
    </div>
  );
}

このコードでは、ボタンを押しても画面の数字は変わりません。コンソールには新しい値が表示されるのに、画面は0のままなんです。

理由:
Reactは、stateが変更されたときだけ画面を再描画(レンダリング)します。普通の変数が変わっても、Reactはそれに気づかないんです。


useState:関数コンポーネントでstateを使う

現代のReactでは、useStateというフック(Hook)を使ってstateを管理します。

useStateの基本

正しい例:

import { useState } from 'react';

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

  const increment = () => {
    setCount(count + 1);  // これで画面も更新される!
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={increment}>増やす</button>
    </div>
  );
}

今度はボタンを押すと、画面の数字もちゃんと増えます!

useStateの構文

const [state, setState] = useState(初期値);

分解すると:

  • state:現在の状態の値
  • setState:状態を更新するための関数
  • useState(初期値):stateを作成するフック
  • 初期値:stateの最初の値

実例:

const [count, setCount] = useState(0);
// count は 0 からスタート
// setCount で count を更新できる

const [name, setName] = useState('太郎');
// name は '太郎' からスタート

const [isOpen, setIsOpen] = useState(false);
// isOpen は false からスタート

配列の分割代入

const [count, setCount]という書き方は、配列の分割代入というJavaScriptの機能です。

useStateは配列を返すので、こう書くことで2つの値を同時に取得できます。

別の書き方(こちらは冗長):

const countState = useState(0);
const count = countState[0];
const setCount = countState[1];

分割代入を使った方がスッキリしますね。


Stateの更新:画面を再描画させる

stateを更新するには、必ずsetterとして用意された関数を使います。

基本的な更新

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

  // ✅ 正しい方法
  const increment = () => {
    setCount(count + 1);
  };

  // ❌ ダメな例
  const badIncrement = () => {
    count = count + 1;  // これは動かない
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

関数型更新:前の値に基づいて更新

前の値を元に計算したい場合、関数を渡す方法が推奨されます。

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

  const increment = () => {
    // 関数を渡す方法(推奨)
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

なぜこちらが良いの?

複数回連続で更新する場合、正しく動作するからです。

比較:

// ❌ これだと1しか増えない
const badMultipleIncrement = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // すべて同じcountの値を参照するため
};

// ✅ これなら3増える
const goodMultipleIncrement = () => {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  // 各更新が前回の結果を受け取る
};

様々な型のState

stateには、数値や文字列だけでなく、様々な型のデータを保存できます。

文字列のstate

function Greeting() {
  const [name, setName] = useState('');

  return (
    <div>
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <p>こんにちは、{name}さん!</p>
    </div>
  );
}

入力フィールドに文字を入力すると、リアルタイムで挨拶が表示されます。

真偽値(boolean)のstate

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOn(!isOn)}>
        {isOn ? 'ON' : 'OFF'}
      </button>
      <p>スイッチは{isOn ? 'オン' : 'オフ'}です</p>
    </div>
  );
}

ボタンをクリックするたびに、ONとOFFが切り替わります。

オブジェクトのstate

function UserProfile() {
  const [user, setUser] = useState({
    name: '太郎',
    age: 25,
    email: 'taro@example.com'
  });

  const updateName = (newName) => {
    setUser({
      ...user,           // 既存のプロパティをコピー
      name: newName      // nameだけ更新
    });
  };

  return (
    <div>
      <p>名前: {user.name}</p>
      <p>年齢: {user.age}</p>
      <button onClick={() => updateName('次郎')}>
        名前を変更
      </button>
    </div>
  );
}

重要:
オブジェクトを更新する際は、スプレッド構文(...)を使って既存の値をコピーする必要があります。

配列のstate

function TodoList() {
  const [todos, setTodos] = useState(['買い物', '掃除']);

  const addTodo = () => {
    setTodos([...todos, '新しいタスク']);
  };

  const removeTodo = (index) => {
    setTodos(todos.filter((_, i) => i !== index));
  };

  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>削除</button>
          </li>
        ))}
      </ul>
      <button onClick={addTodo}>追加</button>
    </div>
  );
}

複数のStateを管理する

一つのコンポーネントで、複数のstateを持つことができます。

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');

    try {
      // ログイン処理
      await login(email, password);
    } catch (err) {
      setError('ログインに失敗しました');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input 
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button disabled={isLoading}>
        {isLoading ? '送信中...' : 'ログイン'}
      </button>
      {error && <p style={{color: 'red'}}>{error}</p>}
    </form>
  );
}

関連するstateが多い場合は、一つのオブジェクトにまとめることもできます。


Stateのリフトアップ:親から子へ

複数のコンポーネントでstateを共有したい場合、共通の親コンポーネントにstateを置くという手法を使います。

これを「State のリフトアップ」と呼びます。

例:温度変換アプリ

function TemperatureConverter() {
  const [celsius, setCelsius] = useState(0);

  // 摂氏から華氏に変換
  const fahrenheit = (celsius * 9/5) + 32;

  return (
    <div>
      <CelsiusInput 
        value={celsius}
        onChange={setCelsius}
      />
      <FahrenheitDisplay value={fahrenheit} />
    </div>
  );
}

function CelsiusInput({ value, onChange }) {
  return (
    <div>
      <label>摂氏:</label>
      <input 
        type="number"
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </div>
  );
}

function FahrenheitDisplay({ value }) {
  return (
    <div>
      <p>華氏: {value.toFixed(1)}°F</p>
    </div>
  );
}

親コンポーネント(TemperatureConverter)がstateを管理し、子コンポーネントに値と更新関数を渡しています。


クラスコンポーネントのState(参考)

古いReactコードでは、クラスコンポーネントを使ったstate管理を見かけることがあります。

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.increment}>+1</button>
      </div>
    );
  }
}

現代のReactでは関数コンポーネント+useStateが主流なので、新しいプロジェクトではクラスコンポーネントは使いません。


Stateを使う際の注意点とベストプラクティス

注意点1:Stateは直接変更しない

// ❌ ダメな例
const [user, setUser] = useState({ name: '太郎' });
user.name = '次郎';  // 直接変更してはいけない

// ✅ 正しい方法
setUser({ ...user, name: '次郎' });

注意点2:State更新は非同期

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

const increment = () => {
  setCount(count + 1);
  console.log(count);  // まだ古い値が表示される!
};

setStateの直後にconsole.logしても、更新前の値が表示されます。更新は非同期で行われるためです。

注意点3:配列やオブジェクトは新しいものを作る

// ❌ ダメな例
const [items, setItems] = useState([1, 2, 3]);
items.push(4);           // 配列を直接変更
setItems(items);         // 再レンダリングされないことがある

// ✅ 正しい方法
setItems([...items, 4]); // 新しい配列を作成

ベストプラクティス1:State は最小限に

計算で求められる値は、stateにしない方が良いです。

// ❌ 冗長な例
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);

const increment = () => {
  setCount(count + 1);
  setDoubleCount((count + 1) * 2);  // 無駄
};

// ✅ 良い例
const [count, setCount] = useState(0);
const doubleCount = count * 2;  // 計算で求める

ベストプラクティス2:関連するStateはまとめる

// ❌ バラバラ
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// ✅ まとめる
const [position, setPosition] = useState({ x: 0, y: 0 });

よくあるパターン:入力フォームの管理

実践的な例として、フォームの状態管理を見てみましょう。

シンプルなフォーム

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('送信データ:', formData);
    // 送信処理
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="名前"
      />
      <input 
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="メール"
      />
      <textarea 
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="メッセージ"
      />
      <button type="submit">送信</button>
    </form>
  );
}

一つのhandleChange関数で、すべての入力欄を管理できます。


useStateの代替:その他の状態管理

stateの管理方法は、useStateだけではありません。

useReducer

複雑な状態のロジックには、useReducerが適しています。

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

Context API

アプリ全体で共有したいstateには、Context APIが便利です。

外部ライブラリ

大規模アプリでは、以下のような状態管理ライブラリが使われます:

  • Redux
  • Zustand
  • Recoil
  • Jotai

よくある質問

Q1. useStateの初期値に関数を渡せる?

はい、できます。初期値の計算が重い場合に便利です。

// ✅ 関数を渡す(初回レンダリング時のみ実行)
const [state, setState] = useState(() => {
  const initialValue = expensiveCalculation();
  return initialValue;
});

Q2. 親コンポーネントから子のstateを変更できる?

基本的にはできません。親から子へはpropsで値を渡し、変更が必要な場合は親がstateを持つようにします(リフトアップ)。

Q3. stateが更新されても再レンダリングされない?

オブジェクトや配列を直接変更していませんか?新しいオブジェクト・配列を作成する必要があります。

Q4. 無限ループになってしまう

useEffectの依存配列にstateを入れて、その中でstateを更新していませんか?条件をつけて更新を制御しましょう。


まとめ:Stateはリアクティブなアプリの基礎

React Stateは、動的なWebアプリケーションを作るための核となる概念です。

React Stateの重要ポイント:

  • コンポーネントが持つ内部データ
  • stateが変わると画面が再レンダリングされる
  • 関数コンポーネントではuseStateを使う

useStateの基本:

const [state, setState] = useState(初期値);
  • state:現在の値
  • setState:更新関数
  • useState:Reactのフック

State更新のルール:

  • 必ずsetter関数を使う
  • 配列・オブジェクトは新しいものを作る
  • 直接変更してはいけない

実践的なパターン:

  • フォームの管理
  • トグルスイッチ
  • カウンター
  • リスト操作

ベストプラクティス:

  • stateは必要最小限に
  • 計算で求められる値はstateにしない
  • 関連するstateはまとめる
  • 関数型更新を活用する

useStateをマスターすれば、ユーザーの操作に応じて変化する、リアクティブなアプリケーションが作れるようになります。

まずは簡単なカウンターから始めて、少しずつ複雑なstateの管理に挑戦してみてください。Reactの世界が広がりますよ!

楽しいReactライフを!

コメント

タイトルとURLをコピーしました