| 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 |
Applies when building 3D scenes, implementing game loops, handling 3D interactions, optimizing Three.js rendering, loading assets, or managing game state with React.
- Declarative first —
@react-three/fiberJSX, not imperative Three.js - Type everything —
useRef<THREE.Mesh>(null), typed props, typed event handlers useFramefor 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,onPointerOveron 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
useStateinsideuseFrame— 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
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>
);
}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} />;
}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;
});// BAD: setInterval for animation
setInterval(() => { mesh.rotation.y += 0.01 }, 16);
// BAD: useState in useFrame (60 renders/sec)
useFrame(() => setPosition(p => p + 1));
// BAD: untyped ref
const meshRef = useRef(null);
// BAD: forgetting disposal
// Leaks geometry and material on unmount
// BAD: loading textures from user-supplied URLs without validation
useTexture(userInputUrl);- Refs are typed (
useRef<THREE.X>(null)) - Animations use
useFrame+ delta; no timers - No
useStateinsideuseFrame - Geometries / materials / textures disposed
- > 10 similar meshes →
InstancedMesh -
prefers-reduced-motionhonored - Asset URLs validated / from bundled sources
- Frame time under 16.67 ms on target hardware