diff --git a/packages/fiber/src/core/hooks.tsx b/packages/fiber/src/core/hooks.tsx index 40ab66cd5f..326ede0800 100644 --- a/packages/fiber/src/core/hooks.tsx +++ b/packages/fiber/src/core/hooks.tsx @@ -92,16 +92,27 @@ export type Extensions = (loader: Loader) => void const memoizedLoaders = new WeakMap, Loader>() +const isConstructor = (value: unknown): value is LoaderProto => + typeof value === 'function' && value?.prototype?.constructor === value + function loadingFn(extensions?: Extensions, onProgress?: (event: ProgressEvent) => void) { - return function (Proto: LoaderProto, ...input: string[]) { - // Construct new loader and run extensions - let loader = memoizedLoaders.get(Proto)! - if (!loader) { - loader = new Proto() - memoizedLoaders.set(Proto, loader) + return async function (Proto: Loader | LoaderProto, ...input: string[]) { + let loader: Loader + + // Construct and cache loader if constructor was passed + if (isConstructor(Proto)) { + loader = memoizedLoaders.get(Proto)! + if (!loader) { + loader = new Proto() + memoizedLoaders.set(Proto, loader) + } + } else { + loader = Proto } + // Apply loader extensions if (extensions) extensions(loader) + // Go through the urls and load them return Promise.all( input.map( @@ -128,14 +139,14 @@ type GLTFLike = { scene: THREE.Object3D } * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#useloader */ export function useLoader( - Proto: LoaderProto, + loader: Loader | LoaderProto, input: U, extensions?: Extensions, onProgress?: (event: ProgressEvent) => void, ) { // Use suspense to load async assets const keys = (Array.isArray(input) ? input : [input]) as string[] - const results = suspend(loadingFn(extensions, onProgress), [Proto, ...keys], { equal: is.equ }) + const results = suspend(loadingFn(extensions, onProgress), [loader, ...keys], { equal: is.equ }) // Return the object(s) return (Array.isArray(input) ? results : results[0]) as unknown as U extends any[] ? LoaderResult[] @@ -146,18 +157,21 @@ export function useLoader( * Preloads an asset into cache as a side-effect. */ useLoader.preload = function ( - Proto: LoaderProto, + loader: Loader | LoaderProto, input: U, extensions?: Extensions, ): void { const keys = (Array.isArray(input) ? input : [input]) as string[] - return preload(loadingFn(extensions), [Proto, ...keys]) + return preload(loadingFn(extensions), [loader, ...keys]) } /** * Removes a loaded asset from cache. */ -useLoader.clear = function (Proto: LoaderProto, input: U): void { +useLoader.clear = function ( + loader: Loader | LoaderProto, + input: U, +): void { const keys = (Array.isArray(input) ? input : [input]) as string[] - return clear([Proto, ...keys]) + return clear([loader, ...keys]) } diff --git a/packages/fiber/tests/hooks.test.tsx b/packages/fiber/tests/hooks.test.tsx index e3e4dec5d0..90edb1de54 100644 --- a/packages/fiber/tests/hooks.test.tsx +++ b/packages/fiber/tests/hooks.test.tsx @@ -159,6 +159,24 @@ describe('hooks', () => { expect(extensions).toBeCalledTimes(1) }) + it('can handle useLoader with an existing loader instance', async () => { + class Loader extends THREE.Loader { + load(_url: string, onLoad: (result: null) => void): void { + onLoad(null) + } + } + + const loader = new Loader() + let proto!: Loader + + function Test(): null { + return useLoader(loader, '', (loader) => (proto = loader)) + } + await act(async () => root.render()) + + expect(proto).toBe(loader) + }) + it('can handle useLoader with a loader extension', async () => { class Loader extends THREE.Loader { load(_url: string, onLoad: (result: null) => void): void {