diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index 04478434e8b..a7f9a3064ce 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -478,6 +478,9 @@ const ARTRenderer = ReactFiberReconciler({ now: ReactScheduler.now, + // The ART renderer is secondary to the React DOM renderer. + isPrimaryRenderer: false, + mutation: { appendChild(parentInstance, child) { if (child.parentNode === parentInstance) { diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js index fbce04a9a2d..ace737e5260 100644 --- a/packages/react-art/src/__tests__/ReactART-test.js +++ b/packages/react-art/src/__tests__/ReactART-test.js @@ -339,6 +339,60 @@ describe('ReactART', () => { doClick(instance); expect(onClick2).toBeCalled(); }); + + it('can concurrently render with a "primary" renderer while sharing context', () => { + const CurrentRendererContext = React.createContext(null); + + function Yield(props) { + testRenderer.unstable_yield(props.value); + return null; + } + + let ops = []; + function LogCurrentRenderer() { + return ( + + {currentRenderer => { + ops.push(currentRenderer); + return null; + }} + + ); + } + + // Using test renderer instead of the DOM renderer here because async + // testing APIs for the DOM renderer don't exist. + const testRenderer = renderer.create( + + + + + + , + { + unstable_isAsync: true, + }, + ); + + testRenderer.unstable_flushThrough(['A']); + + ReactDOM.render( + + + + + + , + container, + ); + + expect(ops).toEqual([null, 'ART']); + + ops = []; + expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']); + + expect(ops).toEqual(['Test']); + }); }); describe('ReactARTComponents', () => { diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 82032f1d1d2..81683c47748 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -690,6 +690,8 @@ const DOMRenderer = ReactFiberReconciler({ now: ReactScheduler.now, + isPrimaryRenderer: true, + mutation: { commitMount( domElement: Instance, diff --git a/packages/react-native-renderer/src/ReactFabricRenderer.js b/packages/react-native-renderer/src/ReactFabricRenderer.js index 0ec040bddf0..fa2fabef644 100644 --- a/packages/react-native-renderer/src/ReactFabricRenderer.js +++ b/packages/react-native-renderer/src/ReactFabricRenderer.js @@ -217,6 +217,9 @@ const ReactFabricRenderer = ReactFiberReconciler({ now: ReactNativeFrameScheduling.now, + // The Fabric renderer is secondary to the existing React Native renderer. + isPrimaryRenderer: false, + prepareForCommit(): void { // Noop }, diff --git a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js b/packages/react-native-renderer/src/ReactNativeFiberRenderer.js index 7de768b3b29..2854f6488fd 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeFiberRenderer.js @@ -169,6 +169,8 @@ const NativeRenderer = ReactFiberReconciler({ now: ReactNativeFrameScheduling.now, + isPrimaryRenderer: true, + prepareForCommit(): void { // Noop }, diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 5cd6df0fbd2..e9c82cbe9c6 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -185,6 +185,8 @@ let SharedHostConfig = { now(): number { return elapsedTimeInMs; }, + + isPrimaryRenderer: true, }; const NoopRenderer = ReactFiberReconciler({ diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 489393bebbc..14927b2db98 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -104,7 +104,11 @@ export default function( const {pushHostContext, pushHostContainer} = hostContext; - const {pushProvider} = newContext; + const { + pushProvider, + getContextCurrentValue, + getContextChangedBits, + } = newContext; const { markActualRenderTimeStarted, @@ -1048,8 +1052,8 @@ export default function( const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - const newValue = context._currentValue; - const changedBits = context._changedBits; + const newValue = getContextCurrentValue(context); + const changedBits = getContextChangedBits(context); if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index ab9c27e88aa..3aaf553ec55 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -11,14 +11,16 @@ import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; import type {StackCursor, Stack} from './ReactFiberStack'; -import warning from 'fbjs/lib/warning'; - export type NewContext = { pushProvider(providerFiber: Fiber): void, popProvider(providerFiber: Fiber): void, + getContextCurrentValue(context: ReactContext): any, + getContextChangedBits(context: ReactContext): number, }; -export default function(stack: Stack) { +import warning from 'fbjs/lib/warning'; + +export default function(stack: Stack, isPrimaryRenderer: boolean) { const {createCursor, push, pop} = stack; const providerCursor: StackCursor = createCursor(null); @@ -34,21 +36,38 @@ export default function(stack: Stack) { function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type._context; - push(changedBitsCursor, context._changedBits, providerFiber); - push(valueCursor, context._currentValue, providerFiber); - push(providerCursor, providerFiber, providerFiber); - - context._currentValue = providerFiber.pendingProps.value; - context._changedBits = providerFiber.stateNode; - - if (__DEV__) { - warning( - context._currentRenderer === null || - context._currentRenderer === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer = rendererSigil; + if (isPrimaryRenderer) { + push(changedBitsCursor, context._changedBits, providerFiber); + push(valueCursor, context._currentValue, providerFiber); + push(providerCursor, providerFiber, providerFiber); + + context._currentValue = providerFiber.pendingProps.value; + context._changedBits = providerFiber.stateNode; + if (__DEV__) { + warning( + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer = rendererSigil; + } + } else { + push(changedBitsCursor, context._changedBits2, providerFiber); + push(valueCursor, context._currentValue2, providerFiber); + push(providerCursor, providerFiber, providerFiber); + + context._currentValue2 = providerFiber.pendingProps.value; + context._changedBits2 = providerFiber.stateNode; + if (__DEV__) { + warning( + context._currentRenderer2 === null || + context._currentRenderer2 === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer2 = rendererSigil; + } } } @@ -61,12 +80,27 @@ export default function(stack: Stack) { pop(changedBitsCursor, providerFiber); const context: ReactContext = providerFiber.type._context; - context._currentValue = currentValue; - context._changedBits = changedBits; + if (isPrimaryRenderer) { + context._currentValue = currentValue; + context._changedBits = changedBits; + } else { + context._currentValue2 = currentValue; + context._changedBits2 = changedBits; + } + } + + function getContextCurrentValue(context: ReactContext): any { + return isPrimaryRenderer ? context._currentValue : context._currentValue2; + } + + function getContextChangedBits(context: ReactContext): number { + return isPrimaryRenderer ? context._changedBits : context._changedBits2; } return { pushProvider, popProvider, + getContextCurrentValue, + getContextChangedBits, }; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index dd395af7530..52a42646ba7 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -95,6 +95,11 @@ export type HostConfig = { now(): number, + // Temporary workaround for scenario where multiple renderers concurrently + // render using the same context objects. E.g. React DOM and React ART on the + // same page. DOM is the primary renderer; ART is the secondary renderer. + isPrimaryRenderer: boolean, + +hydration?: HydrationHostConfig, +mutation?: MutableUpdatesHostConfig, diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 5c9657876c5..562039393da 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -181,7 +181,7 @@ export default function( const stack = ReactFiberStack(); const hostContext = ReactFiberHostContext(config, stack); const legacyContext = ReactFiberLegacyContext(stack); - const newContext = ReactFiberNewContext(stack); + const newContext = ReactFiberNewContext(stack, config.isPrimaryRenderer); const profilerTimer = createProfilerTimer(now); const {popHostContext, popHostContainer} = hostContext; const { diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 561aed6c584..7fa54534d31 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -226,6 +226,8 @@ const TestRenderer = ReactFiberReconciler({ // Even after the reconciler has initialized and read host config values. now: () => nowImplementation(), + isPrimaryRenderer: true, + mutation: { commitUpdate( instance: Instance, diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 10190accc4e..d15163e8134 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -36,7 +36,14 @@ export function createContext( _calculateChangedBits: calculateChangedBits, _defaultValue: defaultValue, _currentValue: defaultValue, + // As a workaround to support multiple concurrent renderers, we categorize + // some renderers as primary and others as secondary. We only expect + // there to be two concurrent renderers at most: React Native (primary) and + // Fabric (secondary); React DOM (primary) and React ART (secondary). + // Secondary renderers store their context values on separate fields. + _currentValue2: defaultValue, _changedBits: 0, + _changedBits2: 0, // These are circular Provider: (null: any), Consumer: (null: any), @@ -50,6 +57,7 @@ export function createContext( if (__DEV__) { context._currentRenderer = null; + context._currentRenderer2 = null; } return context; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 689ed18bf95..c47a380a921 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -85,10 +85,13 @@ export type ReactContext = { _defaultValue: T, _currentValue: T, + _currentValue2: T, _changedBits: number, + _changedBits2: number, // DEV only _currentRenderer?: Object | null, + _currentRenderer2?: Object | null, }; export type ReactPortal = {