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

Posted: 2021/2/24
React/
React Hooks

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

useState の注意点

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

React Hooks は、

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

でしか呼び出すことができません。このルールを守らないと state を正しく保持できなくなります。

Hooks Rule

例を踏まえて説明していきます。以下の例では 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 しています。

しかしこのコードでは追加ボタンを押すと以下のような結果となります。

useState-pitfall1

ご覧の通り、ボタンを押しても 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-pitfall2

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>
    </>
  );
};

useState-pitfall3

こちらボタンをクリックする度に 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 を使いこなしてください 🙌