1294 MODELS INDEXED Account
GUIDE · REACT THREE FIBER

Using GLB models in React Three Fiber

UPDATED
6 MIN READ · BY 3D ASSET

In React Three Fiber, load a GLB with drei’s useGLTF(url, true) inside a <Suspense> boundary, then render <primitive object={scene} />. The second argument enables the Draco decoder. To show the same model more than once, clone the scene per component; for many copies, use instancing.

The smallest working component

import { Suspense } from 'react';
import { Canvas } from '@react-three/fiber';
import { useGLTF, Environment, OrbitControls } from '@react-three/drei';

function Firehouse(props) {
  const { scene } = useGLTF('/glb/firehouse.glb', true);
  return <primitive object={scene} {...props} />;
}

export default function App() {
  return (
    <Canvas camera={{ position: [12, 8, 12] }}>
      <Suspense fallback={null}>
        <Firehouse />
        <Environment preset="city" />
      </Suspense>
      <OrbitControls />
    </Canvas>
  );
}

useGLTF suspends while the file downloads, caches it by URL and returns the same object on every call. Passing true as the second argument makes drei load a Draco decoder; you can pass a decoder path string instead to self-host it. <Environment> gives PBR materials something to reflect, which keeps them from rendering dark.

Show the same model more than once

A three.js object can only have one parent. Rendering the same cached scene twice moves it instead of copying it. Clone it per mount; geometry and materials stay shared, so the copy is cheap:

import { useMemo } from 'react';

function Crate(props) {
  const { scene } = useGLTF('/glb/crate.glb', true);
  const model = useMemo(() => scene.clone(true), [scene]);
  return <primitive object={model} {...props} />;
}

drei’s <Clone object={scene} /> does the same declaratively. Rigged characters need SkeletonUtils.clone() so each copy gets its own skeleton.

Hundreds of copies: instancing

Each cloned mesh is still one draw call. For trees, lamps or crates placed hundreds of times, draw them as instances. drei’s <Merged> turns the meshes of a model into instanced meshes and hands you one component per mesh:

import { Merged } from '@react-three/drei';

function Lamps({ positions }) {
  const { nodes } = useGLTF('/glb/street-lamp.glb', true);
  return (
    <Merged meshes={[nodes.Pole, nodes.Lamp]}>
      {(Pole, Lamp) => positions.map((p, i) => (
        <group key={i} position={p}>
          {/* Keep each part where it sits inside the model. */}
          <Pole position={nodes.Pole.position} rotation={nodes.Pole.rotation} scale={nodes.Pole.scale} />
          <Lamp position={nodes.Lamp.position} rotation={nodes.Lamp.rotation} scale={nodes.Lamp.scale} />
        </group>
      ))}
    </Merged>
  );
}

<Merged> instances the geometry only; it does not remember where each mesh sat inside the model. Passing each node’s own position, rotation and scale restores that, which is enough when the meshes are direct children of the model root. If they are nested under groups that have their own transforms, a node’s own transform is not enough. Either keep using clones, or compute each mesh’s transform relative to the root once: call scene.updateMatrixWorld(true) on the freshly loaded scene, then mesh.matrixWorld.decompose(position, quaternion, scale), and pass those values instead. Check one instance against a clone of the original before placing hundreds.

Mesh names depend on the file. Log Object.keys(nodes) once to see them, or use a typed component as below.

Typed components for every model

Writing nodes.Something by hand is fragile: a renamed mesh breaks at runtime. The gltfjsx tool generates a component and TypeScript types from a file: npx gltfjsx model.glb --types.

3D Asset packs ship these components already generated, one per model, in components/. Each one knows its file URL, types its nodes and materials, clones per mount and preloads its file:

import { CozyTownCottage } from './components/CozyTownCottage';
import { CozyTownCrate } from './components/CozyTownCrate';

<Suspense fallback={null}>
  <CozyTownCottage position={[0, 0, 0]} />
  <CozyTownCrate position={[3, 0, 1]} rotation={[0, Math.PI / 4, 0]} />
  <CozyTownCrate position={[3.6, 0, 1.4]} />
</Suspense>

Each component exports its URL constant (for example CozyTownCrateUrl = '/glb/cozy-town-crate.glb'). Copy the matching file from glb/ into your public folder, or edit the constant if your public path differs. The pack ships no package.json, so it never pins your versions of three, @react-three/fiber or @react-three/drei.

Animations

import { useRef, useEffect } from 'react';
import { useGLTF, useAnimations } from '@react-three/drei';

function Hero(props) {
  const group = useRef();
  const { scene, animations } = useGLTF('/glb/knight.glb', true);
  const { actions } = useAnimations(animations, group);
  useEffect(() => { actions['Idle']?.play(); }, [actions]);
  return <primitive ref={group} object={scene} {...props} />;
}

actions is keyed by clip name. Cross-fade with actions.Run.reset().fadeIn(0.2).play() and actions.Idle.fadeOut(0.2).

Load ahead of time

Call useGLTF.preload(url) at module level for models you know you will need, so they download before the component mounts and the scene does not pop in piece by piece.

Frequently asked questions

Why does my model disappear when I render it twice?

Both components render the same cached object, and an object can only be in one place in the scene graph. Clone it per component with scene.clone(true) or drei’s <Clone>.

Do I need gltfjsx?

No. useGLTF plus <primitive> is enough. gltfjsx is useful when you want typed access to individual meshes and materials; 3D Asset packs include those generated components already.

Does it work with Next.js?

Yes. Put the canvas in a client component ('use client') and serve the .glb files from public/.

Sources

More guides