react HooksでsetTimeoutやsetIntervalなどからstateを参照する

react HooksでsetTimeoutやsetIntervalなどからstateを参照する場合はuseRefを使用するといいらしい。
カスタムイベントなど独自のものを実装する際にも同じ手が使えると思う。

stateの値を読み出すダメな例

コンポーネントがマウントされた際にsetInterval関数を実行し、5秒ごとにコンソールにボタンのクリック回数を出力しようとしている。しかし実際には常に0回と表示されてしまう。

setIntervalを呼び出す際に作成したクロージャはその時のcountA(=0)をキャプチャするためである。以降カウントアップのたびに生み出される新たなcountAはキャプチャされたcountAとは別物であり参照できない。

App.tsx

import React, { useState, useCallback, useEffect } from 'react';
import './App.css';

function App() {
  const [countA, setCountA] = useState(0);

  useEffect(() => {
    const timerid = setInterval(() => {
      console.log(countA + "回ボタンがクリックされました。");
    }, 5000);

    return () => {
      clearInterval(timerid);
    };
    // eslint-disable-next-line
  }, []);

  const onButtonAClick = useCallback(() => {
    setCountA(c => c + 1);
  }, []);

  return (
    <div className="App">
      <div>
        <button onClick={onButtonAClick}>BUTTONA</button>
      </div>
      <div>BUTTONAが{countA}回押されました</div>
    </div>
  );
}
export default App;

stateの値を読み出すうまくいく例

useRefで生成したオブジェクトを介することで期待通りに動作する。

この例の場合setIntervalを呼び出す際に生成したクロージャはその時にuseRefが返したcountARefをキャプチャする。useRefはコンポーネントがアンマウントされるまで同じオブジェクトを返し続けるという特徴がある。そのため、その時その時のcountAの値をcountARefのcurrentプロパティに代入することで、常に最新の値を参照し続けることができる。

クラスのインスタンス変数と同様の感覚で使用できる。

import React, { useState, useCallback, useEffect, useRef } from 'react';
import './App.css';

function App() {
  const [countA, setCountA] = useState(0);

  const countARef = useRef(countA);
  countARef.current = countA;

  useEffect(() => {
    const timerid = setInterval(() => {
      console.log(countARef.current + "回ボタンがクリックされました。");
    }, 5000);

    return () => {
      clearInterval(timerid);
    };
  }, []);

  const onButtonAClick = useCallback(() => {
    setCountA(c => c + 1);
  }, []);

  return (
    <div className="App">
      <div>
        <button onClick={onButtonAClick}>BUTTONA</button>
      </div>
      <div>BUTTONAが{countA}回押されました</div>
    </div>
  );
}

export default App;

stateの値を更新する例

stateの値を更新する場合は単純にuseStateが返すset関数をそのまま使うことができる。これは、useStateが返すset関数は同一に保たれているためであり、setIntervalに渡すクロージャがキャプチャしたものをそのまま使用して問題ない。

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [intervalCount, setIntervalCount] = useState(0);

  useEffect(() => {
    const timerid = setInterval(() => {
      setIntervalCount(c => c + 1);
    }, 5000);

    return () => {
      clearInterval(timerid);
    };
  }, []);

  return (
    <div className="App">
      <div>setIntervalが{intervalCount}回呼ばれました。</div>
    </div>
  );
}

export default App;

コメント

タイトルとURLをコピーしました