Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sharing context objects between concurrent renderers #12779

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 @@ -104,7 +104,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 @@ -1048,8 +1052,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
74 changes: 54 additions & 20 deletions packages/react-reconciler/src/ReactFiberNewContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>): any,
getContextChangedBits(context: ReactContext<any>): 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<Fiber | null> = createCursor(null);
Expand All @@ -34,21 +36,38 @@ export default function(stack: Stack) {
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);

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 ' +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm bumping into this with test renderer when the context object is reused, but resetModules() is called between tests. Seems confusing.

'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;
}
}
}

Expand All @@ -61,12 +80,27 @@ 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._currentValue2 = currentValue;
context._changedBits2 = changedBits;
}
}

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

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

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 @@ -181,7 +181,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
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
8 changes: 8 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.
_currentValue2: defaultValue,
_changedBits: 0,
_changedBits2: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
Expand All @@ -50,6 +57,7 @@ export function createContext<T>(

if (__DEV__) {
context._currentRenderer = null;
context._currentRenderer2 = null;
}

return context;
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,13 @@ export type ReactContext<T> = {
_defaultValue: T,

_currentValue: T,
_currentValue2: T,
_changedBits: number,
_changedBits2: number,

// DEV only
_currentRenderer?: Object | null,
_currentRenderer2?: Object | null,
};

export type ReactPortal = {
Expand Down