name: r3f-native-patterns description: React Three Fiber native setup, Canvas configuration, scene structure, useFrame, useThree, lights, cameras, and the render loop.
React Three Fiber Native Patterns
This skill covers core R3F patterns specific to React Native/Expo.
Installation
npm install three @react-three/fiber @react-three/drei expo-gl
For TypeScript:
npm install -D @types/three
Metro Configuration
Critical: Configure Metro to handle 3D asset files.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
// Add 3D model extensions
config.resolver.assetExts.push('glb', 'gltf', 'obj', 'mtl', 'hdr');
// Add source extensions for R3F
config.resolver.sourceExts.push('cjs', 'mjs');
module.exports = config;
Basic Canvas Setup
import { Canvas } from '@react-three/fiber/native';
import { View, StyleSheet } from 'react-native';
export function Basic3DScene() {
return (
<View style={styles.container}>
<Canvas
camera={{ position: [0, 2, 5], fov: 75 }}
gl={{ antialias: true }}
>
<Scene />
</Canvas>
</View>
);
}
function Scene() {
return (
<>
{/* Lights */}
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={1} />
{/* Objects */}
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
Canvas Props Reference
<Canvas
// Camera configuration
camera={{
position: [0, 0, 5], // Initial position
fov: 75, // Field of view (perspective)
near: 0.1, // Near clipping plane
far: 1000, // Far clipping plane
orthographic: false, // Use orthographic camera
}}
// WebGL configuration
gl={{
antialias: true, // Smooth edges
alpha: true, // Transparent background
powerPreference: 'high-performance',
}}
// Performance
dpr={[1, 2]} // Pixel ratio range
frameloop="always" // 'always' | 'demand' | 'never'
// Events
onCreated={(state) => {}} // Called when canvas initializes
onPointerMissed={() => {}} // Click/tap on empty space
/>
The Render Loop: useFrame
useFrame runs every frame (~60fps). Use it for animations and continuous updates.
import { useFrame } from '@react-three/fiber/native';
import { useRef } from 'react';
import * as THREE from 'three';
function RotatingCube() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
// state: R3F state (camera, scene, gl, etc.)
// delta: Time since last frame in seconds
if (meshRef.current) {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta * 0.5;
}
});
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
useFrame State Object
useFrame((state) => {
state.clock // THREE.Clock - elapsed time
state.camera // Current camera
state.scene // THREE.Scene
state.gl // WebGL renderer
state.size // { width, height } of canvas
state.pointer // Normalized pointer position { x, y }
state.raycaster // THREE.Raycaster for picking
});
Frame Priority
Control execution order with priority (lower = earlier):
// Physics runs first
useFrame((state) => {
updatePhysics();
}, -1);
// Then rendering updates
useFrame((state) => {
updateVisuals();
}, 0);
// Finally, post-processing
useFrame((state) => {
applyEffects();
}, 1);
Accessing Three.js State: useThree
import { useThree } from '@react-three/fiber/native';
function CameraInfo() {
const { camera, gl, scene, size, pointer, raycaster } = useThree();
console.log('Canvas size:', size.width, size.height);
console.log('Camera position:', camera.position);
return null;
}
Subscribing to State Changes
function ResponsiveObject() {
const size = useThree((state) => state.size);
// Re-renders only when size changes
return (
<mesh scale={size.width / 100}>
<boxGeometry />
<meshBasicMaterial color="blue" />
</mesh>
);
}
Scene Organization
Group Components Logically
function GameScene() {
return (
<>
<Lighting />
<Environment />
<Player />
<Obstacles />
<UI3D />
</>
);
}
function Lighting() {
return (
<group name="lighting">
<ambientLight intensity={0.3} />
<directionalLight position={[10, 10, 5]} castShadow />
<pointLight position={[-10, 5, -10]} color="#ff9999" />
</group>
);
}
function Environment() {
return (
<group name="environment">
<Ground />
<Sky />
<Trees />
</group>
);
}
Use Groups for Transform Hierarchies
function Robot() {
const groupRef = useRef<THREE.Group>(null);
// Moving the group moves all children together
useFrame((state) => {
if (groupRef.current) {
groupRef.current.position.x = Math.sin(state.clock.elapsedTime);
}
});
return (
<group ref={groupRef}>
<Body />
<group position={[0, 1.5, 0]}>
<Head />
</group>
<group position={[-0.5, 0.5, 0]}>
<Arm side="left" />
</group>
<group position={[0.5, 0.5, 0]}>
<Arm side="right" />
</group>
</group>
);
}
Cameras
Perspective Camera (Default)
<Canvas camera={{
position: [0, 5, 10],
fov: 50, // Field of view in degrees
near: 0.1,
far: 1000,
}}>
Orthographic Camera
<Canvas
orthographic
camera={{
position: [0, 0, 10],
zoom: 50,
near: 0.1,
far: 1000,
}}
>
Switching Cameras
function CameraSwitcher() {
const perspCam = useRef<THREE.PerspectiveCamera>(null);
const orthoCam = useRef<THREE.OrthographicCamera>(null);
const set = useThree((state) => state.set);
const [isOrtho, setIsOrtho] = useState(false);
useEffect(() => {
if (isOrtho && orthoCam.current) {
set({ camera: orthoCam.current });
} else if (!isOrtho && perspCam.current) {
set({ camera: perspCam.current });
}
}, [isOrtho, set]);
return (
<>
<PerspectiveCamera ref={perspCam} makeDefault={!isOrtho} position={[0, 5, 10]} />
<OrthographicCamera ref={orthoCam} makeDefault={isOrtho} position={[0, 0, 10]} zoom={50} />
</>
);
}
Lighting
Common Light Setup
function StandardLighting() {
return (
<>
{/* Base illumination */}
<ambientLight intensity={0.4} color="#ffffff" />
{/* Main directional (sun-like) */}
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow
shadow-mapSize={[1024, 1024]}
/>
{/* Fill light (soften shadows) */}
<pointLight position={[-10, 5, -5]} intensity={0.3} color="#aaccff" />
{/* Rim light (edge definition) */}
<spotLight position={[0, 10, -10]} intensity={0.5} angle={0.3} />
</>
);
}
Light Types
| Light | Use Case | Performance |
|---|---|---|
ambientLight |
Base fill, no shadows | Cheapest |
directionalLight |
Sun, parallel rays | Medium |
pointLight |
Bulbs, omni-directional | Medium |
spotLight |
Focused cone | More expensive |
hemisphereLight |
Sky + ground colors | Cheap |
Materials Quick Reference
// No lighting needed
<meshBasicMaterial color="red" />
// Standard PBR (needs lights)
<meshStandardMaterial
color="red"
metalness={0.5}
roughness={0.5}
/>
// Physical PBR (more realistic)
<meshPhysicalMaterial
color="red"
metalness={0.8}
roughness={0.2}
clearcoat={1}
/>
// Wireframe
<meshBasicMaterial color="white" wireframe />
// Transparent
<meshBasicMaterial color="blue" transparent opacity={0.5} />
// Double-sided
<meshBasicMaterial color="green" side={THREE.DoubleSide} />
Loading Assets
GLTF/GLB Models
import { useGLTF } from '@react-three/drei/native';
function Model({ url }) {
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
// Preload for better UX
useGLTF.preload('/model.glb');
Textures
import { useTexture } from '@react-three/drei/native';
function TexturedBox() {
const texture = useTexture(require('./texture.png'));
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={texture} />
</mesh>
);
}
Suspense for Loading
import { Suspense } from 'react';
function App() {
return (
<Canvas>
<Suspense fallback={<LoadingIndicator />}>
<HeavyModel />
</Suspense>
</Canvas>
);
}
function LoadingIndicator() {
return (
<mesh>
<sphereGeometry args={[0.5]} />
<meshBasicMaterial color="gray" wireframe />
</mesh>
);
}
Disposing Resources
Critical for mobile memory management:
function DisposableScene() {
const geometryRef = useRef<THREE.BufferGeometry>();
const materialRef = useRef<THREE.Material>();
useEffect(() => {
return () => {
// Clean up on unmount
geometryRef.current?.dispose();
materialRef.current?.dispose();
};
}, []);
return (
<mesh>
<boxGeometry ref={geometryRef} />
<meshStandardMaterial ref={materialRef} />
</mesh>
);
}
Performance Patterns
Memoize Expensive Objects
function OptimizedMesh({ color }) {
// Geometry created once
const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
// Material recreated only when color changes
const material = useMemo(
() => new THREE.MeshStandardMaterial({ color }),
[color]
);
return <mesh geometry={geometry} material={material} />;
}
Conditional Rendering
function LODObject({ distance }) {
if (distance > 100) return null; // Don't render if too far
return (
<mesh>
{distance < 20 ? (
<sphereGeometry args={[1, 32, 32]} /> // High detail
) : (
<sphereGeometry args={[1, 8, 8]} /> // Low detail
)}
<meshStandardMaterial />
</mesh>
);
}
Frameloop Control
// Only render when needed (for static scenes)
<Canvas frameloop="demand">
<StaticScene />
</Canvas>
// Inside scene, trigger render when needed:
function StaticScene() {
const invalidate = useThree((state) => state.invalidate);
const handleChange = () => {
// Something changed, request a render
invalidate();
};
}