React入門第8回~Stateとは?~

Stateとは?

プロ太
プロ太

OZくん、今日はReactのStateについて学んでいきましょう!超重要です!!

OZ
OZ

State??何ですか?なんとなく重要そうだけど…。

Reactでは、コンポーネントが画面に表示される内容を管理してるんですが、このStateコンポーネント中で変化するデータを保持するために必要なんです!

なるほど…例えば、入力欄に文字を打ったときに表示が変わるような状況で、Stateは必要なんですか?

その通り!!Stateがなければ、入力した文字がReactに伝わらず、画面に反映されないんです。ちょっと簡単な例で説明してみますね!

const Example = () => {
  let displayVal; // 表示用の変数を用意

  return (
    <input
      type="text"
      onChange={(e) => {
        displayVal = e.target.value; // 入力された値をdisplayValに代入
        console.log(displayVal); // 値をコンソールに表示して確認
      }}
    />
  );
};

export default Example;

あれ?このコードではdisplayValに入力した値を入れてるけど、これだけだと画面が変わらないんですね?

そうなんです。Reactが画面の再レンダリングをするためには、Stateを使ってdisplayValを管理する必要があるんです。さっきのコードでは、displayValが変更されてもReactにはその情報が伝わらないんです。

じゃあ、どうやってReactに伝えるんですか?

それが次のテーマであるuseState関数なんです。useStateを使うと、Reactに状態を管理させることができるんです。

状態を管理…?

簡単に言うと、useStateを使えば、値が変わったときにReactが自動で画面を更新してくれるんです。使うためには、先程のコードを、以下のように書き換えられます。

import { useState } from 'react';

const Example = () => {
  const [displayVal, setDisplayVal] = useState(''); // 初期値は空文字

  return (
    <input
      type="text"
      onChange={(e) => {
        setDisplayVal(e.target.value); // 値を更新関数で変更
      }}
      value={displayVal} // 表示される値もdisplayValに依存
    />
  );
};

export default Example;

おお!useStateの中にあるsetDisplayValを使うと、自動でReactが画面を再表示してくれるんですね!!

そうです!これで、ユーザーが入力した内容がリアルタイムで画面に反映されるようになるんです。useState関数の仕組みについては、次に詳しく見てみましょう!!

useStateの役割と使い方

  1. 接続 (Hook into)
    • useStateを使うと、React内部でその状態が管理されるようになります
      これを「Hook into」と呼んでいます
  2. 現在の値と更新関数の返却
    • useStateは「現在の値」と「更新関数」の2つを返してくれます。さっきのコードでいうと、displayValが現在の値、setDisplayValが更新関数です
  3. 再レンダリングのトリガー
    • 更新関数setDisplayValを使って値を変えると、Reactが自動的にその変更を画面に反映してくれています

なるほど!useStateを使えば、Reactが自動的に画面を更新してくれるから、値を保持するのも簡単だし、ユーザーの操作に合わせて反映させるのも楽になるんですね!今から帰って実際に使ってみます!!ありがとうございました!

Stateの仕組みをイラストでイメージ

useStateの使い方と注意点

数日後、、、

プロ太先生、あれから、ReactでuseStateを使ってみてるんですけど、エラーが出たり、思った通りに動かなかったりして困ってます。何か使い方にコツがあるんですか?

useStateにはいくつか気をつけるポイントがあります。それを知っておくと、Reactの状態管理がもっと分かりやすくなりますよ!説明しますね。

1.コンポーネントの中で呼び出す

まず、useStateは必ずコンポーネントの中で使わないといけないんです。

コンポーネントの中って、つまり function Example() { ... } の中ですよね?
他の場所では使えないんですか?

使えません。useStateはReactのフック(Hooks)の一つなんですが、Reactのフックは、Reactコンポーネントの中でのみ動作するように作られているんです。もしコンポーネントの外や、ループや条件分岐の中で呼び出してしまうとエラーになるんですよ。

なんでエラーが出るんですか?

ReactはuseStateを使うコンポーネントを再レンダリングするたびに、同じ順番でフックが呼ばれるように管理しているんだ。もしif文の中でuseStateを呼ぶと、条件によって呼ばれる順番が変わる可能性があるから、エラーになってしまうんです。

なるほど、だからコンポーネントのトップレベルで使うのが基本なんですね!

値の更新と再レンダリングは予約(非同期)される

次の注意点はですね、useStateのset関数を使ってstateを更新すると、その変更はすぐには画面に反映されないんです。

えっ?どういうことですか?

Reactはstateの変更を‘予約’するんです。つまり、setCount(count + 1)と書いても、すぐにcountの値が画面に反映されるわけじゃなくて、次の再レンダリングが発生するタイミングで更新されるんです。

へぇ、非同期なんですね!それは、Reactが効率よくレンダリングを管理するための仕組みなんですか?

そうです!Reactはパフォーマンスを重視しているから、無駄に再レンダリングをしないように、必要なタイミングでまとめて画面を更新してくれるんだ。ちょっと、コードで例を見てみましょう!

import React, { useState } from 'react';

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

  const handleClick = () => {
    console.log("クリック前のcount:", count);
    setCount(count + 1);
    console.log("クリック後のcount:", count);
  };

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

export default Counter;

このコードで、<button>をクリックするとcountが増えるんですよね?
でも、handleClickの中のconsole.logcountを表示しているのに、なんでボタンをクリックしてもconsole.logcountが変わらないんですか?

実は、setCount(count + 1)を呼んでも、即座にcountの値が更新されるわけじゃないんです。さっきも言った通り、Reactは変更を予約して、次の再レンダリング時(returnで出力する時)に新しい値を反映するんです。

つまり、setCount(count + 1)を呼んだ直後のconsole.log("クリック後のcount:", count);ではreturnの前で、まだcountが変わっていないから、同じ値が表示されるってことなんですね?

その通り!実際に画面の表示が更新されるのは、次のレンダリング(return)が行われるときです。だから、直後のconsole.logには反映されません。でも、新しいcountの値は次の再レンダリング(return)で表示されるようになります。これが、Stateの注意点の1つです。

前回のstate値を使用する場合は更新関数に関数を渡す

ところで、状態を更新するときに前の値を使いたい場合もありますよね?
例えば、count を1ずつ増やしたいときとか。その場合、setCount(count + 1); と書いても、うまくいかないことがあるんです。

えっ、なんでですか?

さっきも言ったように、stateの更新は非同期的に行われるから、countの現在の値を直接使って更新すると、意図しない動作になることがあります。そんなときは、前の値を使って新しい値を計算するように、関数を渡すといいです。

setCount((prevCount) => prevCount + 1);

なるほど!これなら prevCount に前の count の値が入っているから、確実に1ずつ増やせるんですね!

その通り!特にボタンを連打するような状況では、これを使わないと予期しない結果になることが多いので、覚えておくといいですよ。

オブジェクト型のstateを更新する際には新しいオブジェクトを作成する

じゃあ、user というオブジェクトを state として使う例を見てみよう。

const [user, setUser] = useState({ name: "OZ", age: 17 });

ここで、user の name プロパティだけを更新したい場合、 user.name = '新しい名前' と直接書き換えたくなるかもしれませんよね。でも、これはやっちゃいけません。

どうしてですか?

React は state が変更されたことを検知して再レンダリングを行う仕組みなんだけど、state のオブジェクトを直接変更すると、React がその変更をうまく認識できないんです。

なるほど…じゃあ、どうすればいいんですか?

そこで、新しいオブジェクトを作成して、それを setUser 関数に渡す必要があります。このとき、...prevUser というのが出てくるんです。

え、...prevUser ってどういう意味なんですか?

スプレッド構文 (...) の意味を詳しく説明

この ...prevUser はスプレッド構文と呼ばれるもので、オブジェクトの内容を展開する役割があります。たとえば、user{ name: 'OZ', age: 17 } というオブジェクトだとすると、...prevUser と書くと、user オブジェクトの中身が { name: 'OZ', age: 17 } として展開されるんだよ。つまり、スプレッド構文を使うと、オブジェクトのすべてのプロパティがコピーされます。実際のコードも見ておきましょう。

setUser((prevUser) => ({ ...prevUser, name: '新しい名前' }));

ここで何が起きているかを分解してみると、

  1. prevUser: setUser に渡される関数の引数 prevUser は、現在の user の状態を表すオブジェクトです。つまり、 { name: 'OZ', age: 17 } です。
  2. { ...prevUser }: スプレッド構文 (...prevUser) を使って prevUser の中身を新しいオブジェクトに展開しています。これにより、新しいオブジェクトが { name: 'OZ', age: 17 } として作成されます。
  3. name: '新しい名前': 新しいオブジェクトに { name: '新しい名前' } というプロパティを追加しています。このとき、元々の name: 'OZ' が上書きされ、新しい name の値が '新しい名前' になります。

最終的に、次のようなオブジェクトが setUser に渡されます。

{ name: '新しい名前', age: 17 }

なるほど、だから setUser に渡されるのは age がそのままで、name だけが変わった新しいオブジェクトなんですね!

そうです!新しいオブジェクトを作成することで、React が state の変更を認識して、必要に応じて再レンダリングを行ってくれるんです。

stateの値はコンポーネントごとに独立して管理される

次に、stateはコンポーネントごとに独立していることも覚えておきましょう。
たとえば、Example というコンポーネントが2つあるとします。それぞれに count というstateがあっても、片方の count を更新してももう片方には影響しません。

なるほど!同じコンポーネントでも別々の状態を持つことができるんですね!

一度消滅したコンポーネントのstateの値はリセットされる

じゃあ、モーダルみたいに、表示を消してもう一度表示した場合、stateはどうなるんですか?

もし条件分岐でコンポーネントを一時的に非表示にして、再度表示させた場合、そのコンポーネントのstateは初期化されてしまいます。

なるほど!モーダルを一度閉じて再度開いたときに、入力内容がリセットされるのもそのためなんですね。

stateをpropsとして渡すことで子コンポーネントで利用可能

親コンポーネントで定義した state は、そのまま親コンポーネントの中だけで使うこともできるんだけど、props を使って子コンポーネントにも渡して利用することができますよ。具体的なコードを見てみましょう。

まず、親コンポーネントで count という state を定義します。

import { useState } from "react";
import ChildComponent from "./Childcomponent";

function ParentComponent() {
  const [count, setCount] = useState(0);
  const incrementCount = () => setCount(count + 1);

  return (
    <div>
      <h1>現在のカウント: {count}</h1>
      <ChildComponent count={count} />
      <button onClick={incrementCount}>ボタン</button>
    </div>
  );
}

export default ParentComponent;

このコードのポイントは、<ChildComponent count={count} /> という部分です。ここで count={count} として、countstateprops として子コンポーネントに渡しています。

子コンポーネントで props を受け取る

次に、子コンポーネント側で count を使えるようにします。ChildComponent は次のようになります。

import React from 'react';

function ChildComponent({ count }) {
  return (
    <div>
      <p>子コンポーネントでのカウント: {count}</p>
    </div>
  );
}

export default ChildComponent;

このコードでは、{ count } という記述で propscount を受け取っています。props は通常、props.count としてアクセスできますが、ここでは JavaScript の「分割代入(destructuring)」という書き方を使って、count だけを直接取り出して使っています。

親から子にデータが流れる仕組み
  1. 親コンポーネントで state を定義: ParentComponentcountuseState で管理しています。
  2. props として子コンポーネントに渡す: ParentComponent の中で、<ChildComponent count={count} /> として、countprops として ChildComponent に渡しています。
  3. 子コンポーネントで props を受け取って表示: ChildComponent は、親から渡された propscount を受け取って、子コンポーネントでのカウント: {count} として画面に表示しています。

なるほど!これで、親コンポーネントで管理している count のデータを、子コンポーネントでも使えるようになるんですね。

そうです!これが、React で state を共有する基本的な方法です。親から子に props を通してデータが流れることで、親の state を変更すれば、子コンポーネントでもその変化が反映されるようになります。

コンポーネントの位置によってはstateが維持される

あと、覚えておいてほしいのは、Reactで特定の条件によってコンポーネントを表示・非表示にするときに、key属性がないと、再レンダリングされたときにstateがリセットされる場合があることです。

状態を維持するためのコードの書き方

例えば、フォームに入力していた内容が突然消えたり、カウントしていた数がリセットされたりしたら困りますよね?条件付きでコンポーネントを表示・非表示にする場合、key属性を設定することで、Reactはそのコンポーネントを同一のものとして認識してくれます。これにより、状態が保持されるようになるんです。実際のコードで見てみましょう!

import { useState } from "react";

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

  return (
    <div>
      <h2>カウント:{count}</h2>
      <button onClick={() => setCount(count + 1)}>カウントを増やす</button>
    </div>
  );
}

function App() {
  const [showCounter, setShowCounter] = useState(true);

  return (
    <div>
      <button onClick={() => setShowCounter(!showCounter)}>
        カウンターを{showCounter ? "隠す" : "表示する"}
      </button>
      {showCounter && <Counter />}
    </div>
  );
}

export default App;
コードの説明
  1. Counterコンポーネント:
    • Countercountという状態を持ち、ボタンを押すとカウントが増える仕組みになっています。
  2. Appコンポーネント:
    • showCounterという状態を持ち、showCountertrueのときだけCounterコンポーネントを表示します。
    • 「カウンターを隠す/表示する」ボタンでshowCounterの値をトグルすることで、Counterを表示・非表示に切り替えます。
状態がリセットされる問題

これを実行すると、Counterコンポーネントを隠してもう一度表示すると、カウントがリセットされちゃいますね。

そうです。Counterコンポーネントを一度非表示にすると、そのコンポーネントがDOMから削除されるので、もう一度表示すると新しいインスタンスとして再生成されてしまい、カウントも初期値に戻ります。

解決策:コード修正
import { useState } from "react";

function Counter({ count, countIncrement }) {
  return (
    <div>
      <h2>カウント:{count}</h2>
      <button onClick={countIncrement}>カウントを増やす</button>
    </div>
  );
}

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

  const countIncrement = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <button onClick={() => setShowCounter(!showCounter)}>
        カウンターを{showCounter ? "隠す" : "表示する"}
      </button>
      {showCounter && <Counter count={count} countIncrement={countIncrement} />}
    </div>
  );
}

export default App;

主な変更点は以下になります。

  1. カウントの状態(count)をAppコンポーネントに移動
  2. Counterコンポーネントを制御コンポーネントに変更し、以下のpropsを受け取るように変更
    • count: 現在のカウント値
    • onIncrement: カウントを増やすための関数

おお〜!!カウントが保存されてる!!これは色んな場面で使えそうですね!!
プロ太先生!ありがとうございました!!useStateを使う上で注意すべきポイントがよく分かりました!

どういたしまして!これを意識しながら、実際にコードを書いてみるともっと理解が深まるはずだよ。Reactのstate管理をマスターすれば、もっと便利にアプリが作れるようになるので、ぜひ頑張ってくださいね!

まとめ

今回の記事の主なポイント、

  1. useStateの基本概念と必要性
    • コンポーネント内で変化するデータの管理
    • 画面の自動更新との連携
  2. useStateの重要な特徴
    • コンポーネント内でのみ使用可能
    • 状態更新は非同期で行われる
    • コンポーネントごとに独立した状態管理
  3. 実践的な使用方法
    • 前回のstate値を使用する場合の正しい更新方法
    • オブジェクト型のstate更新時の注意点
    • propsを使った親子コンポーネント間のstate共有
  4. よくある課題と解決策
    • コンポーネントの表示/非表示時のstate維持方法
    • 親コンポーネントでのstate管理による解決策
    • 状態を保持するためのコードの書き方の例