| name | react-threejs-game |
| description | Three.js game development with React using @react-three/fiber and @react-three/drei — strict TypeScript, 60 fps, accessible |
| license | MIT |
react-threejs-game Skill
Context
Applies when building 3D scenes, implementing game loops, handling 3D interactions, optimizing Three.js rendering, loading assets, or managing game state with React.
Rules
- Declarative first —
@react-three/fiber JSX, not imperative Three.js
- Type everything —
useRef<THREE.Mesh>(null), typed props, typed event handlers
useFrame for the game loop — ((state, delta) => …) — delta time, not wall-clock
- Refs for Three.js objects — never mutate props
- Minimize re-renders — 60 Hz updates go through refs, not
useState
- Use Drei helpers —
OrbitControls, useTexture, Html, Sparkles, Trail
- Mesh events —
onClick, onPointerOver on meshes — no manual raycasting
- Dispose resources — geometries, materials, textures, audio buffers on unmount
- InstancedMesh for > 10 similar objects (particles, bullets, enemies)
- Target 60 fps — frame time ≤ 16.67 ms; profile with React DevTools + Spector.js
- Separate concerns — logic in hooks, rendering in JSX, state in React
- No
useState inside useFrame — use refs for transient animation state
- Accessibility — keyboard equivalents,
prefers-reduced-motion, readable HUD contrast
- Asset safety — load textures/models only from trusted origins; no user-supplied URLs without validation
Examples
✅ Typed interactive object with useFrame
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
interface TargetProps {
position: readonly [number, number, number];
size: number;
onClick: () => void;
}
export function Target({ position, size, onClick }: TargetProps): JSX.Element {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
const mesh = meshRef.current;
if (!mesh) return;
mesh.rotation.y += delta * 0.5;
mesh.position.y = position[1] + Math.sin(state.clock.elapsedTime) * 0.3;
});
return (
<mesh ref={meshRef} position={position} onClick={onClick}>
<sphereGeometry args={[size, 16, 16]} />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
✅ Resource disposal on unmount
import { useEffect, useMemo } from 'react';
import * as THREE from 'three';
function Ring(): JSX.Element {
const geometry = useMemo(() => new THREE.TorusGeometry(1, 0.2, 16, 64), []);
const material = useMemo(() => new THREE.MeshStandardMaterial({ color: 'cyan' }), []);
useEffect(() => {
return () => {
geometry.dispose();
material.dispose();
};
}, [geometry, material]);
return <mesh geometry={geometry} material={material} />;
}
✅ Respecting reduced-motion
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
useFrame((_, delta) => {
const speed = prefersReduced ? 0 : 0.5;
if (meshRef.current) meshRef.current.rotation.y += delta * speed;
});
❌ Anti-Patterns
setInterval(() => { mesh.rotation.y += 0.01 }, 16);
useFrame(() => setPosition(p => p + 1));
const meshRef = useRef(null);
useTexture(userInputUrl);
Validation Checklist