Skip to content

Commit

Permalink
refactor: pull context bridge from its-fine (#2512)
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett authored Sep 21, 2022
1 parent 4dea346 commit 6c9b845
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 200 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-native": "0.67.4",
"react-nil": "^1.2.0",
"react-test-renderer": "^18.0.0",
"regenerator-runtime": "^0.13.9",
"three": "^0.139.0",
Expand Down
1 change: 1 addition & 0 deletions packages/fiber/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.26.7",
"its-fine": "^1.0.0",
"react-reconciler": "^0.27.0",
"react-use-measure": "^2.1.1",
"scheduler": "^0.21.0",
Expand Down
83 changes: 0 additions & 83 deletions packages/fiber/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { UseBoundStore } from 'zustand'
import { EventHandlers } from './events'
import { AttachType, Instance, InstanceProps, LocalState } from './renderer'
import { Dpr, RootState, Size } from './store'
import type { Fiber } from 'react-reconciler'

export type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera
export const isOrthographicCamera = (def: Camera): def is THREE.OrthographicCamera =>
Expand All @@ -31,88 +30,6 @@ export function useMutableCallback<T>(fn: T) {
return ref
}

/**
* Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
*/
function traverseFiber(fiber: Fiber, ascending: boolean, selector: (node: Fiber) => boolean | void): Fiber | undefined {
if (selector(fiber) === true) return fiber

let child = ascending ? fiber.return : fiber.child
while (child) {
const match = traverseFiber(child, ascending, selector)
if (match) return match

child = child.sibling
}
}

// Active contexts
const contexts: React.Context<any>[] = []

/**
* Represents a react-context bridge provider component.
*/
export type ContextBridge = React.FC<{ children?: React.ReactNode }>

/**
* React Context currently cannot be shared across [React renderers](https://reactjs.org/docs/codebase-overview.html#renderers) but explicitly forwarded between providers (see [react#17275](https://github.com/facebook/react/issues/17275)). This hook returns a {@link ContextBridge} of live context providers to pierce Context across renderers.
*
* Pass {@link ContextBridge} as a component to a secondary renderer to enable context-sharing within its children.
*/
export function useContextBridge(fiber?: Fiber): ContextBridge {
if (fiber) {
traverseFiber(fiber, true, (node) => {
const context = node.type?._context
if (!context || contexts.includes(context)) return

// In development, React will warn about using contexts between renderers because
// of the above issue. We'll hide the warning because this hook works as expected
// https://github.com/facebook/react/pull/12779
Object.defineProperties(context, {
_currentRenderer: {
get() {
return null
},
set() {},
},
_currentRenderer2: {
get() {
return null
},
set() {},
},
})

contexts.push(context)
})
}

return contexts.reduce(
(Prev, context) => {
const value = (
React as any
).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current?.readContext(context)
return (props) => React.createElement(Prev, null, React.createElement(context.Provider, { ...props, value }))
},
(props) => React.createElement(React.Fragment, props),
)
}

/**
* Exposes the current react-internal {@link Fiber}.
*/
export class FiberProvider extends React.Component<{ setFiber: React.Dispatch<Fiber>; children?: React.ReactNode }> {
private _reactInternals!: Fiber

componentDidMount() {
this.props.setFiber(this._reactInternals)
}

render() {
return this.props.children
}
}

export type SetBlock = false | Promise<null> | null
export type UnblockProps = { set: React.Dispatch<React.SetStateAction<SetBlock>>; children: React.ReactNode }

Expand Down
28 changes: 19 additions & 9 deletions packages/fiber/src/native/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as React from 'react'
import * as THREE from 'three'
import { View, ViewProps, ViewStyle, LayoutChangeEvent, StyleSheet, PixelRatio } from 'react-native'
import { ExpoWebGLRenderingContext, GLView } from 'expo-gl'
import { SetBlock, Block, ErrorBoundary, useMutableCallback, useContextBridge, FiberProvider } from '../core/utils'
import { useContextBridge, FiberProvider } from 'its-fine'
import { SetBlock, Block, ErrorBoundary, useMutableCallback } from '../core/utils'
import { extend, createRoot, unmountComponentAtNode, RenderProps, ReconcilerRoot } from '../core'
import { createTouchEvents } from './events'
import { RootState, Size } from '../core/store'
Expand All @@ -17,7 +18,7 @@ export interface Props extends Omit<RenderProps<HTMLCanvasElement>, 'size' | 'dp
* A native canvas which accepts threejs elements as children.
* @see https://docs.pmnd.rs/react-three-fiber/api/canvas
*/
export const Canvas = /*#__PURE__*/ React.forwardRef<View, Props>(
const CanvasImpl = /*#__PURE__*/ React.forwardRef<View, Props>(
(
{
children,
Expand All @@ -44,8 +45,7 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<View, Props>(
// their own elements by using the createRoot API instead
React.useMemo(() => extend(THREE), [])

const [fiber, setFiber] = React.useState<any>(null)
const Bridge = useContextBridge(fiber)
const Bridge = useContextBridge()

const [{ width, height, top, left }, setSize] = React.useState<Size>({ width: 0, height: 0, top: 0, left: 0 })
const [canvas, setCanvas] = React.useState<HTMLCanvasElement | null>(null)
Expand Down Expand Up @@ -141,11 +141,21 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<View, Props>(
}, [canvas])

return (
<FiberProvider setFiber={setFiber}>
<View {...props} ref={viewRef} onLayout={onLayout} style={{ flex: 1, ...style }} {...bind}>
{width > 0 && <GLView onContextCreate={onContextCreate} style={StyleSheet.absoluteFill} />}
</View>
</FiberProvider>
<View {...props} ref={viewRef} onLayout={onLayout} style={{ flex: 1, ...style }} {...bind}>
{width > 0 && <GLView onContextCreate={onContextCreate} style={StyleSheet.absoluteFill} />}
</View>
)
},
)

/**
* A native canvas which accepts threejs elements as children.
* @see https://docs.pmnd.rs/react-three-fiber/api/canvas
*/
export const Canvas = React.forwardRef<View, Props>(function CanvasWrapper(props, ref) {
return (
<FiberProvider>
<CanvasImpl {...props} ref={ref} />
</FiberProvider>
)
})
51 changes: 24 additions & 27 deletions packages/fiber/src/web/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@ import * as React from 'react'
import * as THREE from 'three'
import useMeasure from 'react-use-measure'
import type { Options as ResizeOptions } from 'react-use-measure'
import {
isRef,
SetBlock,
Block,
ErrorBoundary,
useMutableCallback,
useIsomorphicLayoutEffect,
useContextBridge,
FiberProvider,
} from '../core/utils'
import { useContextBridge, FiberProvider } from 'its-fine'
import { isRef, SetBlock, Block, ErrorBoundary, useMutableCallback, useIsomorphicLayoutEffect } from '../core/utils'
import { ReconcilerRoot, extend, createRoot, unmountComponentAtNode, RenderProps } from '../core'
import { createPointerEvents } from './events'
import { DomEvent } from '../core/events'
Expand All @@ -31,11 +23,7 @@ export interface Props extends Omit<RenderProps<HTMLCanvasElement>, 'size'>, Rea
eventPrefix?: 'offset' | 'client' | 'page' | 'layer' | 'screen'
}

/**
* A DOM canvas which accepts threejs elements as children.
* @see https://docs.pmnd.rs/react-three-fiber/api/canvas
*/
export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(function Canvas(
const CanvasImpl = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(function Canvas(
{
children,
fallback,
Expand Down Expand Up @@ -66,8 +54,7 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
// their own elements by using the createRoot API instead
React.useMemo(() => extend(THREE), [])

const [fiber, setFiber] = React.useState<any>(null)
const Bridge = useContextBridge(fiber)
const Bridge = useContextBridge()

const [containerRef, containerRect] = useMeasure({ scroll: true, debounce: { scroll: 50, resize: 0 }, ...resize })
const canvasRef = React.useRef<HTMLCanvasElement>(null!)
Expand Down Expand Up @@ -144,17 +131,27 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
const pointerEvents = eventSource ? 'none' : 'auto'

return (
<FiberProvider setFiber={setFiber}>
<div
ref={divRef}
style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden', pointerEvents, ...style }}
{...props}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }}>
<canvas ref={canvasRef} style={{ display: 'block' }}>
{fallback}
</canvas>
</div>
<div
ref={divRef}
style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden', pointerEvents, ...style }}
{...props}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }}>
<canvas ref={canvasRef} style={{ display: 'block' }}>
{fallback}
</canvas>
</div>
</div>
)
})

/**
* A DOM canvas which accepts threejs elements as children.
* @see https://docs.pmnd.rs/react-three-fiber/api/canvas
*/
export const Canvas = React.forwardRef<HTMLCanvasElement, Props>(function CanvasWrapper(props, ref) {
return (
<FiberProvider>
<CanvasImpl {...props} ref={ref} />
</FiberProvider>
)
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import * as React from 'react'
import { act, render } from 'react-nil'
import { create } from 'react-test-renderer'
import { is, useContextBridge, FiberProvider } from '../../src/core/utils'
import { is } from '../../src/core/utils'

describe('is', () => {
const myFunc = () => null
Expand Down Expand Up @@ -100,71 +97,3 @@ describe('is', () => {
expect(is.equ([1, 2], [1, 2, 3], { strict: false })).toBe(true)
})
})

describe('useContextBridge', () => {
it('forwards live context between renderers', async () => {
const Context1 = React.createContext<string>(null!)
const Context2 = React.createContext<string>(null!)

const values: string[] = []

function Test() {
values.push(React.useContext(Context1), React.useContext(Context2))

return null
}

const Canvas = React.memo(() => {
const [fiber, setFiber] = React.useState<any>(null)
const Bridge = useContextBridge(fiber)
render(
<Bridge>
<Test />
</Bridge>,
)

return (
<FiberProvider setFiber={setFiber}>
<Context2.Provider value="invalid" />
</FiberProvider>
)
})

function Providers(props: { values: [value1: string, value2?: string]; children: React.ReactNode }) {
const [value1, value2] = props.values
return (
<Context1.Provider value="invalid">
<Context1.Provider value={value1}>
{value2 ? <Context2.Provider value={value2}>{props.children}</Context2.Provider> : props.children}
</Context1.Provider>
</Context1.Provider>
)
}

await act(async () =>
create(
<Providers values={['value1', 'value2']}>
<Canvas />
</Providers>,
),
)

await act(async () =>
create(
<Providers values={['value1__new', 'value2__new']}>
<Canvas />
</Providers>,
),
)

await act(async () =>
create(
<Providers values={['value1__new2']}>
<Canvas />
</Providers>,
),
)

expect(values).toStrictEqual(['value1', 'value2', 'value1__new', 'value2__new', 'value1__new2', null])
})
})
22 changes: 14 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2207,6 +2207,13 @@
dependencies:
"@types/react" "*"

"@types/react-reconciler@^0.28.0":
version "0.28.0"
resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.0.tgz#513acbed173140e958c909041ca14eb40412077f"
integrity sha512-5cjk9ottZAj7eaTsqzPUIlrVbh3hBAO2YaEL1rkjHKB3xNAId7oU8GhzvAX+gfmlfoxTwJnBjPxEHyxkEA1Ffg==
dependencies:
"@types/react" "*"

"@types/react-test-renderer@^17.0.1":
version "17.0.2"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf"
Expand Down Expand Up @@ -5359,6 +5366,13 @@ istanbul-reports@^3.1.3:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"

its-fine@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-1.0.0.tgz#e1c17f4420a433c9e96d264330e96a82c6edc33b"
integrity sha512-EEVcyr+sR21lxLZg3U84HpY3Mb9aKmGYqxPsIbf/Ea4fO4qpvE/7lX5qtrkuCnljJ9RLMaSaeMq35PeLa+oM0w==
dependencies:
"@types/react-reconciler" "^0.28.0"

jest-changed-files@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5"
Expand Down Expand Up @@ -7586,14 +7600,6 @@ react-native@0.67.4:
whatwg-fetch "^3.0.0"
ws "^6.1.4"

react-nil@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-nil/-/react-nil-1.2.0.tgz#0f2e110b50cf12bdf1eebf43bfdb4b348a271c11"
integrity sha512-54yJ8+vFyJlZHiFBLny0RKQ0/FSG32y8pZt0oV7duxaW3lEM9pc6D2Hhnvu0TToJKPCAJ/A5XmjY8nDETxrrGg==
dependencies:
"@types/react-reconciler" "^0.26.7"
react-reconciler "^0.27.0"

react-reconciler@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.27.0.tgz#360124fdf2d76447c7491ee5f0e04503ed9acf5b"
Expand Down

0 comments on commit 6c9b845

Please sign in to comment.