Domphy

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>
ParamTypeDescription
LoaderClassConstructableAny three.js loader class (GLTFLoader, TextureLoader, ...). Instantiated once and reused for every call with that class.
inputstring | string[]One url, or an array resolved in order.
configure(loader: any) => voidRuns right before the loader's first .load() call for this call — set decoders, paths, crossOrigin, etc.

AssetResult<T>

FieldTypeDescription
dataReadableState<T | null>null until the load resolves, then the loaded value.
errorReadableState<Error | null>null unless the load fails.
promisePromise<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
CallEffect
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 cached

The 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)
})