r3f-native-patterns

star 0

React Three Fiber native setup, Canvas configuration, scene structure, useFrame, useThree, lights, cameras, and the render loop.

smartwatermelon By smartwatermelon schedule Updated 1/28/2026

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();
  };
}
Install via CLI
npx skills add https://github.com/smartwatermelon/smartwatermelon-marketplace --skill r3f-native-patterns
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
smartwatermelon
smartwatermelon Explore all skills →