🪝

React useStateを理解する

2024/01/27に公開
2

現在Reactを一から学習中ですが、今回はReactの中でも最も利用されるフックであるuseStateを理解するため、学んだ内容をまとめました。

useStateとは

useStateはReactフックの一つで、関数コンポーネントのstateを保持したり、更新したりするためのフックです。
stateとは、フォームに入力されたデータなど、アプリケーションが保持している状態のことです。

useStateの記述方法

useStateは以下のような形で記述します。

const [stateの変数, stateを更新する関数] = useState(stateの初期値);

stateの値を更新する

stateの値を更新する場合は、useStateの左辺で指定した更新関数を実行します。
更新関数を実行すると、更新関数の引数で渡された値がReact内部で保持されます。

以下はテキストボックスに値を入力する度に、stateの値を更新する例です。
onchangeイベントが発火する度にsetVal関数が実行され、入力された値でstateの値が更新されます。

const Example = () => {
  let [val, setVal] = useState("");

  return (
    <>
      <input type="text" onChange={ (e) => {setVal(e.target.value);} }/>
      {val}
    </>
  );
};

また更新関数が実行されると、stateの値を更新した後に、Reactへ自身の関数コンポーネントを再実行(再レンダリング)するよう依頼します。

stateの値は即時更新されない

更新関数でstateの値を更新しても即時更新されません。
更新関数で更新した値は、関数コンポーネントが再実行されたタイミングで反映されます。

そのため、以下のようにstateの値であるcountをカウントアップする更新関数を2回実行しても、カウントは+1にしかなりません。

const Example = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount(count + 1);
    setCount(count + 1);
  }

  return (
    <>
      <button onClick={countUp}>+</button>
      {count}
    </>
  )
};

1回目の更新関数で変更した値を利用する場合は、更新関数の引数に関数を記述することで実現できます。
以下のようにsetCountに関数を渡すことで、引数prevには、直前のsetCountでセットしたcount + 1がそのままセットされるので、countは+2ずつカウントアップされます。

const Example = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount(count + 1);
    setCount(prev => prev + 1); // prevには、直前で更新された値「count + 1」がセットされる
  }

  return (
    <>
      <button onClick={countUp}>+</button>
      {count} 
    </>
  )
};

オブジェクト型の値をstateにセットする

stateには、プリミティブ型だけではなくオブジェクト型の値もセットできます。
stateにオブジェクト型の値をセットする場合は、オブジェクトで初期値をセットします。

stateの値を更新する際は、更新関数の引数に同じ構造のオブジェクトを渡します。

const Example = () => {
  const personObj = { name: "Ken", age: 20 };
  const [person, setPerson] = useState(personObj);

  const changeName = (e) => {
    setPerson({ name: e.target.value, age: person.age })
  }
  const changeAge = (e) => {
    setPerson({ name: person.name, age: e.target.value })
  }

  return (
    <>
      <h3>Name:{person.name}</h3>
      <h3>Age:{person.age}</h3>
      <input type="text" value={person.name} onChange={changeName} />
      <input type="number" value={person.age} onChange={changeAge} />
    </>
  );
};

注意点としては、更新関数で渡すオブジェクトは新しいオブジェクトである必要があります。
そのため、以下のようにstateにセットされているオブジェクトのプロパティを変更して渡しても、更新することはできません。

const Example = () => {
  const personObj = { name: "Ken", age: 20 };
  const [person, setPerson] = useState(personObj);

  const changeName = (e) => {
    person.name = e.target.value;
    setPerson(person) // 新しいオブジェクトではないため更新されない
  }

更新関数には同じ構造のオブジェクトを渡す必要があるため、オブジェクトのプロパティを全て記述する必要があります。
ただプロパティの数が多いと記述が面倒になりますので、そういった時はスプレッド演算子を利用する事で簡単に記述することができます。

const Example = () => {
  const personObj = { name: "Ken", age: 20 };
  const [person, setPerson] = useState(personObj);

  const changeName = (e) => {
    setPerson({ ...person, name: e.target.value }) // personオブジェクトが展開されるので、更新したいプロパティだけ記述すればよい
  }

おわりに

useStateについて、基本的な記述方法や動作は理解できましたが、まだまだ奥が深い機能ですので、引き続き学習を進めていきたいと思います💪

コラボスタイル Developers

Discussion

Honey32Honey32

失礼します。 「stateの値は即時更新されない」 のソースコードの挙動説明が誤っています。実際に動かして確認してみることをオススメします。

const Example = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    // count が 0 のときの挙動
    setCount(count + 1); // countの値を 1 に更新しようとする
    // setCount(count + 1); setCount(prev => prev + 1); をいくら書いても以下の結果は変わりません

    console.log("synchronous:", count); // ここでの count は更新される前の値 (0) のまま。

    setTimeout(() => {
      console.log("setTimeout 10s :", count); // ここにおいても count は更新される前の値 (0) のままです
    }, 10000);
  })

  return (
    <>
      <button onClick={countUp}>+</button>
      {count}
    </>
  )
};

「set〇〇 が非同期に更新される」だけでは不十分で、ステートの挙動は JS におけるクロージャーの基本的な仕組みに従っている》ことについて、以下の記事が参考になれば幸いです。

https://qiita.com/honey32/items/ee8d1577e68b0d58678d

konykony

コメントありがとうございます!
ご指摘のとおり、誤った説明になっていましたので修正しました🙏

またstateの挙動についての参考記事もありがとうございます!
公式ドキュメントと合わせて確認してみます。