Skip to content
dev-r3f-r3f-fundamentals logo

R3F Fundamentals

dev-r3f-r3f-fundamentals

React Three Fiber core patterns for scene composition and game loop

SKILL.md

Full skill instructions

R3F Fundamentals

"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

NeedUse
Basic 3D scene<Canvas> with mesh + geometry + material
Camera controls<OrbitControls> or custom camera rig
Animation loopuseFrame hook
Access Three.jsuseThree hook
Load assetsuseLoader 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 useState for 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)
  • Use manual loaders like FBXLoader.load() in useEffect – breaks R3F's suspense system

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-z for animated values
  • Use drei hooks (useFBX, useGLTF) for model loading – integrates with R3F suspense

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>;
}

FBX Model Loading Pattern

import { useFBX } from '@react-three/​drei';
import { Suspense } from 'react';

// ❌ WRONG - Manual loading breaks R3F suspense
// useEffect(() => {
//   new FBXLoader().load(url, (obj) => setFbx(obj));
// }, [url]);

// ✅ CORRECT - Use drei's useFBX hook
function CharacterModel({ url }: { url: string }) {
  const fbx = useFBX(url);
  return <primitive object={fbx} />;
}

// Wrap in Suspense for loading states
function Scene() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <CharacterModel url="/​models/​character.fbx" />
    </​Suspense>
  );
}

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
  • For FBX/​GLTF models: using useFBX/useGLTF from drei (not manual loaders)

Reference

Zustand + R3F Integration (NEW - 2026-01-28)

State management for R3F scenes using Zustand with vanilla pattern.

Why Zustand for R3F

  • Zero boilerplate - No wrapper components needed
  • Bundle efficient - R3F already uses Zustand internally
  • TypeScript friendly - Excellent type inference
  • DevTools support - Easy state inspection

Store Structure Pattern

// src/​store/​types.ts - Shared types
export interface Vector3Like {
  x: number;
  y: number;
  z: number;
}

export interface PlayerState {
  position: Vector3Like;
  rotation: number;
  health: number;
  weapon: string;
}
// src/​store/​playerStore.ts - Vanilla Zustand (no React dependencies)
import { mutate } from 'zustand/​mutate';
import type { PlayerState, Vector3Like } from './​types';

interface PlayerStore {
  state: PlayerState;
  setPosition: (position: Vector3Like) => void;
  setHealth: (health: number) => void;
  reset: () => void;
}

const initialState: PlayerState = {
  position: { x: 0, y: 0, z: 0 },
  rotation: 0,
  health: 100,
  weapon: 'blaster',
};

export const usePlayerStore = create<PlayerStore>((set) => ({
  state: initialState,
  setPosition: (position) =>
    set((store) => ({
      state: mutate(store.state, { position }),
    })),
  setHealth: (health) =>
    set((store) => ({
      state: mutate(store.state, { health }),
    })),
  reset: () => set({ state: initialState }),
}));

R3F Component Integration

// src/​components/​player/​Player.tsx
import { usePlayerStore } from '@/​store/​playerStore';
import { useRef } from 'react';
import { useFrame } from '@react-three/​fiber';

export function Player() {
  const { state, setPosition } = usePlayerStore();
  const meshRef = useRef<THREE.Mesh>(null);

  // Sync store position to mesh
  useFrame(() => {
    if (meshRef.current) {
      meshRef.current.position.set(
        state.position.x,
        state.position.y,
        state.position.z
      );
    }
  });

  return (
    <mesh ref={meshRef}>
      <boxGeometry args={[1, 2, 1]} />
      <meshStandardMaterial color="orange" />
    </​mesh>
  );
}

DevTools Setup

// src/​store/​playerStore.ts
import { devtools } from 'zustand/​middleware';

export const usePlayerStore = create<PlayerStore>()(
  devtools(
    (set) => ({
      state: initialState,
      // ... actions
    }),
    { name: 'PlayerStore' }
  )
);

Anti-Patterns

❌ DON'T:

  • Put React hooks inside store files (breaks vanilla pattern)
  • Use setState directly in useFrame (causes infinite loops)
  • Create new objects in store actions (use mutate from zustand/​mutate)

✅ DO:

  • Keep store files vanilla (no React imports)
  • Use selectors for specific state slices
  • Expose stores to window.__ZUSTAND__ for debugging

Testing Pattern

// src/​tests/​store/​playerStore.test.ts
import { renderHook, act } from '@testing-library/​react';
import { usePlayerStore } from '@/​store/​playerStore';

describe('playerStore', () => {
  it('updates position', () => {
    const { result } = renderHook(() => usePlayerStore());

    act(() => {
      result.current.setPosition({ x: 10, y: 0, z: 5 });
    });

    expect(result.current.state.position).toEqual({ x: 10, y: 0, z: 5 });
  });
});

Sources:

TPS Camera Reference Values

Validated working camera distances from feat-tps-003 (2026-01-27):

ModeDistance ValueCharacter FramingNotes
Hipfire3.5 units30-40% of screenCharacter waist up visible
Aim (ADS)1.5 unitsCloser viewFor precision shooting

These values provide proper TPS (Third-Person Shooter) framing where the character is clearly visible on the left side of the screen with enough surrounding context.

TPS Camera Shoulder Offset (feat-tps-004, 2026-01-27)

⚠️ CRITICAL: Over-the-shoulder view requires BOTH position offset AND look-at offset.

ParameterValue (left shoulder view)Notes
shoulderOffsetRight0.75 unitsCamera position offset (right)
shoulderOffsetLeft-0.75 unitsCamera position offset (left)
look-at offsetSame as position offsetOften missed - REQUIRED for proper composition

Common anti-pattern from feat-tps-004:

  • ❌ Camera position offset but look-at at center (0, 0, 0)
  • ✅ Look-at point must ALSO be offset by same shoulder amount

Example implementation:

// Position offset (standard)
const _vec3_position = new Vector3(0, 1.6, 3.5)
  .add(_vec3_right.clone().multiplyScalar(shoulderOffset));

// Look-at offset (CRITICAL - often missed)
const _vec3_lookAt = targetPosition.clone()
  .add(_vec3_right.clone().multiplyScalar(shoulderOffset));

camera.position.copy(_vec3_position);
camera.lookAt(_vec3_lookAt);

Acceptance criteria validation:

  • Exact numerical values must match acceptance criteria (0.75, not 0.85)
  • Add code comments referencing acceptance criteria values
  • E2E tests required for camera validation (20+ tests covering offset, distance, swap)

UI Design System Integration for R3F (Added: ui-001 Playtest)

Learned from ui-001: Functional UI is insufficient - requires design system with tokens, aspect ratio, and professional styling.

Design Token System Pattern

// src/​components/​ui/​tokens.ts - Single source of truth for UI values
export const UITokens = {
  // Base resolution for all calculations
  base: {
    width: 1920,
    height: 1080,
    aspectRatio: 16 / 9,
  },

  // Spacing scale (8px base unit)
  spacing: {
    xs: '8px',
    sm: '16px',
    md: '24px',
    lg: '32px',
    xl: '48px',
    xxl: '64px',
  },

  // Typography
  fontSize: {
    xs: '14px',
    sm: '16px',
    base: '18px',
    lg: '24px',
    xl: '32px',
    '2xl': '48px',
    '3xl': '64px',
    display: '128px',
  },

  // Get responsive scale factor
  getScale(windowWidth: number, windowHeight: number): number {
    const scale = Math.min(
      windowWidth / this.base.width,
      windowHeight / (this.base.width * 9 / 16)
    );
    return Math.max(0.5, Math.min(2, scale));
  },
};

16:9 Aspect Ratio Container Pattern

// src/​components/​ui/​AspectContainer.tsx
import React, { useEffect, useState } from 'react';

export function AspectContainer({ children }: { children: React.ReactNode }) {
  const [scale, setScale] = useState(1);

  useEffect(() => {
    const updateScale = () => {
      const s = UITokens.getScale(window.innerWidth, window.innerHeight);
      setScale(s);
    };

    updateScale();
    window.addEventListener('resize', updateScale);
    return () => window.removeEventListener('resize', updateScale);
  }, []);

  return (
    <div className="fixed inset-0 z-10 flex items-center justify-center bg-black">
      <div
        className="relative overflow-hidden"
        style={{
          aspectRatio: 16 / 9,
          transform: `scale(${scale})`,
          transformOrigin: 'center',
        }}
      >
        {children}
      </​div>
    </​div>
  );
}

Using Design Tokens in Components

import { UITokens } from '@/​components/​ui/​tokens';

export function GameButton({ children, onClick }) {
  return (
    <motion.button
      onClick={onClick}
      style={{
        padding: UITokens.spacing.md,
        fontSize: UITokens.fontSize.lg,
        // Use token colors, not hardcoded values
        background: 'var(--color-primary-500)',
      }}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.97 }}
    >
      {children}
    </​motion.button>
  );
}

CSS Variables Approach (Recommended)

/​* src/​index.css - Define CSS variables for design tokens */
:root {
  /​* Base dimensions */
  --ui-base-width: 1920px;
  --ui-base-height: 1080px;
  --ui-aspect-ratio: 16 / 9;

  /​* Spacing scale */
  --spacing-xs: 8px;
  --spacing-sm: 16px;
  --spacing-md: 24px;
  --spacing-lg: 32px;

  /​* Typography */
  --font-display: 'Orbitron', sans-serif;
  --font-ui: 'Rajdhani', sans-serif;

  /​* Colors - Orange/​Cyan theme */
  --color-primary: #f97316;
  --color-secondary: #06b6d4;
  --color-metal-900: #0a0a0a;
  --color-metal-700: #262626;

  /​* Glow effects */
  --glow-primary: rgba(249, 115, 22, 0.6);
}

Button Component with Design System

// src/​components/​ui/​Button.tsx - Production-ready button
import { motion } from 'framer-motion';
import { UITokens } from './​tokens';

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
}

export function Button({
  variant = 'primary',
  size = 'md',
  children,
  onClick,
}: ButtonProps) {
  const sizeStyles = {
    sm: { height: '48px', padding: '0 24px', fontSize: UITokens.fontSize.sm },
    md: { height: '64px', padding: '0 32px', fontSize: UITokens.fontSize.lg },
    lg: { height: '80px', padding: '0 48px', fontSize: UITokens.fontSize.xl },
  };

  return (
    <motion.button
      className={`button button-${variant}`}
      style={sizeStyles[size]}
      onClick={onClick}
      whileHover={{ scale: 1.05, x: -4 }}
      whileTap={{ scale: 0.97, x: 2 }}
      transition={{
        type: 'spring',
        stiffness: 400,
        damping: 17,
      }}
    >
      {children}
    </​motion.button>
  );
}

Easing Curves for Game UI

// src/​components/​ui/​easing.ts
export const GameEasing = {
  snap: [0.22, 1, 0.36, 1] as const,
  reveal: [0.16, 1, 0.3, 1] as const,
  bounce: [0.34, 1.56, 0.64, 1] as const,
  press: [0.4, 0, 0.2, 1] as const,
};

// Usage with Framer Motion
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.4, ease: GameEasing.snap }}
/>

R3F Canvas Integration with UI

// App.tsx - Layered architecture
export function App() {
  return (
    <>
      {/​* 3D Scene Layer */​}
      <Canvas>
        <ambientLight />
        <GameScene />
      </​Canvas>

      {/​* UI Layer - 16:9 container ensures consistent layout */​}
      <AspectContainer>
        <HUD />
      </​AspectContainer>
    </>
  );
}

Sources:

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View