Skip to content

Commit

Permalink
Revert "fix(core): portals should be nestable (#2746)"
Browse files Browse the repository at this point in the history
This reverts commit 620f5a7.
  • Loading branch information
drcmda committed Jun 13, 2023
1 parent 0806e9f commit 5d1652c
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 38 deletions.
123 changes: 86 additions & 37 deletions packages/fiber/src/core/index.tsx
Expand Up @@ -4,7 +4,18 @@ import { ConcurrentRoot } from 'react-reconciler/constants'
import create, { UseBoundStore } from 'zustand'

import * as ReactThreeFiber from '../three-types'
import { Renderer, createStore, isRenderer, context, RootState, Size, Dpr, Performance } from './store'
import {
Renderer,
createStore,
isRenderer,
context,
RootState,
Size,
Dpr,
Performance,
PrivateKeys,
privateKeys,
} from './store'
import { createRenderer, extend, prepare, Root } from './renderer'
import { createLoop, addEffect, addAfterEffect, addTail, flushGlobalEffects } from './loop'
import { getEventPriority, EventManager, ComputeFunction } from './events'
Expand All @@ -19,7 +30,6 @@ import {
updateCamera,
getColorManagement,
hasColorSpace,
useMutableCallback,
} from './utils'
import { useStore } from './hooks'
import type { Properties } from '../three-types'
Expand Down Expand Up @@ -423,18 +433,19 @@ function unmountComponentAtNode<TCanvas extends Canvas>(canvas: TCanvas, callbac
}

export type InjectState = Partial<
Omit<RootState, 'events'> & {
Omit<RootState, PrivateKeys> & {
events?: {
enabled?: boolean
priority?: number
compute?: ComputeFunction
connected?: any
}
size?: Size
}
>

function createPortal(children: React.ReactNode, container: THREE.Object3D, state?: InjectState): JSX.Element {
return <Portal children={children} container={container} state={state} />
return <Portal key={container.uuid} children={children} container={container} state={state} />
}

function Portal({
Expand All @@ -456,52 +467,90 @@ function Portal({
const [raycaster] = React.useState(() => new THREE.Raycaster())
const [pointer] = React.useState(() => new THREE.Vector2())

const inject = useMutableCallback((rootState: RootState, injectState: RootState) => {
let viewport
if (injectState.camera && size) {
const camera = injectState.camera
// Calculate the override viewport, if present
viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size)
// Update the portal camera, if it differs from the previous layer
if (camera !== rootState.camera) updateCamera(camera, size)
}
const inject = React.useCallback(
(rootState: RootState, injectState: RootState) => {
const intersect: Partial<RootState> = { ...rootState } // all prev state props

// Only the fields of "rootState" that do not differ from injectState
// Some props should be off-limits
// Otherwise filter out the props that are different and let the inject layer take precedence
Object.keys(rootState).forEach((key) => {
if (
// Some props should be off-limits
privateKeys.includes(key as PrivateKeys) ||
// Otherwise filter out the props that are different and let the inject layer take precedence
// Unless the inject layer props is undefined, then we keep the root layer
(rootState[key as keyof RootState] !== injectState[key as keyof RootState] &&
injectState[key as keyof RootState])
) {
delete intersect[key as keyof RootState]
}
})

let viewport = undefined
if (injectState && size) {
const camera = injectState.camera
// Calculate the override viewport, if present
viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size)
// Update the portal camera, if it differs from the previous layer
if (camera !== rootState.camera) updateCamera(camera, size)
}

return {
// The intersect consists of the previous root state
...rootState,
get: injectState.get,
set: injectState.set,
// Portals have their own scene, which forms the root, a raycaster and a pointer
return {
// The intersect consists of the previous root state
...intersect,
// Portals have their own scene, which forms the root, a raycaster and a pointer
scene: container as THREE.Scene,
raycaster,
pointer,
mouse: pointer,
// Their previous root is the layer before it
previousRoot,
// Events, size and viewport can be overridden by the inject layer
events: { ...rootState.events, ...injectState?.events, ...events },
size: { ...rootState.size, ...size },
viewport: { ...rootState.viewport, ...viewport },
...rest,
} as RootState
},
[state],
)

const [usePortalStore] = React.useState(() => {
// Create a mirrored store, based on the previous root with a few overrides ...
const previousState = previousRoot.getState()
const store = create<RootState>((set, get) => ({
...previousState,
scene: container as THREE.Scene,
raycaster,
pointer,
mouse: pointer,
// Their previous root is the layer before it
previousRoot,
// Events, size and viewport can be overridden by the inject layer
events: { ...rootState.events, ...injectState.events, ...events },
size: { ...rootState.size, ...size },
viewport: { ...rootState.viewport, ...viewport },
events: { ...previousState.events, ...events },
size: { ...previousState.size, ...size },
...rest,
// Set and get refer to this root-state
set,
get,
// Layers are allowed to override events
setEvents: (events: Partial<EventManager<any>>) =>
injectState.set((state) => ({ ...state, events: { ...state.events, ...events } })),
} as RootState
set((state) => ({ ...state, events: { ...state.events, ...events } })),
}))
return store
})

const usePortalStore = React.useMemo(() => {
const store = create((set, get) => ({ ...rest, set, get } as RootState))

React.useEffect(() => {
// Subscribe to previous root-state and copy changes over to the mirrored portal-state
const onMutate = (prev: RootState) => store.setState((state) => inject.current(prev, state))
onMutate(previousRoot.getState())
previousRoot.subscribe(onMutate)
const unsub = previousRoot.subscribe((prev) => usePortalStore.setState((state) => inject(prev, state)))
return () => {
unsub()
usePortalStore.destroy()
}
}, [])

return store
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [previousRoot, container])
React.useEffect(() => {
return () => usePortalStore.destroy()
}, [usePortalStore])
usePortalStore.setState((injectState) => inject(previousRoot.getState(), injectState))
}, [inject])

return (
<>
Expand Down
16 changes: 16 additions & 0 deletions packages/fiber/src/core/store.ts
Expand Up @@ -5,6 +5,22 @@ import { prepare } from './renderer'
import { DomEvent, EventManager, PointerCaptureTarget, ThreeEvent } from './events'
import { calculateDpr, Camera, isOrthographicCamera, updateCamera } from './utils'

// Keys that shouldn't be copied between R3F stores
export const privateKeys = [
'set',
'get',
'setSize',
'setFrameloop',
'setDpr',
'events',
'invalidate',
'advance',
'size',
'viewport',
] as const

export type PrivateKeys = typeof privateKeys[number]

export interface Intersection extends THREE.Intersection {
eventObject: THREE.Object3D
}
Expand Down
7 changes: 6 additions & 1 deletion packages/fiber/tests/core/renderer.test.tsx
Expand Up @@ -13,7 +13,7 @@ import {
createPortal,
} from '../../src/index'
import { UseBoundStore } from 'zustand'
import { RootState } from '../../src/core/store'
import { privateKeys, RootState } from '../../src/core/store'
import { Instance } from '../../src/core/renderer'

type ComponentMesh = THREE.Mesh<THREE.BoxBufferGeometry, THREE.MeshBasicMaterial>
Expand Down Expand Up @@ -823,6 +823,11 @@ describe('renderer', () => {
// Creates an isolated state enclave
expect(state.scene).not.toBe(scene)
expect(portalState.scene).toBe(scene)

// Preserves internal keys
const overwrittenKeys = ['get', 'set', 'events', 'size', 'viewport']
const respectedKeys = privateKeys.filter((key) => overwrittenKeys.includes(key) || state[key] === portalState[key])
expect(respectedKeys).toStrictEqual(privateKeys)
})

it('can handle createPortal on unmounted container', async () => {
Expand Down

0 comments on commit 5d1652c

Please sign in to comment.