React の関数コンポーネントで用いる useState は一つの関数コンポーネントの中で何度も使えます。この何度も使えるというのは useState を複数書けるということであり、useState の返り値であるゲッター、セッターを何度も呼べるということでもあります。この何度も呼ぶ、という処理はバグを生みやすく、そのよくある例と対策を紹介します。
次のデモとコードは何度も state のセッターを動かしても値が期待通りに変わらない状態のデモです。これはあるレンダリングの際の変数 a の値が不変であることを考慮できていないため起きます。
import { useState } from "react";
import "./styles.css";
export default function App() {
const [a, setA] = useState<number>(0);
const handle = (v: number) => {
for (let i = 0; i < v; i++) {
// setA(a + 1) を何度繰り返しても、
// ループの最初から最後までの間 a の値は変わらない
setA(a + 1);
}
};
return (
<div className="App">
<div>
<div>a: {a}</div>
</div>
<button onClick={() => handle(1)}>+1</button>
<button onClick={() => handle(2)}>+2</button>
</div>
);
}
これを解決するために次の様にこれまで計算した state の値を用いる機能を使います。これで同じ state のセッターを一度のレンダリングの中で複数回使っても期待通りに動作します。
フック API リファレンス – React#関数型の更新
import { useState } from "react";
import "./styles.css";
export default function App() {
const [a, setA] = useState<number>(0);
const handle = (v: number) => {
for (let i = 0; i < v; i++) {
// ここの新しい値を決める場所を関数に変更
setA((old) => old + 1);
}
};
return (
<div className="App">
<div>
<div>a: {a}</div>
</div>
<button onClick={() => handle(1)}>+1</button>
<button onClick={() => handle(2)}>+2</button>
</div>
);
}
これで一つの state について問題なく扱えますが、複数の state が協調する場合、また異なった問題が起きやすいです。次のデモとコードは a と同じ値をとることを期待した状態 b を増やしたコードです。これは期待通りに動きません。b のセッターではこれまで計算した a の値を用いる機能が使えないためです。
import { useState } from "react";
import "./styles.css";
export default function App() {
const [a, setA] = useState<number>(0);
const [b, setB] = useState<number>(0);
const handle = (v: number) => {
for (let i = 0; i < v; i++) {
setA((old) => old + 1);
}
// 前回の a の値を引数にした関数をセッターに取れない
setB(a);
};
return (
<div className="App">
<div>
<div>a: {a}</div>
<div>b: {b}</div>
</div>
<button onClick={() => handle(1)}>a+=1</button>
<button onClick={() => handle(2)}>a+=2</button>
</div>
);
}
上記の問題を解決したデモとコードが次です。
import { useState } from "react";
import "./styles.css";
export default function App() {
// a と b をまとめて一つの useState で使う
const [ab, setAB] = useState<{ a: number; b: number }>({ a: 0, b: 0 });
const handle = (v: number) => {
for (let i = 0; i < v; i++) {
setAB((old) => {
return {
...old,
a: old.a + 1
};
});
}
// 前回の a の値を引数にした関数をセッターに使える
setAB((old) => {
return {
...old,
b: old.a
};
});
};
return (
<div className="App">
<div>
<div>a: {ab.a}</div>
<div>b: {ab.b}</div>
</div>
<button onClick={() => handle(1)}>a+=1</button>
<button onClick={() => handle(2)}>a+=2</button>
</div>
);
}
この例の様に連携する必要のある state はまとめてオブジェクトにしておき、セッターの中で前回の連携対象の値の計算結果を参照することでバグを起こさずに動作させられます。
この記事では useState を取り扱いましたが、解決すべき問題があまりにも複雑であるならばいっそ useState を捨てて useReducer や他ライブラリ等を使った方がより楽にコーディングできます。
フック API リファレンス – React#useReducer