React Three Fiber (R3F) renders Three.js scenes as React components. In Next.js, all 3D code must be client-only to avoid SSR hydration errors.
Critical: No SSR for 3D
Three.js requires browser APIs (WebGL, canvas). Always use dynamic import with ssr: false:
// In a Server Component or page
import dynamic from "next/dynamic";
const Scene3D = dynamic(() => import("./Scene3D"), { ssr: false });
export default function Page() {
return <Scene3D />;
}
The Scene3D component itself must have "use client" at the top.
Never import Three.js at the top level of a Server Component — use dynamic imports
Never use useEffect for animations — use R3F's useFrame hook instead
Avoid creating new THREE.Vector3() or THREE.Color() inside render — allocate once and reuse
Use <Suspense fallback={...}> around heavy models for loading states
Set reactStrictMode: false in next.config.ts — R3F is not compatible with strict mode double-rendering
For WebGPU detection, check navigator.gpu before attempting WebGPU renderer
Raw content
Copy this into your project — e.g. .instructions.md, .agent.md, or SKILL.md
## Overview
React Three Fiber (R3F) renders Three.js scenes as React components. In Next.js, all 3D code must be client-only to avoid SSR hydration errors.
## Critical: No SSR for 3D
Three.js requires browser APIs (WebGL, canvas). Always use dynamic import with `ssr: false`:
```tsx
// In a Server Component or page
import dynamic from "next/dynamic";
const Scene3D = dynamic(() => import("./Scene3D"), { ssr: false });
export default function Page() {
return <Scene3D />;
}
```
The Scene3D component itself must have `"use client"` at the top.
## Canvas Setup Pattern
```tsx
"use client";
import { Canvas } from "@react-three/fiber";
import { Environment, OrbitControls } from "@react-three/drei";
export default function Scene3D() {
return (
<Canvas camera={{ position: [0, 2, 5], fov: 45 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<Environment preset="studio" />
<OrbitControls />
{/* Your 3D content */}
</Canvas>
);
}
```
## GLB Model Loading
```tsx
import { useGLTF } from "@react-three/drei";
function MachineModel({ url }: { url: string }) {
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
// Preload for instant display
useGLTF.preload("/models/machine.glb");
```
## Module Visibility Toggle
For configurators where modules can be toggled on/off:
```tsx
import { useEffect, useRef } from "react";
import { useGLTF } from "@react-three/drei";
import * as THREE from "three";
function ConfigurableModel({ url, visibleModules }: { url: string; visibleModules: Set<string> }) {
const { scene } = useGLTF(url);
useEffect(() => {
scene.traverse((node: THREE.Object3D) => {
if (node.name && visibleModules !== undefined) {
node.visible = visibleModules.has(node.name);
}
});
}, [scene, visibleModules]);
return <primitive object={scene} />;
}
```
## Postprocessing
```tsx
import { EffectComposer, N8AO, Bloom, ToneMapping } from "@react-three/postprocessing";
<EffectComposer>
<N8AO aoRadius={0.5} intensity={1} />
<Bloom luminanceThreshold={0.9} intensity={0.5} />
<ToneMapping />
</EffectComposer>
```
## Common Pitfalls
- **Never** import Three.js at the top level of a Server Component — use dynamic imports
- **Never** use `useEffect` for animations — use R3F's `useFrame` hook instead
- Avoid creating new `THREE.Vector3()` or `THREE.Color()` inside render — allocate once and reuse
- Use `<Suspense fallback={...}>` around heavy models for loading states
- Set `reactStrictMode: false` in `next.config.ts` — R3F is not compatible with strict mode double-rendering
- For WebGPU detection, check `navigator.gpu` before attempting WebGPU renderer