2025年5月25日日曜日

reactのuseStateの注意点備忘録


背景

reactとはmeta(旧facebook)が開発しているjavascriptの操作画面作成ライブラリです。
reactが提供するuseState利用すると、値を更新後にその値を利用して画面を再描画してくれます。

reactに慣れているつもりでしたが久々にreactを利用して画面を作ったところ意図せぬuseStateの挙動に戸惑ったので、備忘録として注意点を残します。

useStateの変数が配列の場合は元の値を変えない方式を使う(追加はpushではなくconcatかドットによる展開)

Updating arrays without mutationで公式が説明と利用例を提示してくれています。

javascriptの配列の要素変更で使われるpush、pop、reverseなどは、元の配列を変更する形式なため、その操作を行うと意図しない挙動が起こり得ます。

ドットを利用した展開で配列をコピーしてから操作するか、変更後の配列を返してくれるconcatなどの関数を使うのが良いです。
const [valArray, setValArray] = useSttate([])
// Bad
valArray.push(1)
setValArray(valArray)
// OK
setValArray(valArray.concat(1))
// OK
var newValArray = [...valArray]
newValArray.push(1)
setValArray(newValArray)

呼び出し場所によってはuseStateの変数が初期値のまま

useStateuseEffectを組み合わせてsetIntervalで1秒毎に値が変わる処理を下記のように記述すると、初期値+1の値しか生成されません。
// Bad
const [count, setCount] = useState(0)
useEffect(() => {
const idIntervalCountup = setInterval(() => {
setCount(count + 1) // 初期値+1にしかならない
}, 1000)
return () => {
clearInterval(idIntervalCountup)
}
}, [])
上記の不具合はsetIntervalがstateを考慮しない(第2引数が空の配列)useEffect内で定義されているため、setIntervalの中で呼ばれる変数が初期値のままになるのが原因です。
自分が把握する解決方法は2つあります。

解法1: 関数形式で値を更新

setの代入を現在の値を第1引数として受け取る関数として行うと期待通りに動きます。
// OK with functional update
const [count, setCount] = useState(0)
useEffect(() => {
const idIntervalCountup = setInterval(() => {
setCount((count) => { return count + 1 }) // <<== 変更箇所
}, 1000)
return () => {
clearInterval(idIntervalCountup)
}
}, [])
なお、上記の関数を利用する形式の値更新でも、配列は元の値を壊さない方式で更新が必要です。
参考: My initializer or updater function runs twice

解法2: useEffectの更新対象にstateの変数を設定

useEffectの更新対象としてstateの変数を渡す方式も一手です。
しかしながら、変数更新の度にjavascriptの時限式の処理を作り直すことになるので、自分は先程紹介した関数を利用した更新方法の方が好みです。
// OK with set vale to scope of useEffect
const [count, setCount] = useState(0)
useEffect(() => {
const idIntervalCountup = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => {
clearInterval(idIntervalCountup)
}
}, [count]) // <<== 変更箇所

おわり

「reactの基本機能のusetStateが意図するように動かない、なぜだ…」と思わぬ時間を取られましたが、配列などのobjectは元の値を壊さないように、素のjavascriptで実行させる処理は関数形式の値更新をするか値が変わるごとにjavascriptに埋め込む処理を更新すれば良いと分かりました。

参考

useState
useEffect
My initializer or updater function runs twice

0 件のコメント :