Navigate

Search topics across all sections

GitHub
R3F Hooks

useLoader

3D scenes need assets -- textures, 3D models, HDR environments, fonts. Loading them manually with useEffect and state management is tedious and error-prone. useLoader wraps the entire process into one line: load, cache, and suspend until ready.

You load a texture with useEffect and useState. It works fine at first. Then you navigate away and come back -- the texture re-downloads. You mount the same component in two places -- two separate network requests for the same file. Your loading state is a mess of null checks.

terminal
Network tab: GET /textures/earth.jpg (2.4 MB) x3
Three separate downloads of the same file.
No caching. No Suspense fallback. Manual null checks everywhere.
Total wasted bandwidth: 4.8 MB.

Real-world

Think of useLoader as your personal delivery service -- like ordering from Amazon Prime. The first time you order a texture, it gets downloaded and stored in a local warehouse (the cache). The next time you -- or anyone else in your app -- requests the same texture, it arrives instantly from the warehouse. No re-downloading.

And while you wait for that first delivery, Suspense puts up a nice "loading" sign so your visitors never see a broken scene.

How useLoader Works

Call useLoader

Pass loader class + URL

Check cache

Already loaded? Return instantly

Suspend

Not cached? Throw a Promise

Suspense catches it

Shows your fallback UI

Asset arrives

Cached globally, component renders

See It In Action

The demo below loads textures with useLoader. Notice how the Suspense fallback appears briefly, then the textured model pops in once the assets are ready.

Building It Step by Step

Step 1 -- Load a texture in one line

Pass the loader class as the first argument and the URL as the second. The hook suspends until the asset is ready, then returns it directly.

Earth.tsxTSX
const texture = useLoader(TextureLoader, '/earth.jpg')

return (
  <mesh>
    <sphereGeometry args={[1.5, 64, 64]} />
    <meshStandardMaterial map={texture} />
  </mesh>
)

No null checks, no loading state, no useEffect. The texture is guaranteed to exist when the component renders.

Step 2 -- Wrap in Suspense with a fallback

While the asset loads, React needs something to show. Wrap your component in a Suspense boundary with a 3D fallback -- a wireframe, a spinner, or a placeholder shape.

App.tsxTSX
<Canvas>
  <Suspense fallback={<PlaceholderSphere />}>
    <Earth />
  </Suspense>
</Canvas>

The fallback can be any valid R3F component. Wireframe versions of your actual geometry work great for a smooth visual transition.

Step 3 -- Preload and batch for speed

Start downloading critical assets before the component mounts. Load multiple assets of the same type in parallel by passing an array.

Preload.tsxTSX
// Preload at module level -- starts immediately
useLoader.preload(GLTFLoader, '/models/character.glb')

// Batch load PBR textures in parallel
const [color, normal, rough] = useLoader(TextureLoader, [
  '/textures/color.jpg',
  '/textures/normal.jpg',
  '/textures/roughness.jpg',
])

Preloading runs at module evaluation time, which is as early as possible. Array loading creates a single suspension point for all assets instead of loading them one by one.

What you just learned

useLoader loads textures, 3D models, fonts, and HDR environments in one line

Assets are cached globally by URL -- no duplicate downloads

It integrates with React Suspense for declarative loading states

Pass an array of URLs to batch-load multiple assets in parallel

useLoader.preload() starts downloading before the component even mounts

Question

What about drei's useGLTF and useTexture? They are convenience wrappers around useLoader with extra features. useGLTF auto-configures Draco decompression. useTexture accepts a PBR map object with named keys. Under the hood, they both use useLoader and its cache.

Think about it...

You have a hero section that loads a 5MB GLTF model. Users see a loading spinner for 3 seconds. How can you eliminate or reduce this wait?

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

Set roughness to 0 -- mirror sphere!

Try This!

Beginner

Set metalness to 1 -- chrome effect!

Try This!

Beginner

Speed up orbitSpeed to 5 -- dizzy cube!

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

1
Loading assets with useEffect instead of useLoader
Bypasses caching and Suspense integration
Don't do this
TexturedBox.tsxTSX
function TexturedBox() {
  const [texture, setTexture] = useState(null)

  useEffect(() => {
    new THREE.TextureLoader().load('/wood.jpg', (tex) => {
      setTexture(tex) // no caching, no Suspense
    })
  }, [])

  if (!texture) return null
  return <mesh>...</mesh>
}
Using useEffect with manual loading bypasses R3F's global cache. Every time the component mounts, it re-downloads the asset. useLoader caches by URL -- if two components request the same texture, only one network request is made. It also integrates with React Suspense, so you can show loading fallbacks declaratively.
2
Missing Suspense boundary
App crashes or shows a blank screen while loading
Don't do this
App.tsxTSX
function App() {
  return (
    <Canvas>
      {/* useLoader suspends -- but there is no Suspense! */}
      <TexturedSphere />
    </Canvas>
  )
}
useLoader uses React Suspense internally. While loading, the component throws a Promise. Without a Suspense boundary to catch it, React has nowhere to show a fallback and crashes. Always wrap useLoader components in Suspense with a meaningful placeholder like a wireframe shape.
3
Not preloading critical assets
Users see loading spinners that could have been avoided
Don't do this
Product.tsxTSX
// Loading starts only when component mounts
function Product() {
  const gltf = useLoader(GLTFLoader, '/product.glb')
  return <primitive object={gltf.scene} />
}
Without preloading, downloads only begin when the component first renders. useLoader.preload() starts the download immediately when the module is evaluated -- during app initialization. The cache is shared, so when useLoader() runs later with the same URL, the asset is already ready.

Best Practices

Always wrap in Suspense

Every component using useLoader needs a <Suspense> ancestor with a fallback. Wireframe placeholders work great.

Preload critical assets

Call useLoader.preload() at module level for assets needed on the first screen. Downloads start before React even renders.

Use Draco compression

Draco compresses GLTF models by 5-10x. drei's useGLTF sets up Draco automatically.

Batch same-type loads

Pass an array of URLs to load multiple assets in parallel. This creates a single suspension point and is more efficient.