Skip to content

useMemo

Import

import { useMemo } from 'react'

Video Lecture

Section Video Links
useMemo Hook useMemo Hook useMemo Hook

Description

In this section, I will show alternate ways of setting the geometry of our mesh using React and JSX.

By using these alternate methods, we need to consider how React will manage the objects within memory and the scene.

Before starting, replace ./src/Box.jsx with this simplified script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'

export default function Box(props) {
  const ref = useRef()
  const [rotate, setRotate] = useState(false)

  useFrame((_, delta) => {
    ref.current.rotation.x += delta * rotate
    ref.current.rotation.y += 0.5 * delta * rotate
  })

  return (
    <mesh {...props} ref={ref} onPointerDown={() => setRotate(!rotate)}>
      <boxGeometry />
      <meshBasicMaterial color={'lime'} wireframe />
    </mesh>
  )
}

So far, we have basically used this JSX structure, but also including Props, useRef and Events.

<mesh>
  <boxGeometry />
  <meshBasicMaterial />
<mesh>

This is fine, and every time state changes in our Box component, React will re-use the objects already created behind the scenes when it re-renders the scene.

We can verify that React is using the same geometry objects on each render by logging the uuid of the geometry of each box to the console.

We will add a useEffect to ./src/Box.jsx to log the geometry's uuid.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useRef, useState, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'

export default function Box(props) {
  const ref = useRef()
  const [rotate, setRotate] = useState(false)

  useEffect(() => {
    console.log(ref.current.geometry.uuid)
  })

  useFrame((_, delta) => {
    ref.current.rotation.x += delta * rotate
    ref.current.rotation.y += 0.5 * delta * rotate
  })

  return (
    <mesh {...props} ref={ref} onPointerDown={() => setRotate(!rotate)}>
      <boxGeometry />
      <meshBasicMaterial color={'lime'} wireframe />
    </mesh>
  )
}

./src/App.jsx creates two Boxes, and each Box, has its own independent geometry and when we click the Box, we can see It's uuid written to the console.

This is showing us that the geometry is not changing when this Box function component is re-executed by React when the state change happened during the click. This is good and recommended behavior.

Now what if we wanted to replace that boxGeometry dynamically. One day you may want to change a mesh's geometry to a different one at runtime.

We have several ways of setting the boxGeometry other than using the <boxGeometry /> tag.

In this example, I will declare and instantiate a new THREE.BoxGeometry() in the function body, and attach it in the JSX.

Overwrite ./src/Box.jsx with this code below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useRef, useState, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'

export default function Box(props) {
  const ref = useRef()
  const [rotate, setRotate] = useState(false)
  const geometry = new THREE.BoxGeometry()

  useEffect(() => {
    console.log(ref.current.geometry.uuid)
  })

  useFrame((_, delta) => {
    ref.current.rotation.x += delta * rotate
    ref.current.rotation.y += 0.5 * delta * rotate
  })

  return (
    <mesh
      {...props}
      ref={ref}
      onPointerDown={() => setRotate(!rotate)}
      geometry={geometry}
    >
      <meshBasicMaterial color={'lime'} wireframe />
    </mesh>
  )
}

We are not using the <boxGeometry /> tag in the JSX anymore, but instantiating a new THREE.BoxGeometry and appending it to the props in the mesh tag instead.

This appears to work exactly the same. But there is a problem. This is a very small application, so in any case, the problem is likely to go unnoticed. But however, it is there.

This Box function will be re-executed every time there is a state change. This means that it will unnecessarily re-create a new THREE.BoxGeometry() every time it's called.

Click on a Box and you will see that its geometry's uuid is changing. This means that it is a new instance of a THREE.BoxGeometry in memory and being rendered to the scene. Creating new objects unnecessarily is not recommended.

We can solve this problem by using the useMemo hook. useMemo will act as cache for the geometry and return that instead of generating a new object.

Add the import for useMemo

import { useRef, useState, useEffect, useMemo } from 'react'

And modify the line

const geometry = new THREE.BoxGeometry()

to be,

const geometry = useMemo(() => new THREE.BoxGeometry(), [])

Now, when we click a Box, we can see that the uuid is staying consistent.

This is much better.

useMemo will cause the inner function to only run when needed. Use useMemo when you want to keep expensive, resource intensive functions from needlessly re-running.

Now we can use this new technique, to safely replace the geometry of our Box without using any more resources than necessary.

Replace ./src/Box.jsx with this code below, and you will see that the Box will toggle between a pre-cached THREE.SphereGeometry and a THREE.BoxGeometry on every click rather than re-creating new geometries each time we switch it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useRef, useState, useEffect, useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'

export default function Box(props) {
  const ref = useRef()
  const [count, setCount] = useState(0)
  const geometry = useMemo(
    () => [new THREE.BoxGeometry(), new THREE.SphereGeometry(0.785398)],
    []
  )

  useEffect(() => {
    console.log(ref.current.geometry.uuid)
  })

  useFrame((_, delta) => {
    ref.current.rotation.x += delta
    ref.current.rotation.y += 0.5 * delta
  })

  return (
    <mesh
      {...props}
      ref={ref}
      onPointerDown={() => setCount((count + 1) % 2)}
      geometry={geometry[count]}
    >
      <meshBasicMaterial color={'lime'} wireframe />
    </mesh>
  )
}

Note

You should only use useMemo as a performance optimization, in case your code doesn’t work without it. First, try to find the underlying problem, and fix it. Then try using useMemo to see if it improves performance.

Working Example

<>
useMemo reactjs.org w3schools.com sbedit.net
React Logo Example codesandbox.io

GitHub Branch

git clone https://github.com/Sean-Bradley/React-Three-Fiber-Boilerplate.git
cd React-Three-Fiber-Boilerplate
git checkout useMemo
npm install
npm start

Comments