Skip to content

useEffect Hook

Import

import { useEffect } from 'react'

Video Lecture

Section Video Links
useEffect Hook useEffect Hook useEffect Hook

Description

In the last example, there was a very subtle error which you might not have picked up if I didn't bring it to your attention.

When I used console.log(ref) in the last example, the Mesh hadn't actually finished being created in Three.js scene yet. So, if we needed to do any logic on that data within our script, then we would have needed to make sure that it actually existed first, otherwise ref.current would have been still undefined. This is because we tried to access ref and more specifically, it's current property, before it had been set when the JSX was converted into a Three.js object.

A way to improve on my last example would be to use the React useEffect hook.

./src/Box.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { useRef, useEffect } from 'react'

export default function Box(props) {
  const ref = useRef()

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

  return (
    <mesh {...props} ref={ref}>
      <boxGeometry />
      <meshBasicMaterial color={0x00ff00} wireframe />
    </mesh>
  )
}

The useEffect hook executes after the function returns the generated component instance within it, which means the any ref will be assigned before the useEffect hook gets called.

Working Example

<>

Dependency Array

A useEffect without any dependency array will be executed every time state or props change for the component.

useEffect(() => {
  console.log('executed every time')
})

If your useEffect is being called multiple times, for whatever reason, and you only want it to be called once when the component is first rendered, then add an empty dependency array.

useEffect(() => {
  console.log('executed once when the component is first rendered')
}, [])

If you want the useEffect to be called only when a particular state value changes, then you can add the state variable to the dependency array.

const [v, setV] = useState()
useEffect(() => {
  console.log('executed whenever v changes')
}, [v])

Clean Up

Note that when using useEffect, if a components state changes multiple times, then the useEffect may also be called multiple times depending on the contents, or not, of your dependency array. If your useEffect is creating new resources, and those resources accumulate, then it is useful to add a return function to clean up or dispose of those resources, when react unmounts the existing useEffect.

E.g.,

useEffect(() => {
  const onDocumentKey = (e) => {
    console.log(e.code)
  }
  document.addEventListener('keydown', onDocumentKey)
  return () => {
    document.removeEventListener('keydown', onDocumentKey)
  }
}, [])

Without the return function, your DOM would have many event listeners, increasing with every next state or props change, listening and reacting even more, to keydown events. This is discussed more in the Custom Hooks section.

UseLayoutEffect

Now one very subtle detail about useEffect that you may not ever notice when user React Three Fiber, is that since useEffect is asynchronous, the callback may get called after the first time the underlying WebGL pixels were drawn to the Canvas on the DOM.

Since by default, React Three Fiber is continuously triggering WebGL data to be drawn to the Canvas, you may not notice any delay occurring, such as repositioning any mesh. This is not something I have ever noticed when running React Three Fiber using default settings.

However, if you configure the React Three Fiber Canvas to use frameloop="demand".

<canvas frameloop="demand"></canvas>

Then, you may see, very briefly, an object being rendered before it is repositioned, and then the canvas is rendered once again.

If you ever notice this problem, then you can solve it by using useLayoutEffect instead of useEffect. The hook method signature is the same, but useLayoutEffect will be called synchronously before the browser has a chance to paint.

To simulate this, you can try this code in your existing boilerplate. Note that the conditions to reproduce this behavior are very specific, so you may not see it.

./src/App.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Canvas } from '@react-three/fiber'
import Box from './Box'

export default function App() {
  return (
    <Canvas camera={{ position: [0, 0, 2] }} frameloop="demand">
      <Box position={[-0.75, 0, 0]} name="A" />
      <Box position={[0.75, 0, 0]} name="B" />
    </Canvas>
  )
}

./src/Box.jsx

First, using useEffect, when you refresh the browser, you may see the right box flash as its position.y is modified from 0 to 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useRef, useEffect } from 'react'

export default function Box(props) {
  const ref = useRef()

  useEffect(() => {
    if (ref.current.name === 'B') {
      ref.current.position.y = 1
    }
  })

  return (
    <mesh {...props} ref={ref}>
      <boxGeometry />
      <meshBasicMaterial color={0x00ff00} wireframe />
    </mesh>
  )
}

Secondly, using useLayoutEffect, when you refresh the browser, you shouldn't see the flash as the boxes position.y is modified from 0 to 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useRef, useLayoutEffect } from 'react'

export default function Box(props) {
  const ref = useRef()

  useLayoutEffect(() => {
    if (ref.current.name === 'B') {
      ref.current.position.y = 1
    }
  })

  return (
    <mesh {...props} ref={ref}>
      <boxGeometry />
      <meshBasicMaterial color={0x00ff00} wireframe />
    </mesh>
  )
}

Note

Official React documentation states, "useLayoutEffect can hurt performance. Prefer useEffect when possible."

useEffect reactjs.org w3schools.com sbedit.net
useLayoutEffect reactjs.org

GitHub Branch

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

Comments