React Three Fiber 3D

Conventions for React Three Fiber (R3F) components including dynamic imports, SSR avoidance, camera patterns, and postprocessing in Next.js apps.

AuthorNeexoCore
Apply to**/*{canvas,scene,viewer,3d,three,r3f}*.{ts,tsx}
Updated
react-three-fiberthreejs3dnextjs

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:

// 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

"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

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:

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

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

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