smithery.ai

r3f-native-patterns

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

First seen Mar 31, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,754 B
  • docs SUMMARY.md 155 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

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;

Avoid Module-Level Three.js Objects

Warning: Don't declare Three.js objects (Vector3, Quaternion, etc.) as module-level constants. R3F attaches internal r3f tracking metadata to Three.js objects it manages; a shared module-level instance can end up tracked by multiple component instances at once, causing cross-instance interference and Cannot delete property 'r3f' of undefined errors during cleanup.

// ❌ WRONG - Shared across all component instances
const Y_AXIS = new THREE.Vector3(0, 1, 0);

export function MyComponent() {
  const rotation = useMemo(() => {
    // Uses shared Y_AXIS - can cause __r3f tracking conflicts
    return quaternionFromAxis(Y_AXIS);
  }, []);
}

// ✅ CORRECT - Created fresh inside the function
export function MyComponent() {
  const rotation = useMemo(() => {
    const yAxis = new THREE.Vector3(0, 1, 0);  // Local to this call
    return quaternionFromAxis(yAxis);
  }, []);
}

See debugging-3d-mobile for the full __r3f deletion error writeup.

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

React Native Touch Coordinates

Callout: When reading raw touch data off nativeEvent (e.g. inside useThree-driven pointer handling or custom gesture code), React Native uses locationX/locationY — not the web-standard clientX/clientY. Reading the web property silently yields undefined on device, which breaks raycasting and orbit controls.

// ❌ WRONG - Works on web, undefined on React Native
const x = e.nativeEvent.clientX;

// ✅ CORRECT - React Native touch coordinates
const x = (e as unknown as { locationX: number }).locationX;
const y = (e as unknown as { locationY: number }).locationY;

See touch-gesture-3d for the full touch/gesture handling guide.

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

Primitive Arrays vs Three.js Objects in useMemo

Warning: If a useMemo returns a Three.js object (Quaternion, Vector3, etc.) that gets passed as a prop, R3F attaches r3f tracking metadata to it. A new object created on re-render orphans the old one, and R3F's cleanup effect can throw Cannot delete property 'r3f' of undefined trying to detach it. Return primitive arrays instead — they carry no object identity for R3F to track.

// ❌ WRONG - R3F tracks this Quaternion object; new instance each recompute
const { quaternion } = useMemo(() => {
  const quat = new THREE.Quaternion().setFromUnitVectors(yAxis, direction);
  return { quaternion: quat };  // Three.js object returned
}, [deps]);

<mesh quaternion={quaternion} />

// ✅ CORRECT - Primitive array, no object tracking
const { quaternion } = useMemo(() => {
  const quat = new THREE.Quaternion().setFromUnitVectors(yAxis, direction);
  return { quaternion: [quat.x, quat.y, quat.z, quat.w] as [number, number, number, number] };
}, [deps]);

<mesh quaternion={quaternion} />

See debugging-3d-mobile for the full __r3f deletion error writeup, including the state-management (Zustand/Redux) variant of this same problem.

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