Use Cases Compare Learn Blog Docs Open Studio

Avoiding the R3F Re-render Storm When Adding Objects Dynamically

Every R3F project hits this eventually. You add a fifth GLTF model and the entire scene flickers. Add the tenth and the framerate halves. The cause isn't React or Three.js — it's the subscription pattern.

Three patterns we use in Yugma that scale to hundreds of objects.

Pattern 1 — per-object subscriptions

Don't subscribe to the whole objects map. Subscribe per-id at the leaf:

function SceneObjectMesh({ objectId }: { objectId: string }) {
  // Only re-renders when this id's value changes
  const obj = useSceneStore((s) => s.objects[objectId])
  if (!obj) return null
  return <mesh position={obj.transform.position} />
}

function Scene() { // Subscribe only to ids list (changes on add/remove, not on update) const ids = useSceneStore((s) => s.objectOrder) return ( <> {ids.map((id) => <SceneObjectMesh key={id} objectId={id} />)} </> ) }

Adding object N+1 does not re-render objects 1..N.

Pattern 2 — read in handlers, not props

Inside callbacks (mouse, keyboard, AI dispatch), use useSceneStore.getState() instead of subscribing. The handler doesn't re-render when the scene mutates — only when its containing component re-renders for other reasons.

function Toolbar() {
  // ❌ Don't: re-renders on every scene mutation
  // const objects = useSceneStore((s) => s.objects)
  function handleClick() {
    // ✅ Do: read at fire time
    const { objects, selectedObjectId } = useSceneStore.getState()
    // …
  }
  return <button onClick={handleClick}>Action</button>
}

Pattern 3 — refs over state for transforms

When TransformControls drags an object, don't setState every frame. Write the transform straight to the Three.js object via ref, then commit to state on pointerup.

const groupRef = useRef<THREE.Group>(null)

const onTransformChange = () => { // No setState here — reading the gizmo's frame-by-frame state // would force React to re-render every frame. }

const onTransformEnd = () => { // Commit once when the user lets go const g = groupRef.current if (!g) return updateObject(objectId, { transform: { position: [g.position.x, g.position.y, g.position.z], rotation: [g.rotation.x, g.rotation.y, g.rotation.z], scale: [g.scale.x, g.scale.y, g.scale.z], }, }) }

This is the trick that lets Yugma keep 60fps while a designer drags handles around a 200-object scene.

Bonus: instancing for repeated geometry

If your scene has 200 chairs, use Drei's instead of 200 mesh nodes. The CPU cost is one mesh; the GPU draws all 200 in one draw call.

import { Instances, Instance } from '@react-three/drei'

<Instances limit={200}> <boxGeometry args={[0.5, 0.9, 0.5]} /> <meshStandardMaterial color="#a37b53" /> {chairs.map((c, i) => <Instance key={i} position={c.position} />)} </Instances>

What we measure

In Yugma we run a Perf HUD inside the studio. Frame budget: 16ms. Per-pattern:

The R3F re-render storm is solvable. You just have to stop subscribing too widely.

Read the Yugma vs Three.js comparison →