React の画面の描画サイクルは端的に言えば、変更を検知し、変更に合わせた JavaScript を実行して、 HTML とスタイル、イベント等を再定義して、再描画する、といったものです。state 等を使ってこれに素直に従っていれば快適にコーディングできますが、これに従うと再描画が無用に走り、とても使えたものでなくなるという逆らうべき時が稀にあります(大体 onScroll などの頻繁に発生するイベントの度に変化する値を元になんやかんやする時です)。そういった時、レンダリングさせずとも最新の情報を使いたくなる時があります。
state とライフサイクル – React
具体的にこの再描画の仕組みを無視するとどの様な問題が起こるかというと次です。このデモは文をクリックすると ref(レンダリングに関わらない値)を更新して、それぞれのコンポーネントの持つ ref を元にする値を表示するデモです。App コンポーネント内では値が更新されますが、 HogeCountViewer コンポーネント内では値が更新されず、古い値をコンソールに出力することしかできません。
import "./styles.css";
import { useRef } from "react";
function HogeCountViewer(props: { hoge: number }) {
return (
<div onClick={() => console.log(`hoge: ${props.hoge} in viewer`)}>
クリックでこのコンポーネントの持つ hoge の値をコンソールに表示
</div>
);
}
export default function App() {
const hoge = useRef(0);
return (
<div className="App">
<span
onClick={() => {
hoge.current++;
console.log(`hoge: ${hoge.current} in root`);
}}
>
クリックで ref である hoge の値を増加
</span>
<HogeCountViewer hoge={hoge.current} />
</div>
);
}
これは子のコンポーネントに props の値を渡した時点でプリミティブな値になっているのが理由です。JavaScript における数値は不変な値で変更されません。
Primitive (プリミティブ) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
一方で ref でなく state で書いた場合、子のコンポーネントでも値の増加が反映されます。これは React が state が変わる度に子コンポーネントを作り直してくれているためです。作り直しをさせずに値の増加を反映させる例が次です。
import "./styles.css";
import { useRef } from "react";
function HogeCountViewer(props: { hoge: number | (() => number) }) {
return (
<div
onClick={() =>
console.log(
`hoge: ${
typeof props.hoge === "function" ? props.hoge() : props.hoge
} in viewer`
)
}
>
クリックでこのコンポーネントの持つ hoge の値をコンソールに表示
</div>
);
}
export default function App() {
const hoge = useRef(0);
return (
<div className="App">
<span
onClick={() => {
hoge.current++;
console.log(`hoge: ${hoge.current} in root`);
}}
>
クリックで ref である hoge の値を増加
</span>
<HogeCountViewer hoge={() => hoge.current} />
</div>
);
}
レンダリングされた時点の hoge.current そのものではなく、現在の hoge.current を読み取りに行く関数を渡します。こうすると最新の情報を取得できます。
余談ですがオブジェクトの状態で渡して、そのプロパティとして読み取っても最新の情報が反映されます。ref そのものを props として渡す前提のコンポーネントを作るパターンでは単に props.hoge.current とするのが読みやすくもあり、よさそうです。