R3F Fundamentals Skill
"Declarative 3D – compose scenes like React components."
When to Use This Skill
Use when:
- •Setting up a new R3F scene
- •Creating 3D components
- •Implementing game loops with
useFrame - •Managing canvas and renderer settings
Quick Start
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
function App() {
return (
<Canvas camera={{ position: [0, 5, 10], fov: 50 }}>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
<OrbitControls />
</Canvas>
);
}
Decision Framework
| Need | Use |
|---|---|
| Basic 3D scene | <Canvas> with mesh + geometry + material |
| Camera controls | <OrbitControls> or custom camera rig |
| Animation loop | useFrame hook |
| Access Three.js | useThree hook |
| Load assets | useLoader or <Suspense> with drei loaders |
| Performance | <Instances>, LOD, or useInstancedMesh |
Progressive Guide
Level 1: Basic Components
// Simple mesh component
export function Box({ position = [0, 0, 0] }) {
return (
<mesh position={position}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="royalblue" />
</mesh>
);
}
Level 2: Animation with useFrame
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
export function SpinningBox() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta * 0.5;
}
});
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
Level 3: Accessing Three.js State
import { useThree } from '@react-three/fiber';
export function CameraLogger() {
const { camera, gl, scene, size } = useThree();
useFrame(() => {
// Access camera position
console.log(camera.position.toArray());
});
return null;
}
Level 4: Game Loop Pattern
import { useGameStore } from '@/store/gameStore';
export function GameLoop() {
const { phase, updatePhase } = useGameStore();
useFrame((state, delta) => {
// Fixed timestep update
const fixedDelta = Math.min(delta, 1 / 30);
// Update game logic
updatePhase(fixedDelta);
});
return null;
}
Level 5: Performance Optimization
import { Instances, Instance } from '@react-three/drei';
export function ManyBoxes({ count = 1000 }) {
return (
<Instances limit={count}>
<boxGeometry />
<meshStandardMaterial />
{Array.from({ length: count }, (_, i) => (
<Instance
key={i}
position={[Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50]}
/>
))}
</Instances>
);
}
Anti-Patterns
❌ DON'T:
- •Create new objects inside
useFrame(causes GC pressure) - •Use
useStatefor rapidly changing values (use refs instead) - •Import entire Three.js (
import * as THREE) - •Forget to dispose of geometries and materials
- •Use
position={[x, y, z]}with changing values (creates new array each render)
✅ DO:
- •Reuse Vector3/Quaternion instances in useFrame
- •Use refs for animation state
- •Import specific Three.js classes
- •Clean up in useEffect return
- •Use
position-x,position-y,position-zfor animated values
Code Patterns
Reusable Vector Pattern
const tempVec = new THREE.Vector3();
function MovingObject() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
tempVec.set(Math.sin(state.clock.elapsedTime), 0, Math.cos(state.clock.elapsedTime));
meshRef.current?.position.copy(tempVec);
});
return <mesh ref={meshRef}>...</mesh>;
}
Conditional Rendering
function ConditionalMesh({ visible }) {
// Don't render if not visible - saves GPU
if (!visible) return null;
return <mesh>...</mesh>;
}
Rendering Best Practices
Depth Testing and Z-Fighting Prevention
Z-fighting (flickering between overlapping surfaces) occurs when two objects occupy the same depth or when depth testing is misconfigured.
// ❌ WRONG - Causes flickering due to improper depth configuration
<mesh>
<boxGeometry />
<meshStandardMaterial color="blue" />
</mesh>
// ✅ CORRECT - Proper depth configuration for character models
<mesh renderOrder={10}>
<boxGeometry />
<meshStandardMaterial
color="blue"
depthTest={true}
depthWrite={true}
/>
</mesh>
Key Properties:
| Property | Type | Purpose |
|---|---|---|
renderOrder | number | Controls render order (higher = later) |
depthTest | boolean | Enable/disable depth buffer testing |
depthWrite | boolean | Enable/disable depth buffer writing |
When to use:
- •Character models:
renderOrder={10},depthTest={true},depthWrite={true} - •Spawn points/overlays:
renderOrder={10},depthTest={true},depthWrite={true} - •Transparent objects:
renderOrder={1},depthWrite={false},transparent={true} - •Opaque geometry: Default settings usually work
Learned from bugfix-004 retrospective (2026-01-22):
Flickering on character and spawn point models was resolved by setting renderOrder=10, depthWrite=true, and depthTest=true. This ensures proper Z-buffering and prevents depth conflicts.
Material Rendering Order
// For layered materials (e.g., character with equipment)
<CharacterModel renderOrder={10} /> // Renders last (on top)
<BodyArmor renderOrder={5} />
<Underlay renderOrder={0} /> // Renders first
Checklist
Before implementing R3F component:
- • Using refs for animated values (not useState)
- • Not creating objects inside useFrame
- • Proper cleanup in useEffect
- • Using appropriate drei helpers
- • Canvas has proper camera settings
- • Lighting is set up correctly
- • Depth testing configured for layered meshes (renderOrder, depthTest, depthWrite)
- • No z-fighting visible on overlapping surfaces
Related Skills
For materials: Skill("ta-r3f-materials")
For performance: Skill("ta-r3f-performance")
For physics: Skill("ta-r3f-physics")
External References
- •drei documentation — Helper components
- •R3F documentation — Official docs
State-Driven Visual Updates (NEW - 2026-01-28)
Connecting game state to visual updates in R3F using Zustand selectors.
Pattern: Selective Re-renders
// ❌ WRONG - Entire component re-renders on ANY state change
import { useGameStore } from '@/store/gameStore';
function HealthBar() {
const { player, match, ui } = useGameStore(); // Unnecessary dependencies
return <mesh scale={{ x: player.health / 100 }}>{/* ... */}</mesh>;
}
// ✅ CORRECT - Only re-renders when player.health changes
function HealthBar() {
const health = useGameStore((state) => state.player.health);
return <mesh scale={{ x: health / 100 }}>{/* ... */}</mesh>;
}
Performance Pattern: useShallow
import { useShallow } from '@/store/useShallow';
import { usePlayerStore } from '@/store/playerStore';
// Multiple related values - use shallow comparison
function PlayerStatus() {
const { health, armor, weapon } = usePlayerStore(
useShallow((state) => ({
health: state.health,
armor: state.armor,
weapon: state.weapon,
}))
);
// Only re-renders if health, armor, or weapon change
}
Visual State Synchronization
// State-driven material properties
function DamageIndicator() {
const isDamaged = usePlayerStore((state) => state.health < 50);
return (
<mesh>
<meshStandardMaterial
color={isDamaged ? 'red' : 'blue'}
emissive={isDamaged ? 'red' : undefined}
emissiveIntensity={isDamaged ? 0.5 : 0}
/>
</mesh>
);
}
// State-driven animation
function PulsingEffect({ active }: { active: boolean }) {
const meshRef = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
if (!meshRef.current || !active) return;
const scale = 1 + Math.sin(clock.elapsedTime * 5) * 0.1;
meshRef.current.scale.setScalar(scale);
});
return <sphereGeometry ref={meshRef} args={[0.5, 16, 16]} />;
}
Anti-Patterns for State-Driven Visuals
❌ DON'T:
- •Subscribe to entire store when only using one value
- •Call setState inside useFrame (infinite loop)
- •Update state on every frame (performance issue)
- •Use deep selectors without memoization
✅ DO:
- •Use specific selectors for each value
- •Batch visual updates before state commits
- •Use shallow comparison for multiple values
- •Keep visual state separate from game state when possible
State-Driven Texture Changes
function WeaponModel() {
const weaponType = usePlayerStore((state) => state.weapon);
const textures = {
blaster: useTexture('/weapons/blaster.png'),
rocket: useTexture('/weapons/rocket.png'),
};
return (
<mesh>
<meshStandardMaterial map={textures[weaponType]} />
</mesh>
);
}
Sources:
- •https://docs.pmnd.rs/zustand/guides/performance
- •Learned from arch-002 retrospective (2026-01-28)