Skip to main content

Gotchas

Hard-won lessons from building a 3D world builder in React. Read these before contributing.

React Three Fiber

InstancedMesh + map=null

If an InstancedMesh has map=null on its material, the GPU compiles the shader without a texture sampler. When you later assign a texture, the shader recompiles — causing a frame hitch.

Fix: Always use a placeholder texture (1x1 white pixel) instead of null.

Declarative Props on InstancedMesh

R3F's declarative prop system is unreliable for dynamic textures on InstancedMesh. Props may not trigger re-renders correctly.

Fix: Use imperative refs. Set mesh.material.map = texture directly.

drei <Html> with transform

<Html transform> creates a CSS overlay — it's not in the WebGL depth buffer. It won't be occluded by 3D objects.

Fix: Use zIndexRange={[0,0]} to minimize z-fighting. Accept that HTML overlays are always "on top" of the 3D scene.

Next.js

SSR and document

Never use document, window, or other browser APIs at module level. Next.js pre-renders on the server where these don't exist.

Fix: Lazy-initialize in functions, use useEffect, or dynamic imports with ssr: false.

globalThis Cache in Route Handlers

Next.js dev mode splits route handlers into separate chunks. A cache in one route handler won't be visible to another.

Fix: Pin caches to globalThis (e.g., globalThis.__registryCache).

Zustand

Stale Closures in Intervals

// ❌ BAD — captures value at render time
const count = useStore(s => s.count)
setInterval(() => console.log(count), 1000) // always logs initial value

// ✅ GOOD — reads fresh value each time
setInterval(() => console.log(useStore.getState().count), 1000)

Always use useStore.getState() inside setInterval, setTimeout, or any long-lived callback.

World Persistence

Save Debouncing

World saves use 100ms + 1000ms debouncing. Don't call saveWorldState() in tight loops or you'll queue redundant writes.

_worldReady Guard

The _worldReady flag must be true before any save is allowed. This prevents saving an empty state during world loading.

Nuke Protection

If the system loaded 5+ objects but a save attempt contains 0 objects, the save is blocked. This prevents accidental world wipes from state race conditions.

URLs

External URL Detection

When handling asset URLs, check startsWith('http') before prepending the app's basePath. Otherwise you'll create broken URLs like /conjured/https://cdn.meshy.ai/....

Performance

FPS is Never Capped

frameloop="always" runs at native refresh rate. Never add FPS limiting. The Oasis is built for gamers who want 250fps.

Agent Sessions

Session ID Collision

Two Claude Code windows sharing a session ID will corrupt each other's context. Every window MUST have a unique session ID.

HMR Safety

pnpm dev hot-reloads without restarting the server. You can edit files while it's running — but be careful with state that persists across HMR (Zustand stores, globalThis caches).