TL;DR
- コンポーネント内で一度だけ初期化したい値は、useState の初期化関数で作る。
- useMemo はパフォーマンス最適化のためのもので、キャッシュが保たれる保証がない。
内容
素朴に書くと再レンダリングのたびに初期化される
コンポーネント内で一度だけ初期化したい値を、次のように書くことが考えられる。
const Component = () => {
const resource = new Resource()
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}しかし、この書き方ではコンポーネントが再レンダリングされるたびに Resource が作り直される。
useMemo は確実な方法ではない
useMemo を使い、空の依存配列 [] で値をメモ化する方法も考えられる。
const Component = () => {
const resource = React.useMemo(() => new Resource(), [])
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}useMemo は依存配列に基づいて値をキャッシュする。ただし、React の公式ドキュメントによると、useMemo はパフォーマンス最適化のためのものなので、キャッシュが保たれる保証はない。
将来的には、キャッシュが破棄されることを前提とした機能が React に追加される可能性があります。例えば、将来的に仮想リストが組み込みでサポートされた場合、仮想テーブルのビューポートからスクロールアウトした項目は、キャッシュを破棄するようになるかもしれません。このような挙動は、パフォーマンス最適化のみを目的として useMemo を使っている場合には問題ありません。しかし、他の目的で利用している場合は、state 変数 や ref を利用した方が良いかもしれません。
useState の初期化関数を使う
useState に初期化関数を渡すと、その関数は最初のレンダリングで一度だけ実行され、その後のレンダリングでは評価されない。そのため、初期化時に一度だけ実行したい処理に向いている。
const Component = () => {
const [resource] = React.useState(() => new Resource())
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}useRef でも同じように再生成を防げるが、コードが冗長になる。
const Component = () => {
const resource = React.useRef(null)
if (!resource.current) {
resource.current = new Resource()
}
return (
<ResourceProvider resource={resource.current}>
<App />
</ResourceProvider>
)
}キーワード
- React
- React Hooks
- useState
- useMemo
- useRef
自分の理解
React の公式ドキュメントにも、初期 state の再生成を避けるという項目で、まったく同じ状況の参考コードがあった。
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos)
// ...
}