Support sharing context objects between concurrent renderers (#12779)
* Support concurrent primary and secondary renderers. 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. * Add back concurrent renderer warning Only warn for two concurrent primary or two concurrent secondary renderers. * Change "_secondary" suffix to "2" #EveryBitCounts
Andrew Clark committed
May 10, 2018 at 18:34 UTC
b0726e99476ea67c7558cbf268685998a38ade7c
13 files changed
+146
-24
packages/react-art/src/ReactART.js
+3
@@ -478,6 +478,9 @@ const ARTRenderer = ReactFiberReconciler({
478
479
now: ReactScheduler.now,
480
481
+ // The ART renderer is secondary to the React DOM renderer.
482
+ isPrimaryRenderer: false,
483
+
484
mutation: {
485
appendChild(parentInstance, child) {
486
if (child.parentNode === parentInstance) {
packages/react-art/src/__tests__/ReactART-test.js
+54
@@ -339,6 +339,60 @@ describe('ReactART', () => {
339
doClick(instance);
340
expect(onClick2).toBeCalled();
341
});
342
+
343
+ it('can concurrently render with a "primary" renderer while sharing context', () => {
344
+ const CurrentRendererContext = React.createContext(null);
345
+
346
+ function Yield(props) {
347
+ testRenderer.unstable_yield(props.value);
348
+ return null;
349
+ }
350
+
351
+ let ops = [];
352
+ function LogCurrentRenderer() {
353
+ return (
354
+ <CurrentRendererContext.Consumer>
355
+ {currentRenderer => {
356
+ ops.push(currentRenderer);
357
+ return null;
358
+ }}
359
+ </CurrentRendererContext.Consumer>
360
+ );
361
+ }
362
+
363
+ // Using test renderer instead of the DOM renderer here because async
364
+ // testing APIs for the DOM renderer don't exist.
365
+ const testRenderer = renderer.create(
366
+ <CurrentRendererContext.Provider value="Test">
367
+ <Yield value="A" />
368
+ <Yield value="B" />
369
+ <LogCurrentRenderer />
370
+ <Yield value="C" />
371
+ </CurrentRendererContext.Provider>,
372
+ {
373
+ unstable_isAsync: true,
374
+ },
375
+ );
376
+
377
+ testRenderer.unstable_flushThrough(['A']);
378
+
379
+ ReactDOM.render(
380
+ <Surface>
381
+ <LogCurrentRenderer />
382
+ <CurrentRendererContext.Provider value="ART">
383
+ <LogCurrentRenderer />
384
+ </CurrentRendererContext.Provider>
385
+ </Surface>,
386
+ container,
387
+ );
388
+
389
+ expect(ops).toEqual([null, 'ART']);
390
+
391
+ ops = [];
392
+ expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']);
393
+
394
+ expect(ops).toEqual(['Test']);
395
+ });
396
});
397
398
describe('ReactARTComponents', () => {
packages/react-dom/src/client/ReactDOM.js
+2
@@ -690,6 +690,8 @@ const DOMRenderer = ReactFiberReconciler({
690
691
now: ReactScheduler.now,
692
693
+ isPrimaryRenderer: true,
694
+
695
mutation: {
696
commitMount(
697
domElement: Instance,
packages/react-native-renderer/src/ReactFabricRenderer.js
+3
@@ -217,6 +217,9 @@ const ReactFabricRenderer = ReactFiberReconciler({
217
218
now: ReactNativeFrameScheduling.now,
219
220
+ // The Fabric renderer is secondary to the existing React Native renderer.
221
+ isPrimaryRenderer: false,
222
+
223
prepareForCommit(): void {
224
// Noop
225
},
packages/react-native-renderer/src/ReactNativeFiberRenderer.js
+2
@@ -169,6 +169,8 @@ const NativeRenderer = ReactFiberReconciler({
169
170
now: ReactNativeFrameScheduling.now,
171
172
+ isPrimaryRenderer: true,
173
+
174
prepareForCommit(): void {
175
// Noop
176
},
packages/react-noop-renderer/src/ReactNoop.js
+2
@@ -185,6 +185,8 @@ let SharedHostConfig = {
185
now(): number {
186
return elapsedTimeInMs;
187
},
188
+
189
+ isPrimaryRenderer: true,
190
};
191
192
const NoopRenderer = ReactFiberReconciler({
packages/react-reconciler/src/ReactFiberBeginWork.js
+7
-3
@@ -104,7 +104,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
104
105
const {pushHostContext, pushHostContainer} = hostContext;
106
107
- const {pushProvider} = newContext;
107
+ const {
108
+ pushProvider,
109
+ getContextCurrentValue,
110
+ getContextChangedBits,
111
+ } = newContext;
112
113
const {
114
markActualRenderTimeStarted,
@@ -1048,8 +1052,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1052
const newProps = workInProgress.pendingProps;
1053
const oldProps = workInProgress.memoizedProps;
1054
1051
- const newValue = context._currentValue;
1052
- const changedBits = context._changedBits;
1055
+ const newValue = getContextCurrentValue(context);
1056
+ const changedBits = getContextChangedBits(context);
1057
1058
if (hasLegacyContextChanged()) {
1059
// Normally we can bail out on props equality but if context has changed
packages/react-reconciler/src/ReactFiberNewContext.js
+54
-20
@@ -11,14 +11,16 @@ import type {Fiber} from './ReactFiber';
11
import type {ReactContext} from 'shared/ReactTypes';
12
import type {StackCursor, Stack} from './ReactFiberStack';
13
14
-import warning from 'fbjs/lib/warning';
15
-
14
export type NewContext = {
15
pushProvider(providerFiber: Fiber): void,
16
popProvider(providerFiber: Fiber): void,
17
+ getContextCurrentValue(context: ReactContext<any>): any,
18
+ getContextChangedBits(context: ReactContext<any>): number,
19
};
20
21
-export default function(stack: Stack) {
21
+import warning from 'fbjs/lib/warning';
22
+
23
+export default function(stack: Stack, isPrimaryRenderer: boolean) {
24
const {createCursor, push, pop} = stack;
25
26
const providerCursor: StackCursor<Fiber | null> = createCursor(null);
@@ -34,21 +36,38 @@ export default function(stack: Stack) {
36
function pushProvider(providerFiber: Fiber): void {
37
const context: ReactContext<any> = providerFiber.type._context;
38
37
- push(changedBitsCursor, context._changedBits, providerFiber);
38
- push(valueCursor, context._currentValue, providerFiber);
39
- push(providerCursor, providerFiber, providerFiber);
40
-
41
- context._currentValue = providerFiber.pendingProps.value;
42
- context._changedBits = providerFiber.stateNode;
43
-
44
- if (__DEV__) {
45
- warning(
46
- context._currentRenderer === null ||
47
- context._currentRenderer === rendererSigil,
48
- 'Detected multiple renderers concurrently rendering the ' +
49
- 'same context provider. This is currently unsupported.',
50
- );
51
- context._currentRenderer = rendererSigil;
39
+ if (isPrimaryRenderer) {
40
+ push(changedBitsCursor, context._changedBits, providerFiber);
41
+ push(valueCursor, context._currentValue, providerFiber);
42
+ push(providerCursor, providerFiber, providerFiber);
43
+
44
+ context._currentValue = providerFiber.pendingProps.value;
45
+ context._changedBits = providerFiber.stateNode;
46
+ if (__DEV__) {
47
+ warning(
48
+ context._currentRenderer === null ||
49
+ context._currentRenderer === rendererSigil,
50
+ 'Detected multiple renderers concurrently rendering the ' +
51
+ 'same context provider. This is currently unsupported.',
52
+ );
53
+ context._currentRenderer = rendererSigil;
54
+ }
55
+ } else {
56
+ push(changedBitsCursor, context._changedBits2, providerFiber);
57
+ push(valueCursor, context._currentValue2, providerFiber);
58
+ push(providerCursor, providerFiber, providerFiber);
59
+
60
+ context._currentValue2 = providerFiber.pendingProps.value;
61
+ context._changedBits2 = providerFiber.stateNode;
62
+ if (__DEV__) {
63
+ warning(
64
+ context._currentRenderer2 === null ||
65
+ context._currentRenderer2 === rendererSigil,
66
+ 'Detected multiple renderers concurrently rendering the ' +
67
+ 'same context provider. This is currently unsupported.',
68
+ );
69
+ context._currentRenderer2 = rendererSigil;
70
+ }
71
}
72
}
73
@@ -61,12 +80,27 @@ export default function(stack: Stack) {
80
pop(changedBitsCursor, providerFiber);
81
82
const context: ReactContext<any> = providerFiber.type._context;
64
- context._currentValue = currentValue;
65
- context._changedBits = changedBits;
83
+ if (isPrimaryRenderer) {
84
+ context._currentValue = currentValue;
85
+ context._changedBits = changedBits;
86
+ } else {
87
+ context._currentValue2 = currentValue;
88
+ context._changedBits2 = changedBits;
89
+ }
90
+ }
91
+
92
+ function getContextCurrentValue(context: ReactContext<any>): any {
93
+ return isPrimaryRenderer ? context._currentValue : context._currentValue2;
94
+ }
95
+
96
+ function getContextChangedBits(context: ReactContext<any>): number {
97
+ return isPrimaryRenderer ? context._changedBits : context._changedBits2;
98
}
99
100
return {
101
pushProvider,
102
popProvider,
103
+ getContextCurrentValue,
104
+ getContextChangedBits,
105
};
106
}
packages/react-reconciler/src/ReactFiberReconciler.js
+5
@@ -95,6 +95,11 @@ export type HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL> = {
95
96
now(): number,
97
98
+ // Temporary workaround for scenario where multiple renderers concurrently
99
+ // render using the same context objects. E.g. React DOM and React ART on the
100
+ // same page. DOM is the primary renderer; ART is the secondary renderer.
101
+ isPrimaryRenderer: boolean,
102
+
103
+hydration?: HydrationHostConfig<T, P, I, TI, HI, C, CX, PL>,
104
105
+mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>,
packages/react-reconciler/src/ReactFiberScheduler.js
+1
-1
@@ -181,7 +181,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
181
const stack = ReactFiberStack();
182
const hostContext = ReactFiberHostContext(config, stack);
183
const legacyContext = ReactFiberLegacyContext(stack);
184
- const newContext = ReactFiberNewContext(stack);
184
+ const newContext = ReactFiberNewContext(stack, config.isPrimaryRenderer);
185
const profilerTimer = createProfilerTimer(now);
186
const {popHostContext, popHostContainer} = hostContext;
187
const {
packages/react-test-renderer/src/ReactTestRenderer.js
+2
@@ -226,6 +226,8 @@ const TestRenderer = ReactFiberReconciler({
226
// Even after the reconciler has initialized and read host config values.
227
now: () => nowImplementation(),
228
229
+ isPrimaryRenderer: true,
230
+
231
mutation: {
232
commitUpdate(
233
instance: Instance,
packages/react/src/ReactContext.js
+8
@@ -36,7 +36,14 @@ export function createContext<T>(
36
_calculateChangedBits: calculateChangedBits,
37
_defaultValue: defaultValue,
38
_currentValue: defaultValue,
39
+ // As a workaround to support multiple concurrent renderers, we categorize
40
+ // some renderers as primary and others as secondary. We only expect
41
+ // there to be two concurrent renderers at most: React Native (primary) and
42
+ // Fabric (secondary); React DOM (primary) and React ART (secondary).
43
+ // Secondary renderers store their context values on separate fields.
44
+ _currentValue2: defaultValue,
45
_changedBits: 0,
46
+ _changedBits2: 0,
47
// These are circular
48
Provider: (null: any),
49
Consumer: (null: any),
@@ -50,6 +57,7 @@ export function createContext<T>(
57
58
if (__DEV__) {
59
context._currentRenderer = null;
60
+ context._currentRenderer2 = null;
61
}
62
63
return context;
packages/shared/ReactTypes.js
+3
@@ -85,10 +85,13 @@ export type ReactContext<T> = {
85
_defaultValue: T,
86
87
_currentValue: T,
88
+ _currentValue2: T,
89
_changedBits: number,
90
+ _changedBits2: number,
91
92
// DEV only
93
_currentRenderer?: Object | null,
94
+ _currentRenderer2?: Object | null,
95
};
96
97
export type ReactPortal = {