【React】useStateの使い方と注意点まとめ

React

ReactのuseStateの使い方を紹介します。

ReactのuseStateとは?

useStateを使うことで、リアルタイムで画面更新が可能になります。
Reactにコンポーネントの再実行(再レンダリング)を依頼して、新たなReact要素を作成します。
そして、そのためには変更した値をどこかに保持しておく必要があります。
それが、stateという場所に保存します。
これらを行う仕組みがuseStateという関数です。

useStateの使い方

import { useState } from 'react';

const Sample = () => {
  // useStateの()には初期値を設定する
  let arr = useState(0);
  // (2) [0, ƒ] 配列の0番目は参照用の値、1番目は更新用の関数
  const handler = (e) => {
    const setFn = arr[1];
    setFn(e.target.value);
  }
  return (
    <>
      <input type="text" onChange={handler} />
      <p>入力値:{arr[0]}</p>
    </>
  )
}

export default Sample;

まずは、useStateをreactからimportします。
そして、useStateの()には初期値を設定します。
useStateは配列になっています。
配列の0番目は参照用の値、1番目は更新用の関数です。
そのため、最初は、配列の0番目は参照用の値にはデフォルト値が表示されます。

分割代入を使ってuseStateを整理する

import { useState } from 'react';

const Sample = () => {
  // 分割代入で取り出す
  let [val, setVal] = useState(0);
  const handler = (e) => {
    setVal(e.target.value);
  }
  return (
    <>
      <input type="text" onChange={handler} />
      <p>入力値:{val}</p>
    </>
  )
}

export default Sample;

上のように、最初から分割代入でuseStateの参照用の値と関数を分けておくことで、
よりすっきりとしたコードになります。

useStateの仕組み

React内部と接続(Hook into)して、状態管理が必要ですよと伝えます。
状態が管理されるようになります。
useStateはreact hooksと呼ばれます。

「現在の値」と「更新関数」を返却します。これが、上でいうvalとsetValとして分割代入で得たものです。

そして、更新関数で新しい値をReactに渡します。
そして、Reactにコンポーネントを再レンダリングするように依頼します。

stateは、コンポーネントごとに状態、つまり値を保持しています。
つまり、stateは、コンポーネントに紐づいているため、それぞれのコンポーネントで独立して管理されています。
React要素のツリーの中位置によってどのコンポーネントのstateかを識別しています。

state(状態)とは?

stateとは、コンポーネントごとに保持されたり、管理される値のことです。

コンポーネント内に定義した通常の変数は、レンダリングのときには初期化されて保持されません。
レンダリングをしても値を保持したいとき、このstateに保持する必要があるということです。

useStateを複数使う

import { useState } from 'react';

const Sample = () => {
  const [val, setVal] = useState(1);
  const [val2, setVal2] = useState(10);
  const [val3, setVal3] = useState(100);
  const handler = (e) => {
    setVal(val + 1);
  }
  const handler2 = (e) => {
    setVal2(val2 + 1);
  }
  const handler3 = (e) => {
    setVal3(val3 + 1);
  }
  return (
    <>
    <p>ボタン1を{val}回!</p>
    <button onClick={handler}>ボタンA</button>
    <p>ボタン2を{val2}回!</p>
    <button onClick={handler2}>ボタンA</button>
    <p>ボタン3を{val3}回!</p>
    <button onClick={handler3}>ボタンA</button>
    </>
  );
};

export default Sample;

上のようにそれぞれの変数と更新用関数、イベントハンドラーを用意することで複数のuseStateを使うことができます。

useStateの注意点

useStateは、関数コンポーネントのトップレベルか、カスタムHookの中でしか呼ぶことができません。
注意点としては、if文やfor文などもブロックが形成されるため、
useStateが使えません。
理由としては、React内部でコンポーネントごとにuseStateを順番に管理しているためです。
そのため、ブロックを形成するif文などを入れるとその順番がおかしくなり、Reactでエラーがでます。
具体的には、コンポーネントをコンソールで出力すると、ower.memorizedStateというプロパティがあります。
その中のnextというプロパティに、次のuseStateが割当られ、さらにその中のnextに次のuseStateが割当られ、、、
という形状になっています。

useStateの注意点:即時反映されない

import { useState } from 'react';

const Sample = () => {
  const [val, setVal] = useState(1);
  const countUp = (e) => {
    setVal(val + 1);
    console.log(val); //即時に反映されない
  }
  const countDown = (e) => {
    setVal(val - 1);
  }

  return (
    <>
    <p>現在: {val}回!</p>
    <button onClick={countUp}>+</button>
    <button onClick={countDown}>-</button>
    </>
  );
};

export default Sample;

上のように、コンソールでクリック後の値を確認しようとすると、反映されていないことがわかります。
useStateの更新用の関数は、あくまで再レンダリングしてねという依頼をしているだけのため、まだ値自体は更新されていないからです。

useStateの注意点:参照用の値を一度に何度変更しても同じ

import { useState } from 'react';

const Sample = () => {
  const [val, setVal] = useState(1);
  const countUp = (e) => {
    setVal(val + 1);
    setVal(val + 1);
    console.log(val); //即時に反映されない
  }
  const countDown = (e) => {
    setVal(val - 1);
  }

  return (
    <>
    <p>現在: {val}回!</p>
    <button onClick={countUp}>+</button>
    <button onClick={countDown}>-</button>
    </>
  );
};

export default Sample;

上のように、setValという更新用の関数で一度に値を更新しても、参照用の値であるval自体が同じ値(更新される前の値)のため、
値は一回分しか変更されません。
更新用の関数がコンポーネントの再レンダリングを依頼するのも、値が更新されるのも、非同期で行われます。
よって、コンソールでの確認には注意しましょう。

useStateで前の値を複数回更新する方法

import { useState } from 'react';

const Sample = () => {
  const [val, setVal] = useState(1);
  const countUp = (e) => {
    setVal(val + 1);
    setVal(prevState => prevState + 1);
    console.log(val); //即時に反映されない
  }
  const countDown = (e) => {
    setVal(val - 1);
  }

  return (
    <>
    <p>現在: {val}回!</p>
    <button onClick={countUp}>+</button>
    <button onClick={countDown}>-</button>
    </>
  );
};

export default Sample;

上のprevstateように、引数を用意して、それに変更を加えるようにすることで、
将来に実行を約束するのがただの値の変更ではなく、指定した関数の実行を予約することになるため、
前の値を複数回変更することも可能です。

useStateの注意点:オブジェクト型の更新には、更新するプロパティ以外のプロパティも記述する

import { useState } from "react";

const Sample = () => {
  const obj = { name: "yamada", age: 54, country: '日本' };
  const [person, setPerson] = useState(obj);
  const changeAge = (e) => {
    // ここでオブジェクトを返してあげる必要がある
    //setPerson({name: person.name, age: e.target.value, country: person.country});
    setPerson({...person, age: e.target.value});
  }
  return (
    <>
      <p>{person.name}</p>
      <p>{person.age}</p>
      <p>{person.country}</p>
      <input type="text" onChange={changeAge} />
    </>
  )
};

export default Sample;

上のように、オブジェクトを指定した場合、更新用の関数で値を更新するときは、
たとえ値が一つだけ変更する場合であっても同じオブジェクトのカタチを返してあげる必要があります。
そうしないと、他の値が更新時にないものとされます。
そのため、必ず、オブジェクトを更新するときは、更新するプロパティ以外のプロパティも記述します。
また、上のようにスプレッド構文を使うことで、何個プロパティがあっても、更新したいものだけ、あとに記述するだけでよくなります。

useStateの注意点:オブジェクト型の更新には、新しいオブジェクトを生成する必要がある

import { useState } from "react";

const Sample = () => {
  const obj = { name: "yamada", age: 54, country: '日本' };
  const [person, setPerson] = useState(obj);
  const changeAge = (e) => {
    // ここでオブジェクトを返してあげる必要がある
    // setPerson({name: person.name, age: e.target.value, country: person.country});

    // これではうまく動かない
    person.age = e.target.value;
    setPerson(person);

  }
  return (
    <>
      <p>{person.name}</p>
      <p>{person.age}</p>
      <p>{person.country}</p>
      <input type="text" onChange={changeAge} />
    </>
  )
};

export default Sample;

上のように、すでにあるオブジェクトのプロパティの値のみを変更しようとすると、動きそうなものですが、
これでは動きません。エラーも出ないため、気づきにくいポイントになります。
参照用の値には、必ず新しいオブジェクトを生成して、そのオブジェクトに対して新しい値を設定する必要があります。

  //このような書き方もある
  const orderObj = { item: "apple", count: 10 };
  const [order, setOrder] = useState(orderObj);
  const changeItem = (e) => {
    setOrder(order => ({ ...order, item: e.target.value }));
  };
  const countUp = () => {
    setOrder({
      ...order,
      count: order.count + 1,
    });
  };
  const countDown = () => {
    setOrder({
      ...order,
      count: order.count - 1,
    });
  };

ちなみに、上のように、orderというコールバック関数を戻り値として、オブジェクトを返したいとき、
上のような書き方ができます。
オブジェクトを渡す場合とコールバックを渡す場合で挙動が変わるため、なるべく1つ目のように、
オブジェクトをそのまま返すのではなく、コールバック関数で返すのがいいです。
({})という形式が見慣れない場合がありますが、
()の中に、{}でオブジェクトを書くのは、オブジェクトリテラルの{}かアロー関数の{}か不明になるため、
()をつけることでオブジェクトリテラルであることを明記するためにそのようなルールになっています。

useStateで配列を取り扱う

import { useState } from "react";

const Sample = () => {
  const numArray = [1, 2, 3, 4, 5];
  const [nums, setNums] = useState(numArray);
  const changeOrder = () => {
    // 新しい配列を作成
    const newNums = [...nums];
    // 先頭の値を取り出す
    const firstVal = newNums.pop();
    // 最後に付け足す
    newNums.unshift(firstVal);
    // 更新用関数にセットする
    setNums(newNums);
  };
  return (
    <>
      <p>{nums}</p>
      <button onClick={changeOrder}>click</button>
    </>
  );
};

export default Sample;

配列では、オブジェクトと同様、新しい配列を作成する必要があるため、スプレット構文を使って
新しい配列を作成します。
return分では、{}に参照用の値をいれるだけで、配列が展開されて表示させることができます。

ステートとコンポーネント

Reactは、コンポーネントツリーの位置でstateの値を引き継ぐので、
三項演算子を使って同様のコンポーネントを切り替えたりするときなどは注意が必要です。
たとえば、以下のような例です。

import { useState } from "react";

const Sample = () => {
  const [toggle, setToggle] = useState(true);
  const toggleComponent = () => {
    setToggle((val) => !val);
  };
  return (
    <>
      <button onClick={toggleComponent}>トグル</button>

      {/* このコンポーネントでstateを引き継ぐ現象が確認できる */}
      <p>{toggle ? <Count componentName="1" /> : <Count componentName="2" />}</p>
    </>
  );
};
const Count = ({componentName}) => {
  const [count, setCount] = useState(100);
  const countDown = () => {
    setCount((prevState) => prevState - 1);
  };
  return (
    <>
    <p>{componentName} : {count}</p>
    <button onClick={countDown}>-</button>
    </>
  )
};

export default Sample;

これは、toggleでコンポーネントを切り替えているので、コンポーネントのツリー内の位置が全く同じため、
stateを引き継ぐことになります。
一方にdivタグなど、別のツリー構造にすれば、stateは引き継がれません。

同じツリー構造でstateを引き継がないようにする方法

import { useState } from "react";

const Sample = () => {
  const [toggle, setToggle] = useState(true);
  const toggleComponent = () => {
    setToggle((val) => !val);
  };
  return (
    <>
      <button onClick={toggleComponent}>トグル</button>

      {/* このコンポーネントでstateを引き継ぐ現象が確認できる */}
      <p>{toggle ? <Count key={"1"} componentName="1" /> : <Count  key={"2"} componentName="2" />}</p>
    </>
  );
};
const Count = ({componentName}) => {
  const [count, setCount] = useState(100);
  const countDown = () => {
    setCount((prevState) => prevState - 1);
  };
  return (
    <>
    <p>{componentName} : {count}</p>
    <button onClick={countDown}>-</button>
    </>
  )
};

export default Sample;

同じツリー構造でstateを引き継がないようにするには、コンポーネントにkey属性を設定します。
それぞれユニークな値を設定することで一意に決まるため、たとえコンポーネントがツリー内で全く同じに位置していても、
stateは引き継がれません。
ただし、上の実装では、コンポーネントを切り替えると、値がリセットされてしまいます。
意図してやるなら問題ありませんが、複数コンポーネントのステートを管理したい場合があります。

コンポーネントが消滅してもステートを保持したい

import { useState } from "react";

const Sample = () => {
  const [toggle, setToggle] = useState(true);
  // このstateを子ではなく親で管理して、子にはpropsで渡す
  // 同じstateだと値が共有されてしまうまた、それぞれ作成
  const [count1, setCount1] = useState(100);
  const [count2, setCount2] = useState(100);

  const toggleComponent = () => {
    setToggle((val) => !val);
  };
  return (
    <>
      <button onClick={toggleComponent}>トグル</button>

      {/* このコンポーネントでstateを引き継ぐ現象が確認できる */}
      <p>
        {toggle ? (
          <Count
            key={"1"}
            componentName="1"
            count={count1}
            setCount={setCount1}
          />
        ) : (
          <Count
            key={"2"}
            componentName="2"
            count={count2}
            setCount={setCount2}
          />
        )}
      </p>
    </>
  );
};
const Count = ({ componentName, count, setCount }) => {
  // このuseStateを親で管理する
  // const [count, setCount] = useState(100);
  const countDown = () => {
    setCount((prevState) => prevState - 1);
  };
  return (
    <>
      <p>
        {componentName} : {count}
      </p>
      <button onClick={countDown}>-</button>
    </>
  );
};

export default Sample;

コンポーネントが消滅してもステートを保持したい場合は、子コンポーネントで管理していたstateを親コンポーネントで管理して、
propsで子コンポーネントにわたすことで可能になります。
その際、stateはそれぞれの子コンポーネントで必要になる点に注意が必要です。

このように、stateをpropsで渡すケースがあります。
コンポーネントが消滅するときと、特定のstateを複数の子コンポーネントで共有するときは、stateをpropsで渡します。

useStateの使用上の注意まとめ

  • コンポーネントの中で呼び出すこと。
  • if文などブロックを生成するものの中で呼び出さないこと。
  • 値の更新と再レンダリングは非同期で予約される。(即時反映されない)
  • 前のstateの値を使いたいときは、更新用関数にコールバック関数を渡すこと。
  • オブジェクト型のstateを更新するときは、新しいオブジェクトを作成すること。
  • stateの値は、コンポーネントごとに独立して管理されていること。
  • 一回消滅したコンポーネントのstateはリセットされるので、key属性を使うこと。
  • stateを親コンポーネントで定義して、propsで渡すことで、複数の子コンポーネントで別々に値を保持できる。
  • コンポーネントの位置がstateの値と紐付いていること。

React

Posted by devsakaso