【React Hooks】useStateの注意点と解決策3選

Posted: February 23, 2021

useStateを使う上での注意点について解説していきます!useStateの基礎的な内容を知りたい方はuseStateの基本を理解しようをご覧ください👀

useStateの注意点

useStateを呼び出す場所に気を付ける

React Hooksは、

  • 関数コンポーネント・カスタムフック内
  • トップレベルのみ(ループや条件分岐・ネストされた関数内で呼び出してはいけない)

でしか呼び出すことができません。このルールを守らないとstateを正しく保持できなくなります。
[公式 : フックのルール(https://ja.reactjs.org/docs/hooks-rules.html)より]
例を踏まえて説明していきます。以下の例ではpropsで渡された値の真偽によってuseStateの呼び出しが変わります。つまりトップレベルで呼び出されていないことがわかります。

const UseStateAnti: React.FC<boolean> = (isEnabled) => {
  if (isEnabled) {
    const [count, setCount] = useState(0);
  }
	// 何らかの処理
	...

このコードはバグを起こす可能性があります。なぜなら、Reactはフックが呼ばれる順番に依存しているからです
フックの呼び出される順番がレンダリング毎に変化しない場合は、それぞれのフックに対してstateを割り当てることができますが、条件分岐などでフックを呼び出してしまうと、フックの順番が変化してしまいます。そうなるとReactはフックに何を割り当てるかわからなくなり、バグを引き起こす原因となります。

// First render
useState(0)
useState("hoge")
useEffect(updateTitle)

// Second render
// useState(1)  // このフックが条件分岐などでスキップされるとする
useState("hoge") // もともと2番目だったフックに1番目のフックが割り当てられる
useEffect(updateTitle)  // もともと3番目だったフックに2番目のフックが割り当てられる

このようなことが起きないように、Hooksのルールを強制できるeslint-plugin-react-hooksプラグインがあるので、使用することを推奨します。

破壊的変更をしない

以下の例のような、input要素の入力された値を配列のstateで管理するような関数コンポーネントについて考えます。

import React, { useState } from "react";

const UseStateAnti2: React.FC = () => {
  const [messages, setMessages] = useState<string[]>([]);
  const [message, setMessage] = useState("");
  const handleOnClick = () => {
    // pushを使った破壊的変更
    messages.push(message);
    setMessages(messages);
  };
  return (
    <>
      <h4>pushed list</h4>
      <ul>
        {messages.map((m, i) => (
          <li key={i}>{m}</li>
        ))}
      </ul>
      <input value={message} onChange={(e) => setMessage(e.target.value)} />
      <button onClick={handleOnClick}>追加</button>
    </>
  );
};

input要素に入力した値が変化する毎にstateであるmessage が変更され、追加ボタンがクリックされるとmessage を配列として管理するstateのmessagesmessageをpushしています。
しかしこのコードでは追加ボタンを押すと以下のような結果となります。

ご覧の通り、ボタンを押してもstateであるmessagesが変化していないことがわかります。この原因は、

messages.push(message);

にあり、Reactはstateの変更を検知して再レンダリングを行うのですが、そのstateの変更の判定にObject.is() を使用しています。(Object.is()についてはこちらを参照)
そのため同じ配列のstateで更新しても、変更は検知されず画面の再レンダリングもされないということです。

解決策

新しい配列を生成してstateを更新しましょう。具体的には、例を以下のように変更してください。

// ここの破壊的変更を取り消す
// messages.push(message);
// setMessages(messages);
// 新しく配列を生成して、stateを更新する
setMessages([...messages, message]);

このように修正すればstateを更新することができるようになります。


useStateで更新したstateは即座に更新されるわけではない

最後に注意すべき点としては、useStateで更新したstateは即座に更新されるわけではないということです。以下の例を見てみましょう。

import React, { useState } from "react";
const UseStateAnti3: React.FC = () => {
  const [count, setCount] = useState(0);
  const handleOnClick = () => {
    setCount(count + 1);
		setCount(count + 1);
    // 即座にstateが更新されるわけではない
    console.log(count);
  };

  return (
    <>
      <h3>count : {count}</h3>
      <button onClick={handleOnClick}>Click me</button>
    </>
  );
};



こちらボタンをクリックする度にstateであるcountを+2しようとしていますが、setCount直後のcountを見てみると、更新する前の値が出力されていることがわかるかと思います。さらに、setCountは2回呼び出されているのですが+1しかcountが更新されていません。
これは、setStateの呼び出しが非同期であるためです。countは2回めのsetCount時に値が更新されておらず、console.log呼び出し時にもそのままの値であることがわかります。

解決策

  • コールバック関数を用いて更新を行う

setStateにコールバック関数を用いると更新直前のstateを必ず参照できるので、更新直後のstateに依存する場合はこちらを使うとよいでしょう。

 const handleOnClick = () => {
		setCount((c) => c + 1);
		setCount((c) => c + 1);
  };
  • 新しい変数に更新する値を格納する

以下のように新しい変数に更新後の値を格納してしまえば参照することができます。

  const handleOnClick = () => {
		const clickCount = count + 1;
    setCount(clickCount);
    console.log(clickCount);
  };

まとめ

今回、useStateを使う際の注意点として、

  • 関数コンポーネント・カスタムフック内、トップレベルのみで呼び出す
  • stateは破壊的変更をせず、新しいstateで更新する
  • useStateで更新したstateは即座に更新されるわけではないため、更新直後のstateに依存する処理を行う場合は、関数を用いた更新をする

について説明しました!
この記事を参考に是非useStateを使いこなしてください🙌