setStateで古いstateを見てしまった
基本的なところだけど、少し悩んでしまったのでメモ。
アプリのタイマー機能を実装しようとした際、setIntervalを使用しても時間が加算されない問題が発生した。 原因としては、setStateにオブジェクトを渡した場合、stateを即座にアップデートすることを保証しないというもの。
今回のような、setIntervalでsetStateが呼ばれるなど参照したいstateが絶えず更新されているものである場合、「setStateに更新用の関数を渡し、その関数内の引数で最新のstateにアクセスして操作する」のが正しい実装内容となる。
駄目な例
下記の例だと、setState
にtimeに1を足した値を引数として渡しているが、この時にtimeが参照しているのは更新前のtimeである。
つまり、0秒から時間の加算が始まったとして、
// timeは親のコンポーネントからもらってくる数値 const [time, setTime] = useState(props.time || null); useEffect(() => { const { taskId, recordingTaskId } = props; if (taskId && taskId === recordingTaskId) { // setIntervalで時間加算の関数(addSecond)を1秒毎に実行させる setTimerId(setInterval(addSecond, 1000)); } else return; }, [props.recordingTaskId]); const addSecond = () => { // ここでsetStateに「time + 1」という値を渡してしまっているため、常に古いstateを見てしまっている setTime(time + 1); };
良い例
// timeは親のコンポーネントからもらってくる数値 const [time, setTime] = useState(props.time || null); useEffect(() => { const { taskId, recordingTaskId } = props; if (taskId && taskId === recordingTaskId) { // setIntervalで時間加算の関数(addSecond)を1秒毎に実行させる setTimerId(setInterval(addSecond, 1000)); } else return; }, [props.recordingTaskId]); const addSecond = () => { // 更新関数を渡しているため、この中のtは最新のstateが入っていることが保証されている setTime(t => t + 1); };