Skip to content

Commit 9e10ee7

Browse files
refactor(types)!: upgrade Zustand to v4 (#2558)
1 parent e7804f9 commit 9e10ee7

File tree

16 files changed

+63
-79
lines changed

16 files changed

+63
-79
lines changed

packages/fiber/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"react-use-measure": "^2.1.1",
5050
"scheduler": "^0.21.0",
5151
"suspend-react": "^0.0.8",
52-
"zustand": "^3.7.1"
52+
"zustand": "^4.1.2"
5353
},
5454
"peerDependencies": {
5555
"expo": ">=46.0",

packages/fiber/src/core/events.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as THREE from 'three'
22
import { ContinuousEventPriority, DiscreteEventPriority, DefaultEventPriority } from 'react-reconciler/constants'
33
import { getRootState } from './utils'
4-
import type { UseBoundStore } from 'zustand'
54
import type { Instance } from './renderer'
6-
import type { RootState } from './store'
5+
import type { RootState, RootStore } from './store'
76

87
export interface Intersection extends THREE.Intersection {
98
/** The event source (the object which registered the handler) */
@@ -147,7 +146,7 @@ function releaseInternalPointerCapture(
147146
}
148147
}
149148

150-
export function removeInteractivity(store: UseBoundStore<RootState>, object: THREE.Object3D) {
149+
export function removeInteractivity(store: RootStore, object: THREE.Object3D) {
151150
const { internal } = store.getState()
152151
// Removes every trace of an object from the data store
153152
internal.interaction = internal.interaction.filter((o) => o !== object)
@@ -163,7 +162,7 @@ export function removeInteractivity(store: UseBoundStore<RootState>, object: THR
163162
})
164163
}
165164

166-
export function createEvents(store: UseBoundStore<RootState>) {
165+
export function createEvents(store: RootStore) {
167166
/** Calculates delta */
168167
function calculateDistance(event: DomEvent) {
169168
const { internal } = store.getState()

packages/fiber/src/core/hooks.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as THREE from 'three'
22
import * as React from 'react'
3-
import { StateSelector, EqualityChecker } from 'zustand'
43
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
54
import { suspend, preload, clear } from 'suspend-react'
65
import { context, RootState, RenderCallback, StageTypes } from './store'
@@ -49,8 +48,8 @@ export function useStore() {
4948
* @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usethree
5049
*/
5150
export function useThree<T = RootState>(
52-
selector: StateSelector<RootState, T> = (state) => state as unknown as T,
53-
equalityFn?: EqualityChecker<T>,
51+
selector: (state: RootState) => T = (state) => state as unknown as T,
52+
equalityFn?: <T>(state: T, newState: T) => boolean,
5453
) {
5554
return useStore()(selector, equalityFn)
5655
}

packages/fiber/src/core/index.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as THREE from 'three'
22
import * as React from 'react'
33
import { ConcurrentRoot } from 'react-reconciler/constants'
4-
import create, { StoreApi, UseBoundStore } from 'zustand'
4+
import create from 'zustand'
55

66
import { ThreeElement } from '../three-types'
77
import {
@@ -18,6 +18,7 @@ import {
1818
Subscription,
1919
FrameloopLegacy,
2020
Frameloop,
21+
RootStore,
2122
} from './store'
2223
import { reconciler, extend, Root } from './renderer'
2324
import { createLoop, addEffect, addAfterEffect, addTail } from './loop'
@@ -97,7 +98,7 @@ export type RenderProps<TCanvas extends Element> = {
9798
manual?: boolean
9899
}
99100
/** An R3F event manager to manage elements' pointer events */
100-
events?: (store: UseBoundStore<RootState>) => EventManager<HTMLElement>
101+
events?: (store: RootStore) => EventManager<HTMLElement>
101102
/** Callback after the canvas has rendered (but not yet committed) */
102103
onCreated?: (state: RootState) => void
103104
/** Response for pointer clicks that have missed any target */
@@ -122,7 +123,7 @@ const createRendererInstance = <TElement extends Element>(gl: GLProps, canvas: T
122123
})
123124
}
124125

125-
const createStages = (stages: Stage[] | undefined, store: UseBoundStore<RootState, StoreApi<RootState>>) => {
126+
const createStages = (stages: Stage[] | undefined, store: RootStore) => {
126127
const state = store.getState()
127128
let subscribers: Subscription[]
128129
let subscription: Subscription
@@ -157,7 +158,7 @@ const createStages = (stages: Stage[] | undefined, store: UseBoundStore<RootStat
157158

158159
export type ReconcilerRoot<TCanvas extends Element> = {
159160
configure: (config?: RenderProps<TCanvas>) => ReconcilerRoot<TCanvas>
160-
render: (element: React.ReactNode) => UseBoundStore<RootState>
161+
render: (element: React.ReactNode) => RootStore
161162
unmount: () => void
162163
}
163164

@@ -366,7 +367,7 @@ function render<TCanvas extends Element>(
366367
children: React.ReactNode,
367368
canvas: TCanvas,
368369
config: RenderProps<TCanvas>,
369-
): UseBoundStore<RootState> {
370+
): RootStore {
370371
console.warn('R3F.render is no longer supported in React 18. Use createRoot instead!')
371372
const root = createRoot(canvas)
372373
root.configure(config)
@@ -380,7 +381,7 @@ function Provider<TElement extends Element>({
380381
rootElement,
381382
}: {
382383
onCreated?: (state: RootState) => void
383-
store: UseBoundStore<RootState>
384+
store: RootStore
384385
children: React.ReactNode
385386
rootElement: TElement
386387
parent?: React.MutableRefObject<TElement | undefined>

packages/fiber/src/core/renderer.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import * as THREE from 'three'
2-
import { UseBoundStore } from 'zustand'
32
import Reconciler from 'react-reconciler'
43
import { unstable_IdlePriority as idlePriority, unstable_scheduleCallback as scheduleCallback } from 'scheduler'
54
import { is, diffProps, applyProps, invalidateInstance, attach, detach, prepare } from './utils'
6-
import { RootState } from './store'
5+
import { RootState, RootStore } from './store'
76
import { removeInteractivity, getEventPriority, EventHandlers } from './events'
87

98
export interface Root {
109
fiber: Reconciler.FiberRoot
11-
store: UseBoundStore<RootState>
10+
store: RootStore
1211
}
1312

1413
export type AttachFnType<O = any> = (parent: any, self: O) => () => void
@@ -31,7 +30,7 @@ export interface InstanceProps<T = any> {
3130
}
3231

3332
export interface Instance<O = any> {
34-
root: UseBoundStore<RootState>
33+
root: RootStore
3534
type: string
3635
parent: Instance | null
3736
children: Instance[]
@@ -47,7 +46,7 @@ export interface Instance<O = any> {
4746
interface HostConfig {
4847
type: string
4948
props: Instance['props']
50-
container: UseBoundStore<RootState>
49+
container: RootStore
5150
instance: Instance
5251
textInstance: void
5352
suspenseInstance: Instance
@@ -63,11 +62,7 @@ interface HostConfig {
6362
const catalogue: Catalogue = {}
6463
const extend = (objects: Partial<Catalogue>): void => void Object.assign(catalogue, objects)
6564

66-
function createInstance(
67-
type: string,
68-
props: HostConfig['props'],
69-
root: UseBoundStore<RootState>,
70-
): HostConfig['instance'] {
65+
function createInstance(type: string, props: HostConfig['props'], root: RootStore): HostConfig['instance'] {
7166
// Get target from catalogue
7267
const name = `${type[0].toUpperCase()}${type.slice(1)}`
7368
const target = catalogue[name]

packages/fiber/src/core/stages.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { MutableRefObject } from 'react'
2-
import { StoreApi, UseBoundStore } from 'zustand'
3-
import { RootState } from './store'
2+
import { RootState, RootStore } from './store'
43

54
export interface UpdateCallback {
65
(state: RootState, delta: number, frame?: XRFrame): void
76
}
87

98
export type UpdateCallbackRef = MutableRefObject<UpdateCallback>
10-
type Store = UseBoundStore<RootState, StoreApi<RootState>>
11-
export type UpdateSubscription = { ref: UpdateCallbackRef; store: Store }
9+
export type UpdateSubscription = { ref: UpdateCallbackRef; store: RootStore }
1210

1311
export type FixedStageOptions = { fixedStep?: number; maxSubsteps?: number }
1412
export type FixedStageProps = { fixedStep: number; maxSubsteps: number; accumulator: number; alpha: number }
@@ -48,7 +46,7 @@ export class Stage {
4846
* @param store - The store to be used with the callback execution.
4947
* @returns A function to remove the subscription.
5048
*/
51-
add(ref: UpdateCallbackRef, store: Store) {
49+
add(ref: UpdateCallbackRef, store: RootStore) {
5250
this.subscribers.push({ ref, store })
5351

5452
return () => {

packages/fiber/src/core/store.ts

+11-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as THREE from 'three'
22
import * as React from 'react'
3-
import create, { GetState, SetState, StoreApi, UseBoundStore } from 'zustand'
3+
import create, { StoreApi, UseBoundStore } from 'zustand'
44
import { DomEvent, EventManager, PointerCaptureTarget, ThreeEvent } from './events'
55
import { calculateDpr, Camera, isOrthographicCamera, prepare, updateCamera } from './utils'
66
import { FixedStage, Stage } from './stages'
@@ -28,7 +28,7 @@ export interface Intersection extends THREE.Intersection {
2828
export type Subscription = {
2929
ref: React.MutableRefObject<RenderCallback>
3030
priority: number
31-
store: UseBoundStore<RootState, StoreApi<RootState>>
31+
store: RootStore
3232
}
3333

3434
export type Dpr = number | [min: number, max: number]
@@ -89,18 +89,14 @@ export type InternalState = {
8989
render: 'auto' | 'manual'
9090
/** The max delta time between two frames. */
9191
maxDelta: number
92-
subscribe: (
93-
callback: React.MutableRefObject<RenderCallback>,
94-
priority: number,
95-
store: UseBoundStore<RootState, StoreApi<RootState>>,
96-
) => () => void
92+
subscribe: (callback: React.MutableRefObject<RenderCallback>, priority: number, store: RootStore) => () => void
9793
}
9894

9995
export type RootState = {
10096
/** Set current state */
101-
set: SetState<RootState>
97+
set: StoreApi<RootState>['setState']
10298
/** Get current state */
103-
get: GetState<RootState>
99+
get: StoreApi<RootState>['getState']
104100
/** The instance of the renderer */
105101
gl: THREE.WebGLRenderer
106102
/** Default camera */
@@ -156,17 +152,19 @@ export type RootState = {
156152
/** When the canvas was clicked but nothing was hit */
157153
onPointerMissed?: (event: MouseEvent) => void
158154
/** If this state model is layerd (via createPortal) then this contains the previous layer */
159-
previousRoot?: UseBoundStore<RootState, StoreApi<RootState>>
155+
previousRoot?: RootStore
160156
/** Internals */
161157
internal: InternalState
162158
}
163159

164-
const context = React.createContext<UseBoundStore<RootState>>(null!)
160+
export type RootStore = UseBoundStore<StoreApi<RootState>>
161+
162+
const context = React.createContext<RootStore>(null!)
165163

166164
const createStore = (
167165
invalidate: (state?: RootState, frames?: number) => void,
168166
advance: (timestamp: number, runGlobalEffects?: boolean, state?: RootState, frame?: XRFrame) => void,
169-
): UseBoundStore<RootState> => {
167+
): RootStore => {
170168
const rootStore = create<RootState>((set, get) => {
171169
const position = new THREE.Vector3()
172170
const defaultTarget = new THREE.Vector3()
@@ -311,11 +309,7 @@ const createStore = (
311309
render: 'auto',
312310
maxDelta: 1 / 10,
313311
priority: 0,
314-
subscribe: (
315-
ref: React.MutableRefObject<RenderCallback>,
316-
priority: number,
317-
store: UseBoundStore<RootState, StoreApi<RootState>>,
318-
) => {
312+
subscribe: (ref: React.MutableRefObject<RenderCallback>, priority: number, store: RootStore) => {
319313
const state = get()
320314
const internal = state.internal
321315
// If this subscription was given a priority, it takes rendering into its own hands

packages/fiber/src/core/utils.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as THREE from 'three'
22
import * as React from 'react'
33
import type { Fiber } from 'react-reconciler'
4-
import type { UseBoundStore } from 'zustand'
54
import type { EventHandlers } from './events'
6-
import type { Dpr, RootState, Size } from './store'
5+
import type { Dpr, RootState, RootStore, Size } from './store'
76
import type { ConstructorRepresentation, Instance } from './renderer'
87

98
export type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera
@@ -154,12 +153,7 @@ export function getInstanceProps<T = any>(queue: Fiber['pendingProps']): Instanc
154153
}
155154

156155
// Each object in the scene carries a small LocalState descriptor
157-
export function prepare<T = any>(
158-
target: T,
159-
root: UseBoundStore<RootState>,
160-
type: string,
161-
props: Instance<T>['props'],
162-
): Instance<T> {
156+
export function prepare<T = any>(target: T, root: RootStore, type: string, props: Instance<T>['props']): Instance<T> {
163157
const object = target as unknown as Instance['object']
164158

165159
// Create instance descriptor

packages/fiber/src/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
RenderCallback,
1818
Performance,
1919
RootState,
20+
RootStore,
2021
} from './core/store'
2122
export type { ThreeEvent, Events, EventManager, ComputeFunction } from './core/events'
2223
export type { ObjectMap, Camera } from './core/utils'

packages/fiber/src/native.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
RenderCallback,
1818
Performance,
1919
RootState,
20+
RootStore,
2021
} from './core/store'
2122
export type { ThreeEvent, Events, EventManager, ComputeFunction } from './core/events'
2223
export type { ObjectMap, Camera } from './core/utils'

packages/fiber/src/native/events.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { UseBoundStore } from 'zustand'
2-
import { RootState } from '../core/store'
1+
import { RootState, RootStore } from '../core/store'
32
import { createEvents, DomEvent, EventManager, Events } from '../core/events'
43
import { GestureResponderEvent } from 'react-native'
54
/* eslint-disable import/default, import/no-named-as-default, import/no-named-as-default-member */
@@ -31,7 +30,7 @@ const DOM_EVENTS = {
3130
}
3231

3332
/** Default R3F event manager for react-native */
34-
export function createTouchEvents(store: UseBoundStore<RootState>): EventManager<HTMLElement> {
33+
export function createTouchEvents(store: RootStore): EventManager<HTMLElement> {
3534
const { handlePointer } = createEvents(store)
3635

3736
const handleTouch = (event: GestureResponderEvent, name: keyof typeof EVENTS) => {

packages/fiber/src/web/events.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { UseBoundStore } from 'zustand'
2-
import { RootState } from '../core/store'
1+
import { RootState, RootStore } from '../core/store'
32
import { EventManager, Events, createEvents, DomEvent } from '../core/events'
43

54
const DOM_EVENTS = {
@@ -16,7 +15,7 @@ const DOM_EVENTS = {
1615
} as const
1716

1817
/** Default R3F event manager for web */
19-
export function createPointerEvents(store: UseBoundStore<RootState>): EventManager<HTMLElement> {
18+
export function createPointerEvents(store: RootStore): EventManager<HTMLElement> {
2019
const { handlePointer } = createEvents(store)
2120

2221
return {

packages/fiber/tests/index.test.tsx

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react'
22
import * as THREE from 'three'
3-
import { ReconcilerRoot, createRoot, act, useFrame, useThree, createPortal, RootState } from '../src/index'
4-
import { UseBoundStore } from 'zustand'
3+
import { ReconcilerRoot, createRoot, act, useFrame, useThree, createPortal, RootState, RootStore } from '../src/index'
54
import { privateKeys } from '../src/core/store'
65

76
let root: ReconcilerRoot<HTMLCanvasElement> = null!
@@ -26,35 +25,35 @@ describe('createRoot', () => {
2625
})
2726

2827
it('should handle an performance changing functions', async () => {
29-
let state: UseBoundStore<RootState> = null!
28+
let store: RootStore = null!
3029
await act(async () => {
31-
state = root.configure({ dpr: [1, 2], performance: { min: 0.2 } }).render(<group />)
30+
store = root.configure({ dpr: [1, 2], performance: { min: 0.2 } }).render(<group />)
3231
})
3332

34-
expect(state.getState().viewport.initialDpr).toEqual(window.devicePixelRatio)
35-
expect(state.getState().performance.min).toEqual(0.2)
36-
expect(state.getState().performance.current).toEqual(1)
33+
expect(store.getState().viewport.initialDpr).toEqual(window.devicePixelRatio)
34+
expect(store.getState().performance.min).toEqual(0.2)
35+
expect(store.getState().performance.current).toEqual(1)
3736

3837
await act(async () => {
39-
state.getState().setDpr(0.1)
38+
store.getState().setDpr(0.1)
4039
})
4140

42-
expect(state.getState().viewport.dpr).toEqual(0.1)
41+
expect(store.getState().viewport.dpr).toEqual(0.1)
4342

4443
jest.useFakeTimers()
4544

4645
await act(async () => {
47-
state.getState().performance.regress()
46+
store.getState().performance.regress()
4847
jest.advanceTimersByTime(100)
4948
})
5049

51-
expect(state.getState().performance.current).toEqual(0.2)
50+
expect(store.getState().performance.current).toEqual(0.2)
5251

5352
await act(async () => {
5453
jest.advanceTimersByTime(200)
5554
})
5655

57-
expect(state.getState().performance.current).toEqual(1)
56+
expect(store.getState().performance.current).toEqual(1)
5857

5958
jest.useRealTimers()
6059
})

0 commit comments

Comments
 (0)