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 の公式ドキュメントにも、初期 state の再生成を避けるという項目で、まったく同じ状況の参考コードがあった。

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos)
  // ...
}

関連