React + useLatest: 非同期の処理が実行されたとき最新の状態を得る


非同期の処理が実行されたとき最新の状態を得るためのフックuseLatestのご紹介です。

関数がつくられたときの状態を保つのがReactの原則

Reactの「コンポーネント内に書かれた関数からは、その関数が作成された時のpropsstateが『見え』」ます(「関数内で古い props や state が見えているのはなぜですか?」原文:「Why am I seeing stale props or state inside my function?」)。外から勝手に書き替えられることによるバグが防げるので安心です。

もっとも、非同期の処理では、関数が実行されたときの最新の状態を使いたいことも少なくありません。React公式サイトの「フックに関するよくある質問」には、つぎのようなコード例が示されています。

コード001■関数はつくられたときの状態変数の値を保つ

import React from 'react';

function Example() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

ページの[Click me]ボタンを押すと、その数だけ数値が加算されます。[Show alert]はその数値を3秒後に警告ダイアログで表示するボタンです。ただし、その値はボタンを押したときのまま保たれます(図001)。あとにカウントアップした値は見えないのです。以下のCodePenのサンプルで、動きは確かめられます。

図001■警告ダイアログにはボタンを押したときの値が示される

See the Pen React: Seeing stale props by Fumio Nonaka (@FumioNonaka) on CodePen.

これが意図した結果であれば構いません。けれど、最新の値を使って処理したい場合もあるはずです。

useLatestフックで最新の状態を得る

それでは、非同期の処理で最新の状態を得るにはどうしたらよいでしょう。前出「関数内で古い props や state が見えているのはなぜですか?」はつぎのように説明します。

非同期的に実行されるコールバック内で、意図的にstateの最新の値を読み出したいという場合は、その値をref内に保持して、それを書き換えたり読み出したりすることができます。

何を言っているのかよくわからない、あるいは手間がかかりそう、と思った人は、react-useuseLatestを使うとよいでしょう。インストールはnpmで行います。

npm i react-use

フックuseLatestの戻り値は、useRefと同じrefオブジェクトです。ただし、引数には最新値がほしい状態変数を渡します。refオブジェクトですからcurrentプロパティをもち、その中に収められているのが最新の値です。

前掲Reactサイトのコード例に組み込めば、つぎのようになります。モジュールで用いるときは、useLatestをあらかじめimportしてください。これで警告ダイアログは、コールバックが呼び出されたときの最新の状態変数値(count)を示します。

コード001■useLatestフックで最新の状態変数の値を得る

import React from 'react';
import { useLatest } from 'react-use';

const Example = () => {
  const [count, setCount] = React.useState(0);
  const latestCount = useLatest(count);

  function handleAlertClick() {
    setTimeout(() => {
      alert(`Latest count value: ${latestCount.current}`);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
};

モジュール化したサンプルをCodeSandboxに掲げました。useLatestを用いたコードは、src/App.jsでお確かめください。

useLatestの実装は10行足らず

おまけとして、フックuseLatestの実装を確かめておきましょう。TypeScriptファイルのソースコードを見ると、なんと10行にも及びません。そして、useRefフックが用いられています。前述のReactのドキュメントが説明していた「意図的にstateの最新の値を読み出したいという場合は、その値をref内に保持して」というのは、こういうことだったのです。