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

1

5

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?

Touristy answered 24/9, 2024 at 12:51 Comment(0)
T
6

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} />
  ...
)
Trail answered 24/9, 2024 at 12:56 Comment(3)
"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" - If OP does that it might get invoked multiple times: "React will also call your ref callback whenever you pass a different ref callback. In the above example, (node) => { ... } is a different function on every render. When your component re-renders, the previous function will be called with null as the argument, and the next function will be called with the DOM node."Bully
Indeed, but that's why the if (!cameraRef) return check. If the function is called with null - do nothing.Trail
@OriDrori Yeah but not only in case of null, you mentioned situation: "If you only use the ref to invoke setLookAt once". Based on above quote resetCamera might get called multiple times sometimes with null sometimes with DOM node.Bully

© 2022 - 2025 — McMap. All rights reserved.