reactjstypescriptreact-hooksreact-three-fiber

Is there a way to execute some action first time ref.current becomes available?


In React I am using @react-three/fiber for the 3D stuff. There are a lot components that don't support declarative way of doing things and require refs and imperative code (e.g. CameraControls from drei).

I am trying to initialize a CameraControls on the startup:

const cameraRef = useRef<CameraControls>(null)

const resetCamera = () => {
  if (cameraRef.current == null) return
  cameraRef.current.setLookAt(
    ...cameraInitialPosition,
    ...cameraInitialTarget,
    true
  )
}

useEffect(resetCamera, [])

return (
  <Canvas shadows>
    <CameraControls ref={cameraRef} />
    ...
)

Of course, this does not work since on the first and only time useEffect is executed, the cameraRef.current is still null.

When I try with timeout hack, current is still null:

useEffect(() => {
  setTimeout(resetCamera, 0)
}, [])

When I increase timeout to couple of hundred ms, it starts working:

useEffect(() => {
  setTimeout(resetCamera, 500)
}, [])

This approach with timeout is hacky and bad, looking for something better.

Is there a way to write some custom hook that would return refObject and later, when the current gets populated for the first time to execute the provided function?


Solution

  • You can store the ref with useState, using the set state function as a callback function ref, instead of an object ref.

    Since the set state would be called when the ref is available, the state would change, and the useEffect would be triggered:

    const [cameraRef, setCameraRef] = useState<CameraControls>(null)
    
    const resetCamera = () => {
      if (!cameraRef) return
        
      cameraRef.setLookAt(...cameraInitialPosition, ...cameraInitialTarget, true)
    }
    
    useEffect(resetCamera, [cameraRef])
    
    return (<Canvas shadows>
      <CameraControls ref={setCameraRef} />
      ...
    )
    

    If you only use the ref to invoke setLookAt once, and you don't need to store the ref, just call resetCamera directly from the ref prop of CameraControls:

    const resetCamera = (cameraRef: CameraControls) => {
      if (!cameraRef) return
    
      cameraRef.setLookAt(...cameraInitialPosition, ...cameraInitialTarget, true)
    }
    
    return (<Canvas shadows>
      <CameraControls ref={resetCamera} />
      ...
    )