これは
ReactのuseStateとuseRefについてあまり理解できていなかったので最低限まとめておく。
useStateとuseRefの違い
ReactのuseStateとuseRefの違いは、UIに出るかどうかと再レンダリングを引き起こすかどうかでほぼ決まる。
useState
useStateは、画面に表示される値を管理するためのフックだ。
この値が更新されると、Reactは「状態が変わった」と判断して 再レンダリング(= コンポーネント関数の再実行) を行う。
また、前の値に依存して更新する場合は注意が必要で、setX(x + 1)のように直接変数を使うと更新がうまくいかないことがある。
setX(prev => prev + 1) の形で書くのが安全だ。(functional update)
functional update で安全に更新
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const unsafe = () => {
// この count は今のレンダリング時点の値
setCount(count + 1);
setCount(count + 1); // 同じ count をもう一度使っている
};
const safe = () => {
// prev は常に最新の state
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
return (
<div>
<p>count: {count}</p>
<button onClick={unsafe}>+2したい(危険)</button>
<button onClick={safe}>+2(安全)</button>
</div>
);
}
前の値に依存する更新は必ず functional update を使う。
(危険)ボタンでは setCount を2回呼んでも +1 しかされない。
count を直接使う書き方は、古い値を掴んで更新がうまく反映されない原因になる。
useRef
useRefは、UIに直接出さない値やDOM要素への参照を保持するためのフックだ。
ref.currentを更新しても、再レンダリングは発生しない。
ただし、再レンダリングが起きても値は保持され続ける。
これはローカル変数との決定的な違いで、ローカル変数はコンポーネント関数が再実行されるたびに毎回作り直される。
useRefは保持される(でもUIは勝手に更新されない)
import { useRef, useState } from "react";
export default function App() {
const [, forceRerender] = useState(0);
const refCounter = useRef(0);
return (
<div>
<p>refCounter.current: {refCounter.current}</p>
<button
onClick={() => {
refCounter.current += 1;
}}
>
ref +1(画面は変わらない)
</button>
<button onClick={() => forceRerender((p) => p + 1)}>
再レンダリング(表示が追いつく)
</button>
</div>
);
}
refCounter.currentは確かに増えているが、それだけでは画面は更新されない。
しかし、何らかの理由で再レンダリングが起きると、その時点で表示が追いつく。
refでDOM参照
import { useRef } from "react";
export default function App() {
const boxRef = useRef<HTMLDivElement | null>(null);
return (
<div>
<button onClick={() => boxRef.current?.scrollIntoView({ behavior: "smooth" })}>
下のボックスへスクロール
</button>
<div style={{ height: 600 }} />
<div ref={boxRef} style={{ padding: 16, border: "1px solid" }}>
ここが対象のDOM
</div>
</div>
);
}
<div ref={boxRef} /> と書くことで、実DOM要素が boxRef.current に入る。
scrollIntoView や focus はReactの機能ではなく、ブラウザが持っている標準APIだ。
まとめ
useStateは「画面に出る状態」を管理するためのもの、useRefは「画面に出さない値やDOM参照」を保持するためのもの。
前の値に依存する更新は必ず functional update を使う。