React

【React】useState 値が更新できずにハマる

ねこくん
ねこくん
useStateで値を更新してるのに全然反映されないんだけど!
Yuki
Yuki
落ち着け。

それは更新のタイミングの問題なので、理解すれば簡単に解決できます。

先に対処法が知りたい方はこちらからジャンプしちゃってください🏃‍♂️

useStateで値が更新できない

やりたかったこと

onClickした時にuseStateのset関数を更新し、その値を別のset関数に渡したかった。

こんな感じ

💀 Bad

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const onClick = () => {
    // ↓1.ここでcountの数値を更新して、
    setCount((prevCount) => prevCount + 1);
    // ↓2.別の関数(setNum)に1.で更新したcountの値を渡したい
    setNum(count);
  };

  return (
    <div className="App">
      <p>{num}</p>
      <button onClick={onClick}>クリック!</button>
    </div>
  );
}

だがしかし!これだと意図した動きになってくれません。
初回のクリックでは0のままです
2回目のクリックからカウントアップされます。

なぜ値が更新されないのか?

まずはこちらをご覧ください。
更新されるタイミングがキモになりますので、各タイミングでconsole.logを記述しました。

export default function App() {
  console.log(`関数がレンダリングされた`);
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  console.log(`useStateが呼ばれた → count:${count}`);

  const onClickCountUp = () => {
    console.log(`クリックされた → count:${count}`);

    setCount((prevCount) => prevCount + 1);
    console.log(`新しい値でsetCountを呼んだ → count:${count}`);

    setNum(count);
  };
  return (
    <div className="App">
      <p>{num}</p>
      <button onClick={onClickCountUp}>クリック!</button>
    </div>
  );
}

初回のレンダリングで、
・レンダリングされた
useStateが呼ばれた-count:0

と出力されています。

では「クリック」してみてください。

  1. クリックされた count:0
  2. 新しい値でsetCountを呼んだ count:0
  3. 関数がレンダリングされた
  4. setCountが呼ばれた count:1

と出力されたと思います。
注目すべきはクリックされた後の2.新しい値でsetCountを呼んだ→count:0です。
つまり、クリックされた時点ではcountに更新された値が渡っていません。

そして、クリックした時点で再レンダリング(3.関数がレンダリングされた)され、
countに更新された値が渡っています。(4.useStateが呼ばれた→count:1

では、公式サイトの説明を見てみましょう。

ユーザがクリックした時に、新しい値でsetCountを呼びます。React はExampleコンポーネントを再レンダーし、その際には新たなcountの値を渡します。

引用元:React ステートフックの利用法
https://ja.reactjs.org/docs/hooks-state.html

※Exampleコンポーネントは公式サイトのデモで紹介されている関数です。

つまり、このような流れになります。

  1. クリックしてonClickCountUp関数が呼び出される
  2. 新しい値でsetCountを呼ぶ(ただしcountに値は渡されていない)
  3. 再レンダリングされる
  4. ここでようやくcountに値が渡される!

まとめると、onClickCountUpが呼ばれた後にcountの値が更新されるので初回クリックは0のままという事です。

これで解決!

それでは解決する2つの方法をご紹介します。

1.更新する値を新しい定数に代入する。
😍 Good!

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const onClickCountUp = () => {
    setCount((prevCount) => {
      const newCount = prevCount + 1;
      setNum(newCount);
      return newCount;
    });
  };
  return (
    <div className="App">
      <p>{num}</p>
      <button onClick={onClickCountUp}>クリック!</button>
    </div>
  );
}

7行目で新しい定数newCountに値を代入すると、更新された値を渡すことができます👍

2.useEffectを使う。
😍 Good!

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  useEffect(() => {
    setNum(count);
  }, [count]);

  const onClickCountUp = () => {
    setCount((prevCount) => prevCount + 1);
  };
  return (
    <div className="App">
      <p>{num}</p>
      <button onClick={onClickCountUp}>クリック!</button>
    </div>
  );
}

そして2つ目の方法です。
onClickCountUp内で値が渡されないなら、外に出してあげれば良いじゃない😄
という事で外に出してあげました。

ねこくん
ねこくん
やった〜できた〜!

が、ここで注意がひとつ!

ねこくん
ねこくん
なになに??

Yuki
Yuki
そのまま外へ出してしまうと。。。

💀 Bad

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  setNum(count);

  const onClickCountUp = () => {
    setCount((prevCount) => prevCount + 1);
  };
  return (
    <div className="App">
      <p>{num}</p>
      <button onClick={onClickCountUp}>クリック!</button>
    </div>
  );
}

無限レンダリングが起きてしまいます!
※良い子はマネしないでね🐥

↓無限レンダリングで表示されるエラーメッセージ

ErrorToo
Too many re-renders. React limits the number of renders to prevent an infinite loop.

なぜ無限レンダリングが起きるか?

stateが更新された時に再レンダリングされるというReactの性質が関係しています。

つまり今回の例では、

初回レンダリング → setNumcountが渡されて更新 → 再レンダリングsetNumcountが渡されて更新…

となり、無限レンダリングの誕生です。

useEffectで無限レンダリングを解決する

useEffectを使うことにより、特定の変数に変更があった時だけお望みの関数を実行させることができます。
今回の場合ではcountが更新された時だけ、setNumを実行させます。

useEffect(() => {
 setNum(count);
 }, [count]); // ← 第2引数にcountを設定
 

こうする事で、countが更新された場合のみsetNum(count)が実行され、無限レンダリングは起きません。

↓ちなみに、第2引数を空にすると初回のみの実行になり、countが変更されてもsetNum(count)は更新されません。(何回クリックしても表示されている数字は0のままです。)

useEffect(() => {
 setNum(count);
 }, []); // ← 第2引数を空にすると、初回の1回だけ実行される
 

ちなみに(2回目)、今回の場合、↓第2引数を設定しなくても無限レンダリングは解決できます。

useEffect(() => {
 setNum(count);
}); // ← 第2引数の設定なし
 

これは、useEffectはコンポーネントが更新される度に実行されるという性質によるものだと思います。
今回はこの方法でも問題ありませんが、開発が進んでコード量が多くなった時、どんな動きをしているのかパッと見で分からなかったり、バグの原因になりそうな気配がします。

なので、個人的には第2引数を設定して明示する方法が良いかなと思ってます^^

いかがでしたでしょうか?
分かりにくい点や「そこ間違ってる!」などございましたら、そっと優しくコメントいただけたら幸いです。

Yuki
Yuki
またね!

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です