fujjima’s blog

主に備忘録

setStateで古いstateを見てしまった

基本的なところだけど、少し悩んでしまったのでメモ。

アプリのタイマー機能を実装しようとした際、setIntervalを使用しても時間が加算されない問題が発生した。 原因としては、setStateにオブジェクトを渡した場合、stateを即座にアップデートすることを保証しないというもの。

コンポーネントの state – React

今回のような、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);
  };