それは更新のタイミングの問題なので、理解すれば簡単に解決できます。
先に対処法が知りたい方はこちらからジャンプしちゃってください🏃♂️
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
と出力されています。
では「クリック」してみてください。
- クリックされた → count:0
- 新しい値で
setCount
を呼んだ → count:0 - 関数がレンダリングされた
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コンポーネントは公式サイトのデモで紹介されている関数です。
つまり、このような流れになります。
- クリックして
onClickCountUp
関数が呼び出される - 新しい値で
setCount
を呼ぶ(ただしcount
に値は渡されていない) - 再レンダリングされる
- ここでようやく
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
内で値が渡されないなら、外に出してあげれば良いじゃない😄
という事で外に出してあげました。
が、ここで注意がひとつ!
💀 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の性質が関係しています。
つまり今回の例では、
初回レンダリング → setNum
にcount
が渡されて更新 → 再レンダリング → setNum
にcount
が渡されて更新…
となり、無限レンダリングの誕生です。
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引数を設定して明示する方法が良いかなと思ってます^^
いかがでしたでしょうか?
分かりにくい点や「そこ間違ってる!」などございましたら、そっと優しくコメントいただけたら幸いです。