Navigate

Search topics across all sections

GitHub
R3F Hooks

useThree

Sometimes you need more than just animating a mesh. You need the camera. The renderer. The viewport size. The mouse position. The entire internal state of your 3D scene. useThree is the hook that gives you the keys to everything.

You call useThree() with no arguments to grab the camera. It works, but now your component re-renders on every mouse movement, every window resize, every single frame tick. Your FPS drops and you can't figure out why.

terminal
React DevTools: "MyComponent" rendered 847 times in 10 seconds.
Profiler shows useThree() causing re-renders on every pointer move.
Cause: subscribing to entire state store without a selector.

Real-world

Imagine walking into a film studio's control room. There are monitors showing every camera angle, buttons for every light, dials for resolution and frame rate, and a readout of mouse position relative to the set.

useThree gives you access to this control room. You can read any monitor, press any button, twist any dial. But you do not want a notification every time any dial twitches -- you want to subscribe to only the monitors you care about. That is what selectors do.

What Lives in the State

gl

The WebGL renderer

scene

The root scene object

camera

Active camera

viewport

Visible area in 3D units

pointer

Normalized mouse position

clock

Elapsed time + delta

See It In Action

This demo uses useThree to read viewport dimensions and adapt the scene layout in real time. Try resizing your browser window.

Building It Step by Step

Step 1 -- Select only what you need

Always pass a selector function. This tells R3F to only re-render your component when that specific value changes -- not when anything in the entire state store changes.

Selectors.tsxTSX
const camera = useThree((s) => s.camera)
const viewport = useThree((s) => s.viewport)
const gl = useThree((s) => s.gl)

Each selector creates a targeted subscription. Your component stays quiet until its specific value changes.

Step 2 -- Use viewport for 3D layout

viewport.width and viewport.height tell you how much of the 3D world is visible at the camera's distance. Use these to position objects edge-to-edge.

EdgeToEdgePlane.tsxTSX
function EdgeToEdgePlane() {
  const { width, height } = useThree((s) => s.viewport)

  return (
    <mesh>
      <planeGeometry args={[width, height]} />
      <meshBasicMaterial color="#1a1a2e" />
    </mesh>
  )
}

The plane now fills the entire camera view perfectly, regardless of window size or aspect ratio.

Step 3 -- Use size for responsive breakpoints

size gives you the canvas dimensions in CSS pixels. Use it for mobile vs. desktop layout decisions, not for positioning 3D objects.

ResponsiveScene.tsxTSX
function ResponsiveScene() {
  const size = useThree((s) => s.size)
  const isMobile = size.width < 768

  return (
    <mesh position={[isMobile ? 0 : -2, 0, 0]}>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  )
}

size = pixels (1920x1080). viewport = Three.js units (~11x6). Use the right one for the right job.

What you just learned

useThree gives you access to the entire R3F internal state (renderer, camera, scene, etc.)

Always pass a selector to avoid unnecessary re-renders

viewport gives 3D world units -- use it for positioning 3D objects

size gives CSS pixels -- use it for responsive breakpoints

invalidate() only works with frameloop='demand' for on-demand rendering

Question

Can you use useThree outside of a Canvas? No. useThree accesses an internal context that only exists inside the Canvas component tree. If you need R3F state in a regular React component outside Canvas, use R3F's createRoot API and manage the connection yourself.

Think about it...

You want to make a 3D plane that perfectly fills the screen. Which value do you use for its width and height?

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Toggle showCorners off -- where do the corner objects go?

Try This!

Beginner

Set centerScale to 3 -- giant center object!

Try This!

Intermediate

Resize your browser window -- watch the viewport values update.

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Not using a selector (subscribing to everything)
Component re-renders on every mouse move, resize, and clock tick
Don't do this
MyComponent.tsxTSX
function MyComponent() {
  // Subscribes to the ENTIRE state object
  const state = useThree()
  const { camera } = state
  // Re-renders when ANYTHING changes!
}
Calling useThree() with no arguments subscribes to the entire R3F state store. Every mouse movement, every resize, every clock tick triggers a re-render. Passing a selector like (s) => s.camera uses zustand's shallow equality check to only re-render when that specific value changes.
2
Confusing size (pixels) with viewport (3D units)
Your 'fullscreen' plane is 1920 Three.js units wide
Don't do this
FullScreenPlane.tsxTSX
const { size } = useThree()
// size.width = 1920 (pixels!)
// This plane is 1920 Three.js units wide!
<planeGeometry args={[size.width, size.height]} />
size gives you CSS pixels (e.g. 1920x1080). viewport gives you Three.js world units visible at the camera's current distance (e.g. 11.5x6.5). When sizing or positioning 3D objects, you almost always want viewport. Use size only for responsive breakpoints.
3
Calling invalidate() without demand mode
invalidate() does nothing with the default always-rendering loop
Don't do this
OnDemand.tsxTSX
// Default frameloop="always"
<Canvas>
  <Scene />
</Canvas>

function Scene() {
  const invalidate = useThree((s) => s.invalidate)
  // This does nothing -- canvas already renders every frame
  invalidate()
}
invalidate() tells R3F to render the next frame. With the default frameloop='always', R3F already renders every frame continuously, so the call has no effect. Switch to frameloop='demand' for static scenes that only update on interaction. This saves significant GPU and battery.

Best Practices

Always use selectors

useThree(s => s.camera) prevents re-renders from unrelated state changes. Never call useThree() bare.

viewport for 3D, size for 2D

Use viewport (Three.js units) for 3D positioning. Use size (pixels) for responsive breakpoints.

Demand mode for static scenes

Use frameloop="demand" with invalidate() for scenes that only change on user interaction. Saves GPU and battery.

Use set() sparingly

state.set() overrides R3F internals. Only use it when you need a custom camera or raycaster. The defaults work for most cases.