useState
の状態を変更したときは再レンダリングが行われますが、useRef
の状態を変更したときは行われません。
const App: React.FC<{}> = () => {
const [count, setCount] = useState<number>(0);
console.log(count); // 0 と 1 が出力されます
useEffect(() => {
setCount(1);
}, []);
return <></>;
};
const App: React.FC<{}> = () => {
const count = useRef<number>(0);
console.log(count.current); // 0 だけが出力されます
useEffect(() => {
count.current = 1;
}, []);
return <></>;
};
useState
で今の状態から次の状態を作るとき、次のように実装すると意図した結果が得られません。
const App: React.FC<{}> = () => {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
}
return <>
{/* 1 クリックにつき count は 1 しか増えません */}
<button onClick={handleClick}>Click me!</button>
{count}
</>;
};
意図した結果を得るためには、setCount
に関数を渡します。この関数の第一引数には今の状態が渡ります。関数の戻り値が次の状態になります。
const App: React.FC<{}> = () => {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count => count + 1);
setCount(count => count + 1);
}
return <>
{/* 1 クリックにつき count は 2 増える */}
<button onClick={handleClick}>Click me!</button>
{count}
</>;
};
useEffect
に渡した関数が実行されるタイミングは、レンダリングが完了した後です。よって、次のコードは必ずレンダリング後の要素の幅と高さを出力します。
const App: React.FC<{}> = () => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current === null) return;
const style = window.getComputedStyle(ref.current);
console.log(style.width, style.height); //=> 1440px 18px
}, []);
return <div ref={ref}>test</div>;
};
useRef
のcurrent
の変更を検知したいことがあります。しかし、useRef
にはcurrent
の値の変更を検知する方法は用意されていません。変更を検知したいときは、代わりに「コールバックref」を使います。コールバックrefの具体例としては、canvas
要素が存在するときに限りコンテキストを取得したい、というケースがあります。
次のコードは、canvas
要素が存在すればsetContext
を実行し、存在しなければ何もしないコードです。
const App: React.FC<{}> = () => {
const [show, setShow] = useState(true);
const [, setContext] = useState<CanvasRenderingContext2D | null>(null);
useEffect(() => {
// 1秒毎にcanvas要素の存在の有無を切り替えます
const id = window.setInterval(() => setShow(show => !show), 1000);
return () => window.clearInterval(id);
}, []);
// useCallbackに渡した関数は、nodeが変わったときに呼ばれます
const callbackRef = useCallback((node: HTMLCanvasElement | null) => {
if (node === null) return;
setContext(node.getContext('2d'));
}, []);
return <>
{show && <canvas ref={callbackRef}></canvas>}
</>;
};
同等のことをコールバックrefなしで実現しようとすると、おそらく複雑なコードになります。
useState
のsetXXX
を実行した後のレンダリングのタイミングは遅延されることがあります。たとえば次のコードは、setCount(1)
実行後に即座にレンダリングされず、setCount(2)
実行後にレンダリングされます。よってコンソールには0
と2
が出力されます。
const Child: React.FC<{ count: number }> = ({ count }) => {
console.log(count);
return <></>;
};
const App: React.FC<{}> = () => {
const [count, setCount] = useState(0);
useEffect(() => {
(async () => {
// await null;
setCount(1);
setCount(2);
})();
}, []);
return <Child count={count} />;
};
しかし、// await null;
のコメントアウトを外すだけで、コンソールには0
、1
、2
が出力されるようになります。これは、setCount(1)
実行後に即座にレンダリングされることを意味します。
このように、setXXX
実行後にレンダリングされることは保障されていますが、実行後のどのタイミングでレンダリングされるかについては保障されていません。したがって、タイミングへの依存はなくす必要があります。