Skip to content

Commit 5d1652c

Browse files
committed
Revert "fix(core): portals should be nestable (pmndrs#2746)"
This reverts commit 620f5a7.
1 parent 0806e9f commit 5d1652c

File tree

3 files changed

+108
-38
lines changed

3 files changed

+108
-38
lines changed

packages/fiber/src/core/index.tsx

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ import { ConcurrentRoot } from 'react-reconciler/constants'
44
import create, { UseBoundStore } from 'zustand'
55

66
import * as ReactThreeFiber from '../three-types'
7-
import { Renderer, createStore, isRenderer, context, RootState, Size, Dpr, Performance } from './store'
7+
import {
8+
Renderer,
9+
createStore,
10+
isRenderer,
11+
context,
12+
RootState,
13+
Size,
14+
Dpr,
15+
Performance,
16+
PrivateKeys,
17+
privateKeys,
18+
} from './store'
819
import { createRenderer, extend, prepare, Root } from './renderer'
920
import { createLoop, addEffect, addAfterEffect, addTail, flushGlobalEffects } from './loop'
1021
import { getEventPriority, EventManager, ComputeFunction } from './events'
@@ -19,7 +30,6 @@ import {
1930
updateCamera,
2031
getColorManagement,
2132
hasColorSpace,
22-
useMutableCallback,
2333
} from './utils'
2434
import { useStore } from './hooks'
2535
import type { Properties } from '../three-types'
@@ -423,18 +433,19 @@ function unmountComponentAtNode<TCanvas extends Canvas>(canvas: TCanvas, callbac
423433
}
424434

425435
export type InjectState = Partial<
426-
Omit<RootState, 'events'> & {
436+
Omit<RootState, PrivateKeys> & {
427437
events?: {
428438
enabled?: boolean
429439
priority?: number
430440
compute?: ComputeFunction
431441
connected?: any
432442
}
443+
size?: Size
433444
}
434445
>
435446

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

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

459-
const inject = useMutableCallback((rootState: RootState, injectState: RootState) => {
460-
let viewport
461-
if (injectState.camera && size) {
462-
const camera = injectState.camera
463-
// Calculate the override viewport, if present
464-
viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size)
465-
// Update the portal camera, if it differs from the previous layer
466-
if (camera !== rootState.camera) updateCamera(camera, size)
467-
}
470+
const inject = React.useCallback(
471+
(rootState: RootState, injectState: RootState) => {
472+
const intersect: Partial<RootState> = { ...rootState } // all prev state props
473+
474+
// Only the fields of "rootState" that do not differ from injectState
475+
// Some props should be off-limits
476+
// Otherwise filter out the props that are different and let the inject layer take precedence
477+
Object.keys(rootState).forEach((key) => {
478+
if (
479+
// Some props should be off-limits
480+
privateKeys.includes(key as PrivateKeys) ||
481+
// Otherwise filter out the props that are different and let the inject layer take precedence
482+
// Unless the inject layer props is undefined, then we keep the root layer
483+
(rootState[key as keyof RootState] !== injectState[key as keyof RootState] &&
484+
injectState[key as keyof RootState])
485+
) {
486+
delete intersect[key as keyof RootState]
487+
}
488+
})
489+
490+
let viewport = undefined
491+
if (injectState && size) {
492+
const camera = injectState.camera
493+
// Calculate the override viewport, if present
494+
viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size)
495+
// Update the portal camera, if it differs from the previous layer
496+
if (camera !== rootState.camera) updateCamera(camera, size)
497+
}
468498

469-
return {
470-
// The intersect consists of the previous root state
471-
...rootState,
472-
get: injectState.get,
473-
set: injectState.set,
474-
// Portals have their own scene, which forms the root, a raycaster and a pointer
499+
return {
500+
// The intersect consists of the previous root state
501+
...intersect,
502+
// Portals have their own scene, which forms the root, a raycaster and a pointer
503+
scene: container as THREE.Scene,
504+
raycaster,
505+
pointer,
506+
mouse: pointer,
507+
// Their previous root is the layer before it
508+
previousRoot,
509+
// Events, size and viewport can be overridden by the inject layer
510+
events: { ...rootState.events, ...injectState?.events, ...events },
511+
size: { ...rootState.size, ...size },
512+
viewport: { ...rootState.viewport, ...viewport },
513+
...rest,
514+
} as RootState
515+
},
516+
[state],
517+
)
518+
519+
const [usePortalStore] = React.useState(() => {
520+
// Create a mirrored store, based on the previous root with a few overrides ...
521+
const previousState = previousRoot.getState()
522+
const store = create<RootState>((set, get) => ({
523+
...previousState,
475524
scene: container as THREE.Scene,
476525
raycaster,
477526
pointer,
478527
mouse: pointer,
479-
// Their previous root is the layer before it
480528
previousRoot,
481-
// Events, size and viewport can be overridden by the inject layer
482-
events: { ...rootState.events, ...injectState.events, ...events },
483-
size: { ...rootState.size, ...size },
484-
viewport: { ...rootState.viewport, ...viewport },
529+
events: { ...previousState.events, ...events },
530+
size: { ...previousState.size, ...size },
531+
...rest,
532+
// Set and get refer to this root-state
533+
set,
534+
get,
485535
// Layers are allowed to override events
486536
setEvents: (events: Partial<EventManager<any>>) =>
487-
injectState.set((state) => ({ ...state, events: { ...state.events, ...events } })),
488-
} as RootState
537+
set((state) => ({ ...state, events: { ...state.events, ...events } })),
538+
}))
539+
return store
489540
})
490541

491-
const usePortalStore = React.useMemo(() => {
492-
const store = create((set, get) => ({ ...rest, set, get } as RootState))
493-
542+
React.useEffect(() => {
494543
// Subscribe to previous root-state and copy changes over to the mirrored portal-state
495-
const onMutate = (prev: RootState) => store.setState((state) => inject.current(prev, state))
496-
onMutate(previousRoot.getState())
497-
previousRoot.subscribe(onMutate)
544+
const unsub = previousRoot.subscribe((prev) => usePortalStore.setState((state) => inject(prev, state)))
545+
return () => {
546+
unsub()
547+
usePortalStore.destroy()
548+
}
549+
}, [])
498550

499-
return store
500-
// eslint-disable-next-line react-hooks/exhaustive-deps
501-
}, [previousRoot, container])
502551
React.useEffect(() => {
503-
return () => usePortalStore.destroy()
504-
}, [usePortalStore])
552+
usePortalStore.setState((injectState) => inject(previousRoot.getState(), injectState))
553+
}, [inject])
505554

506555
return (
507556
<>

packages/fiber/src/core/store.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ import { prepare } from './renderer'
55
import { DomEvent, EventManager, PointerCaptureTarget, ThreeEvent } from './events'
66
import { calculateDpr, Camera, isOrthographicCamera, updateCamera } from './utils'
77

8+
// Keys that shouldn't be copied between R3F stores
9+
export const privateKeys = [
10+
'set',
11+
'get',
12+
'setSize',
13+
'setFrameloop',
14+
'setDpr',
15+
'events',
16+
'invalidate',
17+
'advance',
18+
'size',
19+
'viewport',
20+
] as const
21+
22+
export type PrivateKeys = typeof privateKeys[number]
23+
824
export interface Intersection extends THREE.Intersection {
925
eventObject: THREE.Object3D
1026
}

packages/fiber/tests/core/renderer.test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
createPortal,
1414
} from '../../src/index'
1515
import { UseBoundStore } from 'zustand'
16-
import { RootState } from '../../src/core/store'
16+
import { privateKeys, RootState } from '../../src/core/store'
1717
import { Instance } from '../../src/core/renderer'
1818

1919
type ComponentMesh = THREE.Mesh<THREE.BoxBufferGeometry, THREE.MeshBasicMaterial>
@@ -823,6 +823,11 @@ describe('renderer', () => {
823823
// Creates an isolated state enclave
824824
expect(state.scene).not.toBe(scene)
825825
expect(portalState.scene).toBe(scene)
826+
827+
// Preserves internal keys
828+
const overwrittenKeys = ['get', 'set', 'events', 'size', 'viewport']
829+
const respectedKeys = privateKeys.filter((key) => overwrittenKeys.includes(key) || state[key] === portalState[key])
830+
expect(respectedKeys).toStrictEqual(privateKeys)
826831
})
827832

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

0 commit comments

Comments
 (0)