From 285896037960e9b1f2211e004df2d55d367b7435 Mon Sep 17 00:00:00 2001 From: Shushovan shakya Date: Fri, 3 Apr 2026 13:34:46 +0200 Subject: [PATCH 1/3] ci: update test workflow actions to remove Node 20 deprecation warning --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 433eb533f3..12bce66a31 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,15 +18,15 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Use Node 20 - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 - name: Cache node_modules and Yarn cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | **/node_modules From da86754c518e321050728695c7388784d8f9a268 Mon Sep 17 00:00:00 2001 From: Shushovan shakya Date: Fri, 3 Apr 2026 13:42:54 +0200 Subject: [PATCH 2/3] feat(useLoader): support custom cache keys --- docs/API/hooks.mdx | 13 ++++++++ packages/fiber/src/core/hooks.tsx | 16 ++++++--- packages/fiber/tests/hooks.test.tsx | 51 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/API/hooks.mdx b/docs/API/hooks.mdx index 7a4f5dbe6a..31398a0042 100644 --- a/docs/API/hooks.mdx +++ b/docs/API/hooks.mdx @@ -194,6 +194,12 @@ function App() { > [!NOTE] > Assets loaded with useLoader are cached by default. The urls given serve as cache-keys. This allows you to re-use loaded data everywhere in the component tree. +You can also provide an explicit cache key as the fifth argument when the request URL changes (for example signed URLs or auth tokens): + +```jsx +const model = useLoader(GLTFLoader, signedUrl, undefined, undefined, '/model.glb') +``` + > [!WARNING] > Be very careful with mutating or disposing of loaded assets, especially when you plan to re-use them. Refer to the automatic disposal section in the API. @@ -245,6 +251,13 @@ You can pre-load assets in global space so that models can be loaded in anticipa useLoader.preload(GLTFLoader, '/model.glb' /* extensions */) ``` +You can pass the same cache key to `preload` and `clear`: + +```jsx +useLoader.preload(GLTFLoader, signedUrl, undefined, '/model.glb') +useLoader.clear(GLTFLoader, signedUrl, '/model.glb') +``` + ## `useGraph` Convenience hook which creates a memoized, named object/material collection from any [`Object3D`](https://threejs.org/docs/#api/en/core/Object3D). diff --git a/packages/fiber/src/core/hooks.tsx b/packages/fiber/src/core/hooks.tsx index 9053128969..34ee849403 100644 --- a/packages/fiber/src/core/hooks.tsx +++ b/packages/fiber/src/core/hooks.tsx @@ -64,6 +64,7 @@ export function useGraph(object: THREE.Object3D): ObjectMap { type InputLike = string | string[] | string[][] | Readonly type LoaderLike = THREE.Loader type GLTFLike = { scene: THREE.Object3D } +type CacheKeyLike = InputLike type LoaderInstance> = T extends ConstructorRepresentation ? InstanceType : T @@ -139,10 +140,12 @@ export function useLoader, onProgress?: (event: ProgressEvent) => void, + cacheKey?: CacheKeyLike, ) { // Use suspense to load async assets - const keys = (Array.isArray(input) ? input : [input]) as string[] - const results = suspend(loadingFn(extensions, onProgress), [loader, ...keys], { equal: is.equ }) + const inputs = (Array.isArray(input) ? input : [input]) as string[] + const keys = (Array.isArray(cacheKey ?? input) ? (cacheKey ?? input) : [cacheKey ?? input]) as string[] + const results = suspend(() => loadingFn(extensions, onProgress)(loader, ...inputs), [loader, ...keys], { equal: is.equ }) // Return the object(s) return (Array.isArray(input) ? results : results[0]) as I extends any[] ? LoaderResult[] : LoaderResult } @@ -154,9 +157,11 @@ useLoader.preload = function , + cacheKey?: CacheKeyLike, ): void { - const keys = (Array.isArray(input) ? input : [input]) as string[] - return preload(loadingFn(extensions), [loader, ...keys]) + const inputs = (Array.isArray(input) ? input : [input]) as string[] + const keys = (Array.isArray(cacheKey ?? input) ? (cacheKey ?? input) : [cacheKey ?? input]) as string[] + return preload(() => loadingFn(extensions)(loader, ...inputs), [loader, ...keys]) } /** @@ -165,7 +170,8 @@ useLoader.preload = function >( loader: L, input: I, + cacheKey?: CacheKeyLike, ): void { - const keys = (Array.isArray(input) ? input : [input]) as string[] + const keys = (Array.isArray(cacheKey ?? input) ? (cacheKey ?? input) : [cacheKey ?? input]) as string[] return clear([loader, ...keys]) } diff --git a/packages/fiber/tests/hooks.test.tsx b/packages/fiber/tests/hooks.test.tsx index d0419e1ce4..27d947df5f 100644 --- a/packages/fiber/tests/hooks.test.tsx +++ b/packages/fiber/tests/hooks.test.tsx @@ -194,6 +194,57 @@ describe('hooks', () => { expect(proto).toBeInstanceOf(Loader) }) + it('can use a custom cache key with useLoader', async () => { + const loadedObjects: THREE.Object3D[] = [] + const loadMock = jest.fn((url: string, onLoad: (result: THREE.Object3D) => void) => { + const object = new THREE.Group() + object.name = url + loadedObjects.push(object) + onLoad(object) + }) + + class Loader extends THREE.Loader { + load = loadMock + } + + function Test({ url, cacheKey }: { url: string; cacheKey: string }) { + const object = useLoader(Loader, url, undefined, undefined, cacheKey) + return + } + + await act(async () => root.render()) + await act(async () => root.render()) + + expect(loadMock).toHaveBeenCalledTimes(1) + expect(loadedObjects).toHaveLength(1) + }) + + it('can clear useLoader cache by custom cache key', async () => { + const loadedObjects: THREE.Object3D[] = [] + const loadMock = jest.fn((url: string, onLoad: (result: THREE.Object3D) => void) => { + const object = new THREE.Group() + object.name = url + loadedObjects.push(object) + onLoad(object) + }) + + class Loader extends THREE.Loader { + load = loadMock + } + + function Test({ url, cacheKey }: { url: string; cacheKey: string }) { + const object = useLoader(Loader, url, undefined, undefined, cacheKey) + return + } + + await act(async () => root.render()) + useLoader.clear(Loader, '/model.glb?token=def', 'model-1') + await act(async () => root.render()) + + expect(loadMock).toHaveBeenCalledTimes(2) + expect(loadedObjects).toHaveLength(2) + }) + it('can handle useGraph hook', async () => { const group = new THREE.Group() const mat1 = new THREE.MeshBasicMaterial() From cb6754b40232d4ca2c6d62522730e4c6d50c366a Mon Sep 17 00:00:00 2001 From: Shushovan shakya Date: Fri, 3 Apr 2026 13:43:38 +0200 Subject: [PATCH 3/3] Revert "feat(useLoader): support custom cache keys" This reverts commit da86754c518e321050728695c7388784d8f9a268. --- docs/API/hooks.mdx | 13 -------- packages/fiber/src/core/hooks.tsx | 16 +++------ packages/fiber/tests/hooks.test.tsx | 51 ----------------------------- 3 files changed, 5 insertions(+), 75 deletions(-) diff --git a/docs/API/hooks.mdx b/docs/API/hooks.mdx index 31398a0042..7a4f5dbe6a 100644 --- a/docs/API/hooks.mdx +++ b/docs/API/hooks.mdx @@ -194,12 +194,6 @@ function App() { > [!NOTE] > Assets loaded with useLoader are cached by default. The urls given serve as cache-keys. This allows you to re-use loaded data everywhere in the component tree. -You can also provide an explicit cache key as the fifth argument when the request URL changes (for example signed URLs or auth tokens): - -```jsx -const model = useLoader(GLTFLoader, signedUrl, undefined, undefined, '/model.glb') -``` - > [!WARNING] > Be very careful with mutating or disposing of loaded assets, especially when you plan to re-use them. Refer to the automatic disposal section in the API. @@ -251,13 +245,6 @@ You can pre-load assets in global space so that models can be loaded in anticipa useLoader.preload(GLTFLoader, '/model.glb' /* extensions */) ``` -You can pass the same cache key to `preload` and `clear`: - -```jsx -useLoader.preload(GLTFLoader, signedUrl, undefined, '/model.glb') -useLoader.clear(GLTFLoader, signedUrl, '/model.glb') -``` - ## `useGraph` Convenience hook which creates a memoized, named object/material collection from any [`Object3D`](https://threejs.org/docs/#api/en/core/Object3D). diff --git a/packages/fiber/src/core/hooks.tsx b/packages/fiber/src/core/hooks.tsx index 34ee849403..9053128969 100644 --- a/packages/fiber/src/core/hooks.tsx +++ b/packages/fiber/src/core/hooks.tsx @@ -64,7 +64,6 @@ export function useGraph(object: THREE.Object3D): ObjectMap { type InputLike = string | string[] | string[][] | Readonly type LoaderLike = THREE.Loader type GLTFLike = { scene: THREE.Object3D } -type CacheKeyLike = InputLike type LoaderInstance> = T extends ConstructorRepresentation ? InstanceType : T @@ -140,12 +139,10 @@ export function useLoader, onProgress?: (event: ProgressEvent) => void, - cacheKey?: CacheKeyLike, ) { // Use suspense to load async assets - const inputs = (Array.isArray(input) ? input : [input]) as string[] - const keys = (Array.isArray(cacheKey ?? input) ? (cacheKey ?? input) : [cacheKey ?? input]) as string[] - const results = suspend(() => loadingFn(extensions, onProgress)(loader, ...inputs), [loader, ...keys], { equal: is.equ }) + const keys = (Array.isArray(input) ? input : [input]) as string[] + const results = suspend(loadingFn(extensions, onProgress), [loader, ...keys], { equal: is.equ }) // Return the object(s) return (Array.isArray(input) ? results : results[0]) as I extends any[] ? LoaderResult[] : LoaderResult } @@ -157,11 +154,9 @@ useLoader.preload = function , - cacheKey?: CacheKeyLike, ): void { - const inputs = (Array.isArray(input) ? input : [input]) as string[] - const keys = (Array.isArray(cacheKey ?? input) ? (cacheKey ?? input) : [cacheKey ?? input]) as string[] - return preload(() => loadingFn(extensions)(loader, ...inputs), [loader, ...keys]) + const keys = (Array.isArray(input) ? input : [input]) as string[] + return preload(loadingFn(extensions), [loader, ...keys]) } /** @@ -170,8 +165,7 @@ useLoader.preload = function >( loader: L, input: I, - cacheKey?: CacheKeyLike, ): void { - const keys = (Array.isArray(cacheKey ?? input) ? (cacheKey ?? input) : [cacheKey ?? input]) as string[] + const keys = (Array.isArray(input) ? input : [input]) as string[] return clear([loader, ...keys]) } diff --git a/packages/fiber/tests/hooks.test.tsx b/packages/fiber/tests/hooks.test.tsx index 27d947df5f..d0419e1ce4 100644 --- a/packages/fiber/tests/hooks.test.tsx +++ b/packages/fiber/tests/hooks.test.tsx @@ -194,57 +194,6 @@ describe('hooks', () => { expect(proto).toBeInstanceOf(Loader) }) - it('can use a custom cache key with useLoader', async () => { - const loadedObjects: THREE.Object3D[] = [] - const loadMock = jest.fn((url: string, onLoad: (result: THREE.Object3D) => void) => { - const object = new THREE.Group() - object.name = url - loadedObjects.push(object) - onLoad(object) - }) - - class Loader extends THREE.Loader { - load = loadMock - } - - function Test({ url, cacheKey }: { url: string; cacheKey: string }) { - const object = useLoader(Loader, url, undefined, undefined, cacheKey) - return - } - - await act(async () => root.render()) - await act(async () => root.render()) - - expect(loadMock).toHaveBeenCalledTimes(1) - expect(loadedObjects).toHaveLength(1) - }) - - it('can clear useLoader cache by custom cache key', async () => { - const loadedObjects: THREE.Object3D[] = [] - const loadMock = jest.fn((url: string, onLoad: (result: THREE.Object3D) => void) => { - const object = new THREE.Group() - object.name = url - loadedObjects.push(object) - onLoad(object) - }) - - class Loader extends THREE.Loader { - load = loadMock - } - - function Test({ url, cacheKey }: { url: string; cacheKey: string }) { - const object = useLoader(Loader, url, undefined, undefined, cacheKey) - return - } - - await act(async () => root.render()) - useLoader.clear(Loader, '/model.glb?token=def', 'model-1') - await act(async () => root.render()) - - expect(loadMock).toHaveBeenCalledTimes(2) - expect(loadedObjects).toHaveLength(2) - }) - it('can handle useGraph hook', async () => { const group = new THREE.Group() const mat1 = new THREE.MeshBasicMaterial()