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