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