Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v9] fix(useLoader): properly infer Loader types #2723

Merged
merged 12 commits into from
Jun 22, 2023
91 changes: 39 additions & 52 deletions packages/fiber/src/core/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
import * as THREE from 'three'
import * as React from 'react'
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { suspend, preload, clear } from 'suspend-react'
import { context, RootState, RenderCallback, UpdateCallback, StageTypes } from './store'
import { context, RootState, RenderCallback, UpdateCallback, StageTypes, RootStore } from './store'
import { buildGraph, ObjectMap, is, useMutableCallback, useIsomorphicLayoutEffect } from './utils'
import { Stages } from './stages'
import type { Instance } from './reconciler'

export interface Loader<T> extends THREE.Loader {
load(
url: string,
onLoad?: (result: T) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (event: ErrorEvent) => void,
): unknown
loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<T>
}

export type LoaderProto<T> = new (...args: any) => Loader<T extends unknown ? any : T>
export type LoaderReturnType<T, L extends LoaderProto<T>> = T extends unknown
? Awaited<ReturnType<InstanceType<L>['loadAsync']>>
: T
// TODO: this isn't used anywhere, remove in v9
export type LoaderResult<T> = T extends any[] ? Loader<T[number]> : Loader<T>
export type Extensions<T extends { prototype: LoaderProto<any> }> = (loader: T['prototype']) => void
export type ConditionalType<Child, Parent, Truthy, Falsy> = Child extends Parent ? Truthy : Falsy
export type BranchingReturn<T, Parent, Coerced> = ConditionalType<T, Parent, Coerced, T>

/**
* Exposes an object's {@link Instance}.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle
Expand All @@ -42,7 +21,7 @@ export function useInstanceHandle<O>(ref: React.MutableRefObject<O>): React.Muta
return instance
}

export function useStore() {
export function useStore(): RootStore {
const store = React.useContext(context)
if (!store) throw new Error('R3F: Hooks can only be used within the Canvas component!')
return store
Expand All @@ -55,7 +34,7 @@ export function useStore() {
export function useThree<T = RootState>(
selector: (state: RootState) => T = (state) => state as unknown as T,
equalityFn?: <T>(state: T, newState: T) => boolean,
) {
): T {
return useStore()(selector, equalityFn)
}

Expand All @@ -78,7 +57,7 @@ export function useFrame(callback: RenderCallback, renderPriority: number = 0):
* Executes a callback in a given update stage.
* Uses the stage instance to indetify which stage to target in the lifecycle.
*/
export function useUpdate(callback: UpdateCallback, stage: StageTypes = Stages.Update) {
export function useUpdate(callback: UpdateCallback, stage: StageTypes = Stages.Update): void {
const store = useStore()
const stages = store.getState().internal.stages
// Memoize ref
Expand All @@ -93,29 +72,37 @@ export function useUpdate(callback: UpdateCallback, stage: StageTypes = Stages.U
* Returns a node graph of an object with named nodes & materials.
* @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usegraph
*/
export function useGraph(object: THREE.Object3D) {
export function useGraph(object: THREE.Object3D): ObjectMap {
return React.useMemo(() => buildGraph(object), [object])
}

function loadingFn<L extends LoaderProto<any>>(
extensions?: Extensions<L>,
onProgress?: (event: ProgressEvent<EventTarget>) => void,
) {
return function (Proto: L, ...input: string[]) {
export interface Loader<T> extends THREE.Loader {
load(
url: string | string[] | string[][],
onLoad?: (result: T, ...args: any[]) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (event: ErrorEvent) => void,
): unknown
}

export type LoaderProto<T> = new (...args: any[]) => Loader<T>
export type LoaderResult<T> = T extends { scene: THREE.Object3D } ? T & ObjectMap : T
export type Extensions<T> = (loader: Loader<T>) => void

function loadingFn<T>(extensions?: Extensions<T>, onProgress?: (event: ProgressEvent) => void) {
return function (Proto: LoaderProto<T>, ...input: string[]) {
// Construct new loader and run extensions
const loader = new Proto()
if (extensions) extensions(loader)
// Go through the urls and load them
return Promise.all(
input.map(
(input) =>
new Promise((res, reject) =>
new Promise<LoaderResult<T>>((res, reject) =>
loader.load(
input,
(data: any) => {
if (data.scene) Object.assign(data, buildGraph(data.scene))
res(data)
},
(data: any) =>
res(data?.scene instanceof THREE.Object3D ? Object.assign(data, buildGraph(data.scene)) : data),
onProgress,
(error) => reject(new Error(`Could not load ${input}: ${error.message})`)),
),
Expand All @@ -131,37 +118,37 @@ function loadingFn<L extends LoaderProto<any>>(
* Note: this hook's caller must be wrapped with `React.Suspense`
* @see https://docs.pmnd.rs/react-three-fiber/api/hooks#useloader
*/
export function useLoader<T, U extends string | string[], L extends LoaderProto<T>, R = LoaderReturnType<T, L>>(
Proto: L,
export function useLoader<T, U extends string | string[] | string[][]>(
Proto: LoaderProto<T>,
input: U,
extensions?: Extensions<L>,
onProgress?: (event: ProgressEvent<EventTarget>) => void,
): U extends any[] ? BranchingReturn<R, GLTF, GLTF & ObjectMap>[] : BranchingReturn<R, GLTF, GLTF & ObjectMap> {
extensions?: Extensions<T>,
onProgress?: (event: ProgressEvent) => void,
) {
// Use suspense to load async assets
const keys = (Array.isArray(input) ? input : [input]) as string[]
const results = suspend(loadingFn<L>(extensions, onProgress), [Proto, ...keys], { equal: is.equ })
// Return the object/s
return (Array.isArray(input) ? results : results[0]) as U extends any[]
? BranchingReturn<R, GLTF, GLTF & ObjectMap>[]
: BranchingReturn<R, GLTF, GLTF & ObjectMap>
const results = suspend(loadingFn(extensions, onProgress), [Proto, ...keys], { equal: is.equ })
// Return the object(s)
return (Array.isArray(input) ? results : results[0]) as unknown as U extends any[]
? LoaderResult<T>[]
: LoaderResult<T>
}

/**
* Preloads an asset into cache as a side-effect.
*/
useLoader.preload = function <T, U extends string | string[], L extends LoaderProto<T>>(
Proto: L,
useLoader.preload = function <T, U extends string | string[] | string[][]>(
Proto: LoaderProto<T>,
input: U,
extensions?: Extensions<L>,
) {
extensions?: Extensions<T>,
): void {
const keys = (Array.isArray(input) ? input : [input]) as string[]
return preload(loadingFn<L>(extensions), [Proto, ...keys])
return preload(loadingFn(extensions), [Proto, ...keys])
}

/**
* Removes a loaded asset from cache.
*/
useLoader.clear = function <T, U extends string | string[], L extends LoaderProto<T>>(Proto: L, input: U) {
useLoader.clear = function <T, U extends string | string[] | string[][]>(Proto: LoaderProto<T>, input: U): void {
const keys = (Array.isArray(input) ? input : [input]) as string[]
return clear([Proto, ...keys])
}
3 changes: 0 additions & 3 deletions packages/fiber/tests/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ Array [
"Args",
"AttachFnType",
"AttachType",
"BranchingReturn",
"Camera",
"CameraProps",
"Canvas",
"CanvasProps",
"Catalogue",
"Color",
"ComputeFunction",
"ConditionalType",
"ConstructorRepresentation",
"Disposable",
"DomEvent",
Expand Down Expand Up @@ -42,7 +40,6 @@ Array [
"Loader",
"LoaderProto",
"LoaderResult",
"LoaderReturnType",
"MathType",
"ObjectMap",
"Overwrite",
Expand Down
6 changes: 4 additions & 2 deletions packages/fiber/tests/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,14 @@ describe('hooks', () => {

it('can handle useLoader with a loader extension', async () => {
class Loader extends THREE.Loader {
load = (_url: string) => null
load(_url: string, onLoad: (result: null) => void): void {
onLoad(null)
}
}

let proto!: Loader

function Test() {
function Test(): null {
return useLoader(Loader, '', (loader) => (proto = loader))
}
await act(async () => root.render(<Test />))
Expand Down