Skip to content

Commit

Permalink
Support concurrent primary and secondary renderers.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed May 11, 2018
1 parent fc3777b commit a211b39
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 68 deletions.
3 changes: 3 additions & 0 deletions packages/react-art/src/ReactART.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
54 changes: 54 additions & 0 deletions packages/react-art/src/__tests__/ReactART-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<CurrentRendererContext.Consumer>
{currentRenderer => {
ops.push(currentRenderer);
return null;
}}
</CurrentRendererContext.Consumer>
);
}

// Using test renderer instead of the DOM renderer here because async
// testing APIs for the DOM renderer don't exist.
const testRenderer = renderer.create(
<CurrentRendererContext.Provider value="Test">
<Yield value="A" />
<Yield value="B" />
<LogCurrentRenderer />
<Yield value="C" />
</CurrentRendererContext.Provider>,
{
unstable_isAsync: true,
},
);

testRenderer.unstable_flushThrough(['A']);

ReactDOM.render(
<Surface>
<LogCurrentRenderer />
<CurrentRendererContext.Provider value="ART">
<LogCurrentRenderer />
</CurrentRendererContext.Provider>
</Surface>,
container,
);

expect(ops).toEqual([null, 'ART']);

ops = [];
expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']);

expect(ops).toEqual(['Test']);
});
});

describe('ReactARTComponents', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/react-dom/src/__tests__/ReactDOMRoot-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

let React = require('react');
let ReactDOM = require('react-dom');
let ReactART = require('react-art');
let ReactDOMServer = require('react-dom/server');
let AsyncMode = React.unstable_AsyncMode;

Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/client/ReactDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,8 @@ const DOMRenderer = ReactFiberReconciler({

now: ReactScheduler.now,

isPrimaryRenderer: true,

mutation: {
commitMount(
domElement: Instance,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native-renderer/src/ReactFabricRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ const NativeRenderer = ReactFiberReconciler({

now: ReactNativeFrameScheduling.now,

isPrimaryRenderer: true,

prepareForCommit(): void {
// Noop
},
Expand Down
2 changes: 2 additions & 0 deletions packages/react-noop-renderer/src/ReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ let SharedHostConfig = {
now(): number {
return elapsedTimeInMs;
},

isPrimaryRenderer: true,
};

const NoopRenderer = ReactFiberReconciler({
Expand Down
10 changes: 7 additions & 3 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(

const {pushHostContext, pushHostContainer} = hostContext;

const {pushProvider} = newContext;
const {
pushProvider,
getContextCurrentValue,
getContextChangedBits,
} = newContext;

const {
markActualRenderTimeStarted,
Expand Down Expand Up @@ -1006,8 +1010,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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
Expand Down
60 changes: 36 additions & 24 deletions packages/react-reconciler/src/ReactFiberNewContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,37 @@ 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>): any,
getContextChangedBits(context: ReactContext<any>): number,
};

export default function(stack: Stack) {
export default function(stack: Stack, isPrimaryRenderer: boolean) {
const {createCursor, push, pop} = stack;

const providerCursor: StackCursor<Fiber | null> = createCursor(null);
const valueCursor: StackCursor<mixed> = createCursor(null);
const changedBitsCursor: StackCursor<number> = createCursor(0);

let rendererSigil;
if (__DEV__) {
// Use this to detect multiple renderers using the same context
rendererSigil = {};
}

function pushProvider(providerFiber: Fiber): void {
const context: ReactContext<any> = providerFiber.type._context;

push(changedBitsCursor, context._changedBits, providerFiber);
push(valueCursor, context._currentValue, providerFiber);
push(providerCursor, providerFiber, providerFiber);
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;
context._currentValue = providerFiber.pendingProps.value;
context._changedBits = providerFiber.stateNode;
} else {
push(changedBitsCursor, context._changedBits_secondary, providerFiber);
push(valueCursor, context._currentValue_secondary, providerFiber);
push(providerCursor, providerFiber, providerFiber);

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;
context._currentValue_secondary = providerFiber.pendingProps.value;
context._changedBits_secondary = providerFiber.stateNode;
}
}

Expand All @@ -61,12 +54,31 @@ export default function(stack: Stack) {
pop(changedBitsCursor, providerFiber);

const context: ReactContext<any> = providerFiber.type._context;
context._currentValue = currentValue;
context._changedBits = changedBits;
if (isPrimaryRenderer) {
context._currentValue = currentValue;
context._changedBits = changedBits;
} else {
context._currentValue_secondary = currentValue;
context._changedBits_secondary = changedBits;
}
}

function getContextCurrentValue(context: ReactContext<any>): any {
return isPrimaryRenderer
? context._currentValue
: context._currentValue_secondary;
}

function getContextChangedBits(context: ReactContext<any>): number {
return isPrimaryRenderer
? context._changedBits
: context._changedBits_secondary;
}

return {
pushProvider,
popProvider,
getContextCurrentValue,
getContextChangedBits,
};
}
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export type HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL> = {

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<T, P, I, TI, HI, C, CX, PL>,

+mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -724,46 +724,6 @@ describe('ReactNewContext', () => {
}
});

it('warns if multiple renderers concurrently render the same context', () => {
spyOnDev(console, 'error');
const Context = React.createContext(0);

function Foo(props) {
ReactNoop.yield('Foo');
return null;
}

function App(props) {
return (
<Context.Provider value={props.value}>
<Foo />
<Foo />
</Context.Provider>
);
}

ReactNoop.render(<App value={1} />);
// Render past the Provider, but don't commit yet
ReactNoop.flushThrough(['Foo']);

// Get a new copy of ReactNoop
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
React = require('react');
ReactNoop = require('react-noop-renderer');

// Render the provider again using a different renderer
ReactNoop.render(<App value={1} />);
ReactNoop.flush();

if (__DEV__) {
expect(console.error.calls.argsFor(0)[0]).toContain(
'Detected multiple renderers concurrently rendering the same ' +
'context provider. This is currently unsupported',
);
}
});

it('warns if consumer child is not a function', () => {
spyOnDev(console, 'error');
const Context = React.createContext(0);
Expand Down
2 changes: 2 additions & 0 deletions packages/react-test-renderer/src/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/react/src/ReactContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ export function createContext<T>(
_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.
_currentValue_secondary: defaultValue,
_changedBits: 0,
_changedBits_secondary: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ export type ReactContext<T> = {
_defaultValue: T,

_currentValue: T,
_currentValue_secondary: T,
_changedBits: number,
_changedBits_secondary: number,

// DEV only
_currentRenderer?: Object | null,
Expand Down

0 comments on commit a211b39

Please sign in to comment.