React: useRefフックで状態変数のひとつ前の値や最新の値を得る


ReactのuseRefフックは、戻り値のオブジェクトを子コンポーネントのrefプロパティに与えて、要素の参照を得るのが代表的な使い方です(「React + React Use: ウィンドウのサイズ変更に応じて要素の絶対座標を計算し直す ー useRefとuseWindowSizeを使って」の「useRefフックで要素の参照から絶対座標を求める」参照)。

けれど、それだけがuseRefフックの役割ではありません。状態変数を渡して使うこともできるのです。本稿ではその例をふたつご紹介します。ひとつは、状態変数が書き替わったとき、その前の値をもつことです。そしてもうひとつ、状態変数を参照したとき、直近に設定した値が得られない場合の対処にも使えます。

状態変数のひとつ前の設定値をもっておく

まず、状態変数が書き替わったとき、その前の値をもっておくにはどうしたらよいかです。これは、React公式サイト「フックに関するよくある質問」の「前回の props や state はどうすれば取得できますか?」に紹介されています。使いまわしやすいようカスタムフック(usePrevious)に定めたのがつぎのコードです。

const usePrevious = (value) => {
    const ref = useRef(value);
    useEffect(() => {
        ref.current = value;
    });
    return ref.current;
};

短いコードとはいえ、説明がないとわかりにくいかもしれません。先に使い方を示しましょう。つぎのようにカスタムフック(usePrevious)に状態変数(count)を渡して、変数(prevCount)に収めるだけです。これで、状態変数を書き替えるたびに、ひとつ前の値が変数に入ります。

const initialCount = 0;
function App() {
    const [count, setCount] = useState(initialCount);
    const prevCount = usePrevious(count);

}

前掲カスタムフック(usePrevious)のコードを見返して、仕組みを確かめましょう。引数(value)に受け取るのは、前述のとおり状態変数です。その値を初期値として、useRefフックによりrefオブジェクトが変数(ref)に収められます。

そして、useEffectフックで、refオブジェクト(ref)のcurrentプロパティを状態変数の新たな値で書き替えます。鍵となるのは、useEffectの実行より先に、カスタムフック(usePrevious)から値(ref.current)が返されることです。つまり、戻り値はひとつ前の値になります。

「フック API リファレンス」の「useRef」の項を参照しつつ、いくつか確かめておきましょう。カスタムフック(usePrevious)は、関数コンポーネントと基本は同じです。useRefに渡した状態変数が書き替わるたびに、新たな値を引数に受け取って実行されます。けれど、useRefで毎回refオブジェクトがつくり直されるわけではありません。

useRef()を使うことと自分で{current: ...}というオブジェクトを作成することとの唯一の違いとは、useRefは毎回のレンダーで同じ ref オブジェクトを返す、ということです。

そして、refオブジェクトの実質はcurrentプロパティの値です。プロパティ値は、代入で書き替えないかぎり、それまでの値が保たれます。

本質的にuseRefとは、書き換え可能な値を.currentプロパティ内に保持することができる「箱」のようなものです。

前掲のカスタムフック(usePrevious)でカウンターのひとつ前の値をとり、現在値とともに示すサンプル001はCodeSandboxに公開しました。

サンプル001■React: usePrevious hook with useRef


>> CodeSandboxへ

なお、前出「前回の props や state はどうすれば取得できますか?」には、つぎのように記されています。

これは比較的よくあるユースケースですので、将来的にusePreviousというフックを React が最初から提供するようにする可能性があります。

関数内から最新の状態変数値を得る

つぎに、関数内から最新の状態変数値を得たいという場合です。これは逆に、その値が得られない場合を知っておかなければならないでしょう。「フックに関するよくある質問」の「関数内で古い props や state が見えているのはなぜですか?」に以下のような例が示されています。

最初に “Show alert” ボタンをクリックして、次にカウンタを増加させた場合、アラートダイアログに表示されるのは “Show alert” ボタンをクリックした時点でのcount変数の値になります。これにより props や state が変わらないことを前提として書かれたコードによるバグが防止できます。

function App() {

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

    return (
        <div className="App">

            <button onClick={handleAlertClick}>
                Show alert
            </button>
        </div>
    );
}

つまり、“Show alert”ボタンをクリックしたあとカウンターの値を増減しても、警告ダイアログにはボタンをクリックしたときの変数値が示されるのです(図001)。

図001■警告ダイアログ表示のボタンをクリックしたときの状態変数値が保持される

前出「関数内で古い props や state が見えているのはなぜですか?」には、つぎのような解決方法が示唆されています。けれど、具体的なコードはありません。

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

そこで、カスタムフック(useLatest)に定めてみました。前掲カスタムフック(usePrevious)と似たつくりです。けれど、useEffectフックがなく、refオブジェクト(ref)のcurrentプロパティに最新の値(value)を代入しています。そして、戻り値はrefオブジェクトです。

const useLatest = (value) => {
    const ref = useRef(value);
    ref.current = value;
    return ref;
};

カスタムフック(useLatest)には、やはり状態変数を渡します。戻り値をrefオブジェクトにしたのは、非同期のコールバック関数内でcurrentプロパティ値を取り出さなくてはならないからです。

function App() {

    const latestCountRef = useLatest(count);

    const handleAlertClick = () => {
        // setTimeout(() =>
        setTimeout(() => {
            const latestCount = latestCountRef.current;
            // alert('You clicked on: ' + count)
            alert('You clicked on: ' + latestCount);
        // , 3000);
        }, 3000);
    }

}

ここでまたひとつ、前出「フック API リファレンス」の「useRef」の項から引用します。

useRefは中身が変更になってもそのことを通知しないということを覚えておいてください。.currentプロパティを書き換えても再レンダーは発生しません。

逆に、レンダーとかかわらずrefオブジェクトのcurrentプロパティは書き替えられます。そのため、前掲カスタムフック(useLatest)で得たrefオブジェクトのcurrentプロパティから最新の値が取り出せるのです。カスタムフックが用いられたサンプル002をCodeSandboxに公開します。

サンプル002■React: useLatest hook with useRef


>> CodeSandboxへ

ところで、このカスタムフックuseLatestは、実は便利フックを詰め合わせたライブラリreact-useの同名フックuselatest実装です。このライブラリも1度試してみることをお勧めします(「React + useLatest: 非同期の処理が実行されたとき最新の状態を得る」参照)。

参考:「useRef は何をやっているのか