Navigate

Search topics across all sections

GitHub
Loaders

useGLTF Hook

Think of useGLTF like opening an IKEA flatpack. Your model arrives as one box (the GLB file). useGLTF opens it and lays out all the parts on the floor -- the body, the wheels, the glass, the paint colors -- so you can access each piece individually. You can use the whole assembly as-is, or pick out just the parts you need.

You load a model with useGLTF and it appears on screen. Great! But now you want to change the color of just the car's body while keeping the glass transparent. You have no idea how to reach inside the model to modify individual parts.

terminal
Using <primitive object={scene} /> renders the entire model as a black box. Cannot access individual meshes, override materials, or add per-part event handlers.

Real-world

You order a bookshelf from IKEA. It arrives in one box. You could leave everything sealed and just prop the box against the wall, but that is not very useful. Instead, you open the box and lay out every piece: the shelves, the sides, the screws, the backing board.

useGLTF does the same thing. It opens the GLB file and gives you access to every piece inside: nodes (the individual meshes), materials (the surface finishes), and animations (the moving parts). You can use the whole scene as-is with primitive, or you can destructure the parts and build something custom.

GLB File

The sealed box

useGLTF

Open and unpack

nodes

Individual meshes

materials

Surface finishes

Your Scene!

Assemble freely

Unpacking Your Model

useGLTF gives you two ways to work with a model: drop the whole thing in with primitive, or destructure nodes and materials for full control. Let's start simple and then open the box.

Step 1: Load and display the whole model

WholeModel.tsxTSX
const { scene } = useGLTF('/models/robot.glb')
return <primitive object={scene} scale={1.5} />

This is the quickest approach. Like propping the IKEA box against the wall. The model shows up exactly as the designer exported it, but you cannot easily change individual parts.

Step 2: Destructure for full control

DestructuredModel.tsxTSX
const { nodes, materials } = useGLTF('/models/car.glb')

return (
  <group>
    <mesh geometry={nodes.Body.geometry}
          material={materials.Paint} castShadow />
    <mesh geometry={nodes.Glass.geometry}>
      <meshPhysicalMaterial transmission={0.9} />
    </mesh>
  </group>
)

Now you have opened the box. You can pick each piece by name, override its material, add click handlers, or decide which parts cast shadows. This is the recommended approach for any model you need to customize.

Step 3: Preload for instant display

PreloadedCar.tsxTSX
function Car() {
  const { nodes, materials } = useGLTF('/models/car.glb')
  return <primitive object={nodes.Body} />
}

// Start downloading before the component mounts
useGLTF.preload('/models/car.glb')

useGLTF.preload() starts the download as soon as your JavaScript file is imported -- long before the component mounts. When the component finally renders, the model is already cached and appears instantly with no loading spinner.

Step 4: Auto-generate with gltfjsx

terminal.shBASH
npx gltfjsx public/models/robot.glb --transform --types

For complex models with many parts, run gltfjsx to auto-generate a typed React component. It inspects the model and creates a file with every node and material properly destructured and typed. The --transform flag also optimizes the model file itself.

What you just learned

useGLTF is like opening an IKEA flatpack -- it unpacks a GLB file and lays out all the parts (nodes, materials, animations).

Use <primitive object={scene} /> for quick display, or destructure nodes/materials for full control.

useGLTF handles Draco decompression automatically -- no manual DRACOLoader setup needed.

Call useGLTF.preload() at the module level to start downloading before the component mounts.

Use gltfjsx to auto-generate typed React components from complex models.

Question

If useGLTF gives you individual nodes and materials, could you mix and match parts from two different models? Like putting the wheels from one car onto the body of another?

Think about it...

You have a car model with a Body, Glass, and Wheels part. You want the body to be red, the glass to be transparent, and the wheels to cast shadows. Which approach gives you this level of control?

Hint: Think about which approach lets you use React's strengths...

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Change bodyColor — custom robot!

Try This!

Beginner

Set eyeColor to red — evil robot

Try This!

Beginner

Toggle showAntenna — stealth mode

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Not preloading models causes visible delays
Models download only when the component mounts
Don't do this
PreloadModel.tsxTSX
function Scene() {
  // Download starts when Scene mounts
  // User stares at a fallback for 3 seconds
  const { scene } = useGLTF('/models/heavy.glb')
  return <primitive object={scene} />
}
Without preloading, the model download only begins when the component first mounts, causing a visible delay. By calling useGLTF.preload() at the module level, the download starts as soon as the JavaScript is imported. When the component eventually renders, the model is already cached.
2
Rendering the whole scene instead of picking parts
Using primitive when you need control over individual pieces
Don't do this
DestructuredCar.tsxTSX
function Car() {
  const { scene } = useGLTF('/models/car.glb')
  // No control over individual parts
  return <primitive object={scene} />
}
Using <primitive object={scene} /> renders the entire model as a black box. By destructuring nodes and materials, you can cherry-pick specific meshes, override materials, add event handlers, and control shadows individually. This is the recommended pattern for any model you need fine-grained control over.
3
Manually setting up DRACOLoader with useGLTF
useGLTF already handles Draco automatically
Don't do this
AutoDraco.tsxTSX
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'

function Model() {
  const gltf = useLoader(GLTFLoader, '/models/model.glb',
    (loader) => {
      const draco = new DRACOLoader()
      draco.setDecoderPath('/draco/')
      loader.setDRACOLoader(draco)
    })
  return <primitive object={gltf.scene} />
}
useGLTF automatically configures Draco decompression using a CDN-hosted decoder. You do not need to download decoder files or set decoder paths. This is one of the main reasons to prefer useGLTF over raw useLoader + GLTFLoader.

Best Practices

Always Preload

Call useGLTF.preload() at the module level for every model. This starts the download when the JS is parsed, not when the component mounts, eliminating visible loading delays.

Use gltfjsx for Complex Models

Run npx gltfjsx on your model to generate a typed React component with every node properly extracted. The --transform flag also optimizes the model file itself.

Destructure Nodes

Prefer destructuring nodes and materials over rendering the whole scene with primitive. This gives you full React control: event handlers, conditional rendering, material overrides.

Pair with useAnimations

For animated models, use the useAnimations hook from drei. It handles mixer creation, action management, and cleanup automatically. Just pass in the animations from useGLTF.