# React Three Fiber

> React Three Fiber (R3F) + Three.js + drei 最佳實踐。當需要建立 3D 場景、處理相機/光照、互動事件、動畫、效能優化時使用。

- Skill: `recca0120/react-three-fiber` (Agent Skill)
- Install (CLI): `npx skillmds@latest add recca0120/react-three-fiber`
- Raw SKILL.md: https://api.skillmd.com/api/skills/recca0120/react-three-fiber/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: recca0120 (https://skillmd.com/u/recca0120)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/recca0120/react-three-fiber

---


# React Three Fiber 最佳實踐

> 基於 2025/5 知識。版本基準：three ^0.170、@react-three/fiber ^9、@react-three/drei ^10。

## 安裝

```bash
pnpm add three @react-three/fiber @react-three/drei
pnpm add -D @types/three
```

## 基本 Canvas

```tsx
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment } from '@react-three/drei'

<Canvas camera={{ position: [5, 5, 5], fov: 50 }} dpr={[1, 2]} shadows>
  <ambientLight intensity={0.4} />
  <directionalLight position={[5, 10, 5]} castShadow />
  <mesh>
    <boxGeometry args={[1, 1, 1]} />
    <meshStandardMaterial color="orange" />
  </mesh>
  <OrbitControls />
</Canvas>
```

## 核心 Hooks

```tsx
useFrame((state, delta) => {
  // 每幀執行，不要 setState！用 ref 直接改 mesh
  ref.current.rotation.y += delta
})

const { camera, gl, scene, size } = useThree()
```

## drei 常用

- `OrbitControls` / `TrackballControls`：相機控制
- `PerspectiveCamera makeDefault`：自訂主相機
- `Environment preset="city"`：HDR 環境光
- `Bounds`：自動 fit 物件入鏡
- `Html`：3D 裡嵌 DOM（ tooltip/label）
- `useGLTF('/model.glb')`：載 GLTF，記得 `useGLTF.preload()`
- `Instances` / `Instance`：大量重複物件用 instancing

## 互動事件

```tsx
<mesh
  onPointerDown={(e) => { e.stopPropagation(); /* e.point, e.face, e.object */ }}
  onPointerOver={(e) => setHover(true)}
  onPointerMissed={() => setSelected(null)}
>
```

事件會冒泡，要 `stopPropagation()` 阻止穿透。`onPointerMissed` 在點空白處觸發（適合取消選取）。

## 群組旋轉（魔術方塊關鍵）

```tsx
// 把 9 個 cubie 暫時 attach 到一個 Group，旋轉 Group，動畫完再 detach
const group = useRef<THREE.Group>(null!)
const pivot = useRef<THREE.Object3D>(new THREE.Object3D())

function rotateFace(cubies: THREE.Object3D[], axis: 'x'|'y'|'z', angle: number) {
  cubies.forEach(c => pivot.current.attach(c))
  // useFrame 內 lerp pivot.rotation[axis] 到 angle
  // 完成後：cubies.forEach(c => scene.attach(c)) 把世界變換烘回各 cubie
}
```

關鍵：`attach()`（非 `add()`）會保留世界座標。

## 效能陷阱

1. **useFrame 裡 setState**：會觸發 React render，每幀 60 次 → 災難。用 ref 直接操作。
2. **每幀 new 物件**：`new Vector3()` 放元件頂層或 useMemo，不要 useFrame 內建。
3. **同 geometry/material 重複建**：extract 成常數或 useMemo。
4. **shadow 開太多光源**：每個 castShadow 光源一次深度 pass。
5. **dpr 固定 2**：改 `dpr={[1, 2]}` 讓 R3F 自適應。
6. **大 scene 不用 instancing**：>100 重複物件請用 `<Instances>`。
7. **HMR after geometry change**：有時要 key prop 強制 remount。

## 動畫

- 簡單 lerp：useFrame + `THREE.MathUtils.lerp`
- 複雜：`@react-spring/three`（declarative）或 `framer-motion-3d`
- Tween：自己 state machine + useFrame

## TypeScript

R3F v9 以後 primitive 元素型別在 `@react-three/fiber` 的 `ThreeElements` 已正確。若要擴充自訂元素：

```ts
import { extend } from '@react-three/fiber'
import { MyMaterial } from './myMaterial'
extend({ MyMaterial })
declare module '@react-three/fiber' {
  interface ThreeElements { myMaterial: ThreeElement<typeof MyMaterial> }
}
```

## 測試策略

R3F 元件難 unit test → 把 **狀態/邏輯** 拆出純函式測（如魔術方塊的 rotate 邏輯），元件只負責渲染。必要時用 `@react-three/test-renderer`。

