Loading Assets
Domphy has no Suspense analog. loadAsset is the async primitive @domphy/three ports from @react-three/fiber's useLoader — instead of a component suspending, you get a plain reactive AssetResult back immediately and read its data/error states wherever you need them.
loadAsset
function loadAsset<T>(
LoaderClass: Constructable,
input: string | string[],
configure?: (loader: any) => void,
): AssetResult<T>| Param | Type | Description |
|---|---|---|
LoaderClass | Constructable | Any three.js loader class (GLTFLoader, TextureLoader, ...). Instantiated once and reused for every call with that class. |
input | string | string[] | One url, or an array resolved in order. |
configure | (loader: any) => void | Runs right before the loader's first .load() call for this call — set decoders, paths, crossOrigin, etc. |
AssetResult<T>
| Field | Type | Description |
|---|---|---|
data | ReadableState<T | null> | null until the load resolves, then the loaded value. |
error | ReadableState<Error | null> | null unless the load fails. |
promise | Promise<T> | The underlying load promise. |
Cache key = LoaderClass + input (an array input joins into one key). Call loadAsset as many times as you like with the same class/input — inside a reactive scene function, a render loop, wherever — you always get back the exact same AssetResult; the network/parse work happens once.
If the resolved value has a .scene that's an Object3D (a glTF result), loadAsset walks it once and assigns nodes/materials/meshes — named lookups onto the result itself, mirroring r3f's useGLTF.
Rendering the result
Read data inside scene's listener. Falsy scene children are skipped, so render a placeholder for the slot until the asset resolves:
preloadAsset
function preloadAsset(LoaderClass: Constructable, input: string | string[], configure?: (loader: any) => void): Promise<unknown>Kicks off the same load loadAsset would, without handing back the reactive AssetResult — warm the cache before a route or page mounts:
import { preloadAsset } from "@domphy/three"
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"
preloadAsset(GLTFLoader, "/models/Duck.glb")A later loadAsset(GLTFLoader, "/models/Duck.glb") call hits the warmed cache and returns the same AssetResult, already resolved.
clearAsset
function clearAsset(LoaderClass: Constructable, input?: string | string[]): void| Call | Effect |
|---|---|
clearAsset(LoaderClass, input) | Drops the one cached AssetResult for that class/input. |
clearAsset(LoaderClass) | Drops every input cached for that class. |
clearAsset(GLTFLoader, "/models/Duck.glb") // drop one cached asset
clearAsset(GLTFLoader) // drop everything GLTFLoader has cachedThe loader instance itself is untouched — only the AssetResult cache entry is dropped. The next loadAsset call for that class/input reloads from scratch and returns a brand-new AssetResult.
Error handling
result.error is a ReadableState<Error | null>, set when the loader's error callback fires; result.data stays null. loadAsset never throws synchronously, and data/error live on the AssetResult object, not on any DOM node — reading them inside three()'s own scene function only affects the scene tree, it never reaches errorBoundary() (@domphy/ui), which catches errors thrown inside a core reactive-children function (div: (l) => [...]), not inside @domphy/three's own internal reactivity.
Route the check through the surrounding Domphy DOM instead — read result.error in a reactive div children function that wraps the three() mount, and throw there so errorBoundary() can catch it:
configure — loader setup and DRACO
configure(loader) runs synchronously right before the loader's first .load() call for every distinct (LoaderClass, input) pair that misses the cache — not once per loader instance. A LoaderClass is instantiated once and reused across every subsequent loadAsset call with that same class, even with a different input, so if two calls each pass their own configure, both run against the same shared instance. Keep configure idempotent (setDRACOLoader, setPath, setCrossOrigin, ...) so a later call doesn't need to differ from an earlier one.
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js"
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"
import { loadAsset } from "@domphy/three"
const result = loadAsset(GLTFLoader, "/models/compressed.glb", (loader) => {
const draco = new DRACOLoader()
draco.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.6/")
loader.setDRACOLoader(draco)
})