Navigate
Search topics across all sections
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.
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.
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.
<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 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.
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>
}function App() {
return (
<Canvas>
{/* useLoader suspends -- but there is no Suspense! */}
<TexturedSphere />
</Canvas>
)
}// Loading starts only when component mounts
function Product() {
const gltf = useLoader(GLTFLoader, '/product.glb')
return <primitive object={gltf.scene} />
}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.