はじめに
useState
の値を合計にして、一気に計算しようとしたところ、うまく合計の計算ができなかった。
問題
以下のコードを実行した結果totalの値が、NaNになってしまいます。
import { useEffect, useState } from 'react';
function App() {
// 1〜10 の数値データ
const arr = [...Array(3).keys()];
const [total, setTotal] = useState(0);
useEffect(() => {
arr.map((record) => {
setTotal(parseInt(total) + parseInt(record));
});
}, []);
return (
<div style={{ padding: 16 }}>
<p>期待値: 55(1〜10 の合計)</p>
<p>実際のtotal: {total}</p>
</div>
);
}
export default App;
これの原因が、map関数とuseStateです。
map関数で、配列分繰り返してstateの値を更新するイメージでした。
しかし、この場合だと値は更新されず、一番最後の値が、setTotal
によって更新されて、total
に入ってしまいます。
結論から言うと、「1回のレンダリング中では、stateの“値そのもの”は固定(1回だけ)」と言うことです。
map関数はレンダリング中に繰り返し処理を行ってますが、setTotal
に関しては、まだ処理を行わず、全てが終わってから値が更新されるので、一番最後の値が入って更新がされると言うわけです。
解決方法
3つ解決方法がありましたので共有です。
解決方法1 forEach(おすすめしない)
1個目の解決方法はforEach
を使うことです。配列と繰り返しといえば!!と言う手法ですね。
- useEffect(() => {
- arr.map((record) => {
- setTotal(parseInt(total) + parseInt(record));
- });
- }, []);
+ arr.forEach((record) => {
+ setTotal(parseInt(total) + parseInt(record));
+ });
おすすめしない理由としては、ループ回数分のレンダリングが回ってしまうからです。
解決方法2 useStateの中に関数を記載。(おすすめしない)
こちらの仕方がシンプルですね。
- useEffect(() => {
- arr.map((record) => {
- setTotal(parseInt(total) + parseInt(record));
- });
- }, []);
+ useEffect(() => {
+ arr.map((record) => {
+ setTotal((prev) => prev + record);
+ });
+ }, []);
useStateの中に関数を書くことができます。このやり方だと、最新の値を逐次反映してくれます。ただし開発モードで行なってるとレンダリングが2回行われますので、合計が2倍になります。StrictMode
を外せば解決しますが、開発段階で外すのはおすすめしないです。
- <StrictMode>
<App />
- </StrictMode>
解決方法3 reduceを使う。(おすすめ)
reduce()は、配列を畳むメソッドです。今回は配列の中身を足して、1つの変数にまとめ、それをsetstate
で値を設定していきます。
- useEffect(() => {
- arr.map((record) => {
- setTotal(parseInt(total) + parseInt(record));
- });
- }, []);
+ useEffect(() => {
+ const sum = arr.reduce((acc, record) => acc +
+ parseInt(record), 0);
+ setTotal(sum);
+ }, []);
おわりに
いざ使ってみると忘れていたり、使えないことが多いので備忘録して残しておきます。
#参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce