LoginSignup
4
1

More than 1 year has passed since last update.

【React】useStateの基本と利用時の注意点まとめ

Posted at

■stateとは

state = 状態のこと。

■useStateとは

Reactデフォルトで提供する関数の1つ。
コンポーネント内で状態管理をしたい変数をこのuseState()を利用して宣言する。

■useStateの特徴

特定の値を保持・管理することができるのが特徴。


具体例として、inputフォームに入力した値を即座に出力するシステムが挙げられる。

const Example = () => {
  let value;

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

上記のようなExampleコンポーネントを作成し、inputに入力された際に入力値を変数valueに代入し、表示するというソースを実行してみる。

スクリーンショット 2022-12-17 20.34.02.png

結果は、consoleには出力されるものの、画面には表示されない。
理由は以下の2点。

①inputタグの外の{value}は、Exampleコンポーネントを再実行(再レンダリング)されないと反映されない。

②変数valueは、Exampleコンポーネントが再レンダリングする際に一番最初の宣言で初期化されるので、value変数は値が保持されず、常に何もないままである。


つまり入力した値を画面に表示するには、更新された値をどこかに保存しておく必要がある。
これを可能にするのが、useStateである。

■使い方

■基本構文

import { useState } from 'react';

const [value, setValue] = useState("");

useState()の引数には、初期値を代入する。

また、useState()の戻り値は以下のようになっている。

[
  string,  // 読み込み用の値
  React.Dispatch<React.SetStateAction<number>> // 変更用の関数
]

・第一引数 = 読み込み用の値
・第二引数 = 変更用の関数

※第一引数の型は必ずstringなわけではなく、useState宣言の際、初期値に何を設定したかによって変わる。


useState関数を実行した際には、
読み込み用の変数と、その変数を更新する用の関数を持った配列が戻り値となっている。

先ほどの基本構文、

const [value, setValue]

というのは、useStateの戻り値を分割代入していることを表している。
よって、以下のように取得することも可能。

const value = useState("");

value[0] // 読み込み用変数へのアクセス
value[1] // 更新用関数へのアクセス

しかし、この記述の仕方は基本使用されず、分割代入で書くことが基本。

■useStateを用いた具体例

先ほどの入力した値を表示する例を、useStateを利用したパターンに変えると以下の通り。

import {useState} from "react";

export default function App() {
  const [value, setValue] = useState("");

  const changeValue = (e) => {
    setValue(e.target.value);
  }

  return (
    <>
      <input
        type="text"
        onChange={changeValue}
      />
      = {value}
    </>
  );
}

スクリーンショット 2022-12-17 21.16.30.png


ここで、コンポーネントのトップにconsole.logを追加してみると、以下のようなことが起こる。
スクリーンショット 2022-12-17 22.15.35.png

レンダリングというconsole出力が10回も出力されている。

この結果から、inputタグに文字を入力する度にコンポーネントが再レンダリングされていることがわかる。

なぜ再レンダリングが起きるのか

これは、useStateのsetStateが以下の2つの役割をしているから。

①読み取り専用変数の更新する。

②Reactに対し、関数コンポーネントを再レンダリングするよう呼びかける。


この2つ目の役割があることにより、コンポーネントが再レンダリングされる。

■useStateの注意点

①useStateはコンポーネントのトップレベルでしか宣言できない。

トップレベルとは、関数コンポーネント内の{}の中のこと。

// OK
const Example = () => {
   const [value, setaValue] = useState();
   ...
}

// NG 1
const [value, setaValue] = useState();
const Example = () => {
   ...
}

// NG 2
const Example = () => {
  if() { // for文やwhile文も同様にNG
   const [value, setaValue] = useState();
  }
   ...
}

②更新は非同期で行われる

前の説明で、setStateは読み取り専用変数の更新を行うと説明したが、この更新は即座には行わない。

例として、以下のソースを実行してみる。

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [value, setValue] = useState(0);

  const changeValue = () => {
    setValue(value + 1);
    setValue(value + 1);
    console.log(value);
  };

  return (
    <>
      <button onClick={changeValue}>{value}</button>
    </>
  );
}

スクリーンショット 2022-12-17 22.25.30.png

上記ソースでは、setStateを2回実行し、その結果をconsole.log()で出力している。
setStateを2回実行しているので、結果は2ずつ増えていくかと思うが、結果は数値は1ずつ増えconsoleは初回は0でその後も1ずつ増えている。

・なぜこのようなことが起きるのか


この原因は、setStateの呼び出しは非同期であるから。
setStateが非同期で実行されているので、velueに値が反映されるのも当然非同期になる。

よって、setStateを実行した際は読み取り変数にはまだ+1は反映されておらず、valueには初期値の0が代入されている状態となる。結果、setStateは

setCount(0 + 1);
setCount(0 + 1);

ということになり、value変数に1をセットするという行為を2回続けて実行していることになる。

・setStateを呼び出した分値を増やす方法


実際には呼び出した分値を増やす方法もある。それはsetStateに関数を渡す方法である。

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [value, setValue] = useState(0);

  const changeValue = () => {
    setValue(value + 1);
    setValue(prev => prev + 1);
  };

  return (
    <>
      <button onClick={changeValue}>{value}</button>
    </>
  );
}

このようにsetStateに関数を渡すことで、引数prevには、
以前のsetValueでセットした(count + 1)がそのままセットされるようになる。

これは、Reactの仕様である。
スクリーンショット 2022-12-17 22.59.21.png

③オブジェクトをステートする際は、同じ形で設定する。

オブジェクトを管理する際は、更新を実行する際も同じ形で値を設定しなければいけない。

import "./styles.css";
import { useState } from "react";

export default function App() {
  const obj = { name: "sample", age: 22 };

  const [newObj, setNewObj] = useState(obj);

  const changeName = (e) => {
    setNewObj({ name: e.target.value, age: newObj.age });
  };
  const changeAge = (e) => {
    setNewObj({ name: newObj.name, age: e.target.value });
  };

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

スクリーンショット 2022-12-17 23.20.06.png

ポイントは以下の箇所。

  const obj = { name: "sample", age: 22 };

  const [newObj, setNewObj] = useState(obj);

  const changeName = (e) => {
    setNewObj({ name: e.target.value, age: newObj.age });
  };
  const changeAge = (e) => {
    setNewObj({ name: newObj.name, age: e.target.value });
  };

inputに名前もしくは年齢を更新した際に、それぞれに対応する箇所が更新するようになっているが、
更新する際は必ず初期値で設定したオブジェクトと同じ形で更新を記述しなければならない。

名前、もしくは年齢だけが更新されるからといって、以下のような記述をするとエラーになる。

const changeName = (e) => {
    setNewObj(name: e.target.value);
  };
  const changeAge = (e) => {
    setNewObj(age: e.target.value);
  };

また、以下のように記載した場合、記載されなかったプロパティは消滅する。

  const changeName = (e) => {
    setNewObj({ name: e.target.value });
  };
  const changeAge = (e) => {
    setNewObj({ age: e.target.value });
  };

④オブジェクトを更新する際は、必ず新規オブジェクトを作成する。

 オブジェクトを更新する際は異なる新しいオブジェクトを作成するという、useState上の制約が存在する。

先ほどのソースのsetStateを一部以下のように書き換えてみる。

const obj = { name: "sample", age: 22 };

const [newObj, setNewObj] = useState(obj);

const changeName = (e) => {
    obj.name = e.target.value;
    setNewObj(obj);
  };
  const changeAge = (e) => {
    setNewObj({ name: newObj.name, age: e.target.value });
  };

上記のソースは、inputで入力した値を定義した既存のオブジェクトobjのnameプロパティに代入し、setStateにobjを設定している。

この結果、changeNameを実行する方のinputタグが変更できなくなる。

先ほどの説明で記載していたソースの、

    setNewObj({ name: e.target.value, age: newObj.age });

    setNewObj({ name: newObj.name, age: e.target.value });

という箇所は、{}を記載して設定している。
{}はオブジェクトの新規作成を意味しており、そのオブジェクトのプロパティに対して新しい値を設定している。

オブジェクトの更新の際は必ず上記のような形で更新を行うこと。

⑤Stateの引き継ぎ

ReactのStateは、コンポーネント毎にStateを保持している。

import "./styles.css";
import { useState } from "react";

export default function App() {
  return (
    <>
      <Count title="A"/>
      <Count title="B"/>
    </>
  )
}

export const Count = ({ title }) => {
  const [count, setCount] = useState(0);

  const countUp = () => {
    setCount(prev => prev + 1);
  };

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

上記ソースを実行すると、ABは同じコンポーネントから作成しているが、それぞれが独立してStateを保持し、値を更新しているのがわかる。
スクリーンショット 2022-12-18 0.29.10.png

これはReactが、
React要素のツリー内の位置によってどのコンポーネントのStateかを判別している
からである。

逆にReact要素のツリー内の位置が同じ場合、Stateは引き継ぐという性質を持つ。
以下は、ボタンを押すとコンポーネントが切り替わるという処理を示したソースである。

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [toggle, setToggle] = useState(true);

  const changeToggle = () => {
    return setToggle((prev) => !prev);
  };
  return (
    <>
      <button onClick={changeToggle}>toggle</button>
      {toggle ? <Count title="A" /> : <Count title="B" />}
    </>
  );
}

export const Count = ({ title }) => {
  const [count, setCount] = useState(0);

  const countUp = () => {
    setCount(prev => prev + 1);
  };

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

まずAの状態で数値を増やす。
スクリーンショット 2022-12-18 0.36.51.png
その後toggleボタンを押しBに切り替えると、
スクリーンショット 2022-12-18 0.37.35.png
Bの値も同じ値になっていることがわかる。これがStateの引き継ぎである。

・同じ要素ツリー内でStateを独立させるには


同じツリーでそれぞれStateを独立させるには、それぞれのコンポーネントに対し、一意のKeyを設定する。
先ほどのソースの一部を以下のように変更する。

{toggle ? <Count title="A" key="A" /> : <Count title="B" key="B" />}

このようにkeyを設定することで、独立させることが可能になる。

Aの状態でカウントを増やす。
スクリーンショット 2022-12-18 0.42.46.png
その後toggleボタンを押下し、Bに切り替えると、
スクリーンショット 2022-12-18 0.43.26.png
Bが更新されず、独立していることがわかる。

Stateのリセットと保持

先ほどのStateの引き継ぎで作成したソース実行した際、

Aのカウントをあげる → Bに切り替える → またAに戻す

という動作を行うと、Aのカウントがリセットされ、0に戻っていることがわかる。
これは、切り替わった事でコンポーネントが消滅し、Stateがリセットされてしまったからである。

このような場面で、コンポーネントが消滅した後もStateを保持したい時には、

親コンポーネントでStateを管理し、Propsで子に値を渡す

という方法をとるのが良い。

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [toggle, setToggle] = useState(true);
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);

  const changeToggle = () => {
    return setToggle((prev) => !prev);
  };
  return (
    <>
      <button onClick={changeToggle}>toggle</button>
      {toggle ? (
        <Count title="A" key="A" count={countA} setCount={setCountA} />
      ) : (
        <Count title="B" key="B" count={countB} setCount={setCountB} />
      )}
    </>
  );
}

export const Count = ({ title, count, setCount }) => {
  const countUp = () => {
    setCount((prev) => prev + 1);
  };

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

ポイントは以下の箇所。

  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);

 <Count title="A" key="A" count={countA} setCount={setCountA} />

 <Count title="B" key="B" count={countB} setCount={setCountB} />

AとBそれぞれのStateを定義し、定義した読み取り変数とsetStateをそのままpropsとして渡している。
そして子コンポーネントがpropsを受け取って処理し、結果を反映している。

export const Count = ({ title, count, setCount }) => {
  const countUp = () => {
    setCount((prev) => prev + 1);
  };

  return (
    <>
      <p>{title}</p>
      <button onClick={countUp}>{count}</button>
    </>

ちなみに、親コンポーネントで定義するStateを1つだけにしてしまうと、
AとBで同じStateの値をpropsに渡していることになるので、切り替えても同じカウント数になってしまう。
必ずそれぞれのStateを定義すること。

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

 <Count title="A" key="A" count={count} setCount={setCount} />

 <Count title="B" key="B" count={count} setCount={setCount} />

参考文献

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1