Implement basic support for context
Dan Abramov committed
Nov 11, 2016 at 21:14 UTC
12bee76ff7f8508a515271e2ec0cefe2065547ec
7 files changed
+380
-45
src/isomorphic/classic/__tests__/ReactContextValidator-test.js
+40
-5
@@ -70,11 +70,10 @@ describe('ReactContextValidator', () => {
70
expect(instance.refs.child.context).toEqual({foo: 'abc'});
71
});
72
73
- it('should filter context properly in callbacks', () => {
73
+ it('should pass next context to lifecycles', () => {
74
var actualComponentWillReceiveProps;
75
var actualShouldComponentUpdate;
76
var actualComponentWillUpdate;
77
- var actualComponentDidUpdate;
77
78
var Parent = React.createClass({
79
childContextTypes: {
@@ -113,6 +112,45 @@ describe('ReactContextValidator', () => {
112
actualComponentWillUpdate = nextContext;
113
},
114
115
+ render: function() {
116
+ return <div />;
117
+ },
118
+ });
119
+
120
+ var container = document.createElement('div');
121
+ ReactDOM.render(<Parent foo="abc" />, container);
122
+ ReactDOM.render(<Parent foo="def" />, container);
123
+ expect(actualComponentWillReceiveProps).toEqual({foo: 'def'});
124
+ expect(actualShouldComponentUpdate).toEqual({foo: 'def'});
125
+ expect(actualComponentWillUpdate).toEqual({foo: 'def'});
126
+ });
127
+
128
+ it('should pass previous context to lifecycles', () => {
129
+ var actualComponentDidUpdate;
130
+
131
+ var Parent = React.createClass({
132
+ childContextTypes: {
133
+ foo: React.PropTypes.string.isRequired,
134
+ bar: React.PropTypes.string.isRequired,
135
+ },
136
+
137
+ getChildContext: function() {
138
+ return {
139
+ foo: this.props.foo,
140
+ bar: 'bar',
141
+ };
142
+ },
143
+
144
+ render: function() {
145
+ return <Component />;
146
+ },
147
+ });
148
+
149
+ var Component = React.createClass({
150
+ contextTypes: {
151
+ foo: React.PropTypes.string,
152
+ },
153
+
154
componentDidUpdate: function(prevProps, prevState, prevContext) {
155
actualComponentDidUpdate = prevContext;
156
},
@@ -125,9 +163,6 @@ describe('ReactContextValidator', () => {
163
var container = document.createElement('div');
164
ReactDOM.render(<Parent foo="abc" />, container);
165
ReactDOM.render(<Parent foo="def" />, container);
128
- expect(actualComponentWillReceiveProps).toEqual({foo: 'def'});
129
- expect(actualShouldComponentUpdate).toEqual({foo: 'def'});
130
- expect(actualComponentWillUpdate).toEqual({foo: 'def'});
166
expect(actualComponentDidUpdate).toEqual({foo: 'abc'});
167
});
168
src/renderers/shared/fiber/ReactFiberBeginWork.js
+20
-5
@@ -23,7 +23,13 @@ var {
23
reconcileChildFibersInPlace,
24
cloneChildFibers,
25
} = require('ReactChildFiber');
26
+
27
var ReactTypeOfWork = require('ReactTypeOfWork');
28
+var {
29
+ getMaskedContext,
30
+ pushContextProvider,
31
+ resetContext,
32
+} = require('ReactFiberContext');
33
var {
34
IndeterminateComponent,
35
FunctionalComponent,
@@ -144,6 +150,7 @@ module.exports = function<T, P, I, TI, C>(
150
function updateFunctionalComponent(current, workInProgress) {
151
var fn = workInProgress.type;
152
var props = workInProgress.pendingProps;
153
+ var context = getMaskedContext(workInProgress);
154
155
// TODO: Disable this before release, since it is not part of the public API
156
// I use this for testing to compare the relative overhead of classes.
@@ -159,9 +166,9 @@ module.exports = function<T, P, I, TI, C>(
166
167
if (__DEV__) {
168
ReactCurrentOwner.current = workInProgress;
162
- nextChildren = fn(props);
169
+ nextChildren = fn(props, context);
170
} else {
164
- nextChildren = fn(props);
171
+ nextChildren = fn(props, context);
172
}
173
reconcileChildren(current, workInProgress, nextChildren);
174
return workInProgress.child;
@@ -182,11 +189,14 @@ module.exports = function<T, P, I, TI, C>(
189
} else {
190
shouldUpdate = updateClassInstance(current, workInProgress);
191
}
192
+ const instance = workInProgress.stateNode;
193
+ if (typeof instance.getChildContext === 'function') {
194
+ pushContextProvider(workInProgress);
195
+ }
196
if (!shouldUpdate) {
197
return bailoutOnAlreadyFinishedWork(current, workInProgress);
198
}
199
// Rerender
189
- const instance = workInProgress.stateNode;
200
ReactCurrentOwner.current = workInProgress;
201
const nextChildren = instance.render();
202
reconcileChildren(current, workInProgress, nextChildren);
@@ -249,13 +259,15 @@ module.exports = function<T, P, I, TI, C>(
259
}
260
var fn = workInProgress.type;
261
var props = workInProgress.pendingProps;
262
+ var context = getMaskedContext(workInProgress);
263
+
264
var value;
265
266
if (__DEV__) {
267
ReactCurrentOwner.current = workInProgress;
256
- value = fn(props);
268
+ value = fn(props, context);
269
} else {
258
- value = fn(props);
270
+ value = fn(props, context);
271
}
272
273
if (typeof value === 'object' && value && typeof value.render === 'function') {
@@ -355,6 +367,9 @@ module.exports = function<T, P, I, TI, C>(
367
}
368
369
function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
370
+ if (!workInProgress.return) {
371
+ resetContext();
372
+ }
373
if (workInProgress.pendingWorkPriority === NoWork ||
374
workInProgress.pendingWorkPriority > priorityLevel) {
375
return bailoutOnLowPriority(current, workInProgress);
src/renderers/shared/fiber/ReactFiberClassComponent.js
+33
-24
@@ -15,19 +15,21 @@
15
import type { Fiber } from 'ReactFiber';
16
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
17
18
+var {
19
+ getMaskedContext,
20
+} = require('ReactFiberContext');
21
var {
22
createUpdateQueue,
23
addToQueue,
24
addCallbackToQueue,
25
mergeUpdateQueue,
26
} = require('ReactFiberUpdateQueue');
24
-var { isMounted } = require('ReactFiberTreeReflection');
27
+var { getComponentName, isMounted } = require('ReactFiberTreeReflection');
28
var ReactInstanceMap = require('ReactInstanceMap');
29
var shallowEqual = require('shallowEqual');
30
var warning = require('warning');
31
var invariant = require('invariant');
32
30
-
33
const isArray = Array.isArray;
34
35
module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
@@ -74,7 +76,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
76
},
77
};
78
77
- function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState) {
79
+ function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) {
80
const updateQueue = workInProgress.updateQueue;
81
if (oldProps === null || (updateQueue && updateQueue.isForced)) {
82
return true;
@@ -82,14 +84,14 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
84
85
const instance = workInProgress.stateNode;
86
if (typeof instance.shouldComponentUpdate === 'function') {
85
- const shouldUpdate = instance.shouldComponentUpdate(newProps, newState);
87
+ const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext);
88
89
if (__DEV__) {
90
warning(
91
shouldUpdate !== undefined,
92
'%s.shouldComponentUpdate(): Returned undefined instead of a ' +
93
'boolean value. Make sure to return true or false.',
92
- getName(workInProgress, instance)
94
+ getComponentName(workInProgress)
95
);
96
}
97
@@ -107,19 +109,9 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
109
return true;
110
}
111
110
- function getName(workInProgress: Fiber, inst: any): string {
111
- const type = workInProgress.type;
112
- const constructor = inst && inst.constructor;
113
- return (
114
- type.displayName || (constructor && constructor.displayName) ||
115
- type.name || (constructor && constructor.name) ||
116
- 'A Component'
117
- );
118
- }
119
-
112
function checkClassInstance(workInProgress: Fiber, inst: any) {
113
if (__DEV__) {
122
- const name = getName(workInProgress, inst);
114
+ const name = getComponentName(workInProgress);
115
const renderPresent = inst.render;
116
warning(
117
renderPresent,
@@ -194,7 +186,15 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
186
invariant(
187
false,
188
'%s.state: must be set to an object or null',
197
- getName(workInProgress, inst)
189
+ getComponentName(workInProgress)
190
+ );
191
+ }
192
+ if (typeof inst.getChildContext === 'function') {
193
+ invariant(
194
+ typeof workInProgress.type.childContextTypes === 'object',
195
+ '%s.getChildContext(): childContextTypes must be defined in order to ' +
196
+ 'use getChildContext().',
197
+ getComponentName(workInProgress)
198
);
199
}
200
}
@@ -209,7 +209,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
209
function constructClassInstance(workInProgress : Fiber) : any {
210
const ctor = workInProgress.type;
211
const props = workInProgress.pendingProps;
212
- const instance = new ctor(props);
212
+ const context = getMaskedContext(workInProgress);
213
+ const instance = new ctor(props, context);
214
checkClassInstance(workInProgress, instance);
215
adoptClassInstance(workInProgress, instance);
216
return instance;
@@ -218,7 +219,6 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
219
// Invokes the mount life-cycles on a previously never rendered instance.
220
function mountClassInstance(workInProgress : Fiber) : void {
221
const instance = workInProgress.stateNode;
221
-
222
const state = instance.state || null;
223
224
let props = workInProgress.pendingProps;
@@ -228,6 +228,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
228
229
instance.props = props;
230
instance.state = state;
231
+ instance.context = getMaskedContext(workInProgress);
232
233
if (typeof instance.componentWillMount === 'function') {
234
instance.componentWillMount();
@@ -253,6 +254,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
254
throw new Error('There should always be pending or memoized props.');
255
}
256
}
257
+ const newContext = getMaskedContext(workInProgress);
258
259
// TODO: Should we deal with a setState that happened after the last
260
// componentWillMount and before this componentWillMount? Probably
@@ -262,7 +264,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
264
workInProgress,
265
workInProgress.memoizedProps,
266
newProps,
265
- newState
267
+ newState,
268
+ newContext
269
)) {
270
return false;
271
}
@@ -272,6 +275,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
275
const newInstance = constructClassInstance(workInProgress);
276
newInstance.props = newProps;
277
newInstance.state = newState = newInstance.state || null;
278
+ newInstance.context = getMaskedContext(workInProgress);
279
280
if (typeof newInstance.componentWillMount === 'function') {
281
newInstance.componentWillMount();
@@ -300,14 +304,16 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
304
throw new Error('There should always be pending or memoized props.');
305
}
306
}
307
+ const oldContext = instance.context;
308
+ const newContext = getMaskedContext(workInProgress);
309
310
// Note: During these life-cycles, instance.props/instance.state are what
311
// ever the previously attempted to render - not the "current". However,
312
// during componentDidUpdate we pass the "current" props.
313
308
- if (oldProps !== newProps) {
314
+ if (oldProps !== newProps || oldContext !== newContext) {
315
if (typeof instance.componentWillReceiveProps === 'function') {
310
- instance.componentWillReceiveProps(newProps);
316
+ instance.componentWillReceiveProps(newProps, newContext);
317
}
318
}
319
@@ -328,6 +334,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
334
335
if (oldProps === newProps &&
336
previousState === newState &&
337
+ oldContext === newContext &&
338
updateQueue && !updateQueue.isForced) {
339
return false;
340
}
@@ -336,18 +343,20 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
343
workInProgress,
344
oldProps,
345
newProps,
339
- newState
346
+ newState,
347
+ newContext
348
)) {
349
// TODO: Should this get the new props/state updated regardless?
350
return false;
351
}
352
353
if (typeof instance.componentWillUpdate === 'function') {
346
- instance.componentWillUpdate(newProps, newState);
354
+ instance.componentWillUpdate(newProps, newState, newContext);
355
}
356
357
instance.props = newProps;
358
instance.state = newState;
359
+ instance.context = newContext;
360
return true;
361
}
362
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+27
-11
@@ -18,6 +18,7 @@ import type { HostConfig } from 'ReactFiberReconciler';
18
import type { ReifiedYield } from 'ReactReifiedYield';
19
20
var { reconcileChildFibers } = require('ReactChildFiber');
21
+var { popContextProvider } = require('ReactFiberContext');
22
var ReactTypeOfWork = require('ReactTypeOfWork');
23
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
24
var {
@@ -120,10 +121,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
121
122
function completeWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
123
switch (workInProgress.tag) {
123
- case FunctionalComponent:
124
+ case FunctionalComponent: {
125
transferOutput(workInProgress.child, workInProgress);
126
return null;
126
- case ClassComponent:
127
+ }
128
+ case ClassComponent: {
129
transferOutput(workInProgress.child, workInProgress);
130
// Don't use the state queue to compute the memoized state. We already
131
// merged it and assigned it to the instance. Transfer it from there.
@@ -148,8 +150,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
150
workInProgress.callbackList = updateQueue;
151
markCallback(workInProgress);
152
}
153
+ const instance = workInProgress.stateNode;
154
+ if (typeof instance.getChildContext === 'function') {
155
+ popContextProvider();
156
+ }
157
return null;
152
- case HostContainer:
158
+ }
159
+ case HostContainer: {
160
transferOutput(workInProgress.child, workInProgress);
161
// We don't know if a container has updated any children so we always
162
// need to update it right now. We schedule this side-effect before
@@ -158,7 +165,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
165
// are invoked.
166
markUpdate(workInProgress);
167
return null;
161
- case HostComponent:
168
+ }
169
+ case HostComponent: {
170
let newProps = workInProgress.pendingProps;
171
if (current && workInProgress.stateNode != null) {
172
// If we have an alternate, that means this is an update and we need to
@@ -200,7 +208,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
208
}
209
workInProgress.memoizedProps = newProps;
210
return null;
203
- case HostText:
211
+ }
212
+ case HostText: {
213
let newText = workInProgress.pendingProps;
214
if (current && workInProgress.stateNode != null) {
215
// If we have an alternate, that means this is an update and we need to
@@ -222,25 +231,32 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
231
}
232
workInProgress.memoizedProps = newText;
233
return null;
225
- case CoroutineComponent:
234
+ }
235
+ case CoroutineComponent: {
236
return moveCoroutineToHandlerPhase(current, workInProgress);
227
- case CoroutineHandlerPhase:
237
+ }
238
+ case CoroutineHandlerPhase: {
239
transferOutput(workInProgress.stateNode, workInProgress);
240
// Reset the tag to now be a first phase coroutine.
241
workInProgress.tag = CoroutineComponent;
242
return null;
232
- case YieldComponent:
243
+ }
244
+ case YieldComponent: {
245
// Does nothing.
246
return null;
235
- case Fragment:
247
+ }
248
+ case Fragment: {
249
transferOutput(workInProgress.child, workInProgress);
250
return null;
251
+ }
252
253
// Error cases
240
- case IndeterminateComponent:
254
+ case IndeterminateComponent: {
255
throw new Error('An indeterminate component should have become determinate before completing.');
242
- default:
256
+ }
257
+ default: {
258
throw new Error('Unknown unit of work tag');
259
+ }
260
}
261
}
262
src/renderers/shared/fiber/ReactFiberContext.js
new
+91
@@ -0,0 +1,91 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ReactFiberContext
10
+ * @flow
11
+ */
12
+
13
+'use strict';
14
+
15
+import type { Fiber } from 'ReactFiber';
16
+
17
+var emptyObject = require('emptyObject');
18
+var invariant = require('invariant');
19
+var {
20
+ getComponentName,
21
+} = require('ReactFiberTreeReflection');
22
+
23
+if (__DEV__) {
24
+ var checkReactTypeSpec = require('checkReactTypeSpec');
25
+}
26
+
27
+let index = -1;
28
+const stack = [];
29
+
30
+function getUnmaskedContext() {
31
+ if (index === -1) {
32
+ return emptyObject;
33
+ }
34
+ return stack[index];
35
+}
36
+
37
+exports.getMaskedContext = function(fiber : Fiber) {
38
+ const type = fiber.type;
39
+ const contextTypes = type.contextTypes;
40
+ if (!contextTypes) {
41
+ return null;
42
+ }
43
+
44
+ const unmaskedContext = getUnmaskedContext();
45
+ const context = {};
46
+ for (let key in contextTypes) {
47
+ context[key] = unmaskedContext[key];
48
+ }
49
+
50
+ if (__DEV__) {
51
+ const name = getComponentName(fiber);
52
+ const debugID = 0; // TODO: pass a real ID
53
+ checkReactTypeSpec(contextTypes, context, 'context', name, null, debugID);
54
+ }
55
+
56
+ return context;
57
+};
58
+
59
+exports.popContextProvider = function() {
60
+ stack[index] = emptyObject;
61
+ index--;
62
+};
63
+
64
+exports.pushContextProvider = function(fiber : Fiber) {
65
+ const instance = fiber.stateNode;
66
+ const childContextTypes = fiber.type.childContextTypes;
67
+ const childContext = instance.getChildContext();
68
+
69
+ for (let contextKey in childContext) {
70
+ invariant(
71
+ contextKey in childContextTypes,
72
+ '%s.getChildContext(): key "%s" is not defined in childContextTypes.',
73
+ getComponentName(fiber),
74
+ contextKey
75
+ );
76
+ }
77
+ if (__DEV__) {
78
+ const name = getComponentName(fiber);
79
+ const debugID = 0; // TODO: pass a real ID
80
+ checkReactTypeSpec(childContextTypes, childContext, 'childContext', name, null, debugID);
81
+ }
82
+
83
+ const mergedContext = Object.assign({}, getUnmaskedContext(), childContext);
84
+ index++;
85
+ stack[index] = mergedContext;
86
+};
87
+
88
+exports.resetContext = function() {
89
+ index = -1;
90
+};
91
+
src/renderers/shared/fiber/ReactFiberTreeReflection.js
+11
@@ -114,3 +114,14 @@ exports.findCurrentHostFiber = function(component : ReactComponent<any, any, any
114
}
115
return null;
116
};
117
+
118
+exports.getComponentName = function(fiber: Fiber): string {
119
+ const type = fiber.type;
120
+ const instance = fiber.stateNode;
121
+ const constructor = instance && instance.constructor;
122
+ return (
123
+ type.displayName || (constructor && constructor.displayName) ||
124
+ type.name || (constructor && constructor.name) ||
125
+ 'A Component'
126
+ );
127
+};
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+158
@@ -1509,4 +1509,162 @@ describe('ReactIncremental', () => {
1509
]);
1510
expect(instance.state.n).toEqual(3);
1511
});
1512
+
1513
+ it('merges and masks context', () => {
1514
+ var ops = [];
1515
+
1516
+ class Intl extends React.Component {
1517
+ static childContextTypes = {
1518
+ locale: React.PropTypes.string,
1519
+ };
1520
+ getChildContext() {
1521
+ return {
1522
+ locale: this.props.locale,
1523
+ };
1524
+ }
1525
+ render() {
1526
+ ops.push('Intl ' + JSON.stringify(this.context));
1527
+ return this.props.children;
1528
+ }
1529
+ }
1530
+
1531
+ class Router extends React.Component {
1532
+ static childContextTypes = {
1533
+ route: React.PropTypes.string,
1534
+ };
1535
+ getChildContext() {
1536
+ return {
1537
+ route: this.props.route,
1538
+ };
1539
+ }
1540
+ render() {
1541
+ ops.push('Router ' + JSON.stringify(this.context));
1542
+ return this.props.children;
1543
+ }
1544
+ }
1545
+
1546
+ class ShowLocale extends React.Component {
1547
+ static contextTypes = {
1548
+ locale: React.PropTypes.string,
1549
+ };
1550
+
1551
+ render() {
1552
+ ops.push('ShowLocale ' + JSON.stringify(this.context));
1553
+ return this.context.locale;
1554
+ }
1555
+ }
1556
+
1557
+ class ShowRoute extends React.Component {
1558
+ static contextTypes = {
1559
+ route: React.PropTypes.string,
1560
+ };
1561
+
1562
+ render() {
1563
+ ops.push('ShowRoute ' + JSON.stringify(this.context));
1564
+ return this.context.route;
1565
+ }
1566
+ }
1567
+
1568
+ function ShowBoth(props, context) {
1569
+ ops.push('ShowBoth ' + JSON.stringify(context));
1570
+ return `${context.route} in ${context.locale}`;
1571
+ }
1572
+ ShowBoth.contextTypes = {
1573
+ locale: React.PropTypes.string,
1574
+ route: React.PropTypes.string,
1575
+ };
1576
+
1577
+ class ShowNeither extends React.Component {
1578
+ render() {
1579
+ ops.push('ShowNeither ' + JSON.stringify(this.context));
1580
+ return null;
1581
+ }
1582
+ }
1583
+
1584
+ class Indirection extends React.Component {
1585
+ render() {
1586
+ ops.push('Indirection ' + JSON.stringify(this.context));
1587
+ return [
1588
+ <ShowLocale />,
1589
+ <ShowRoute />,
1590
+ <ShowNeither />,
1591
+ <Intl locale="ru">
1592
+ <ShowBoth />
1593
+ </Intl>,
1594
+ <ShowBoth />,
1595
+ ];
1596
+ }
1597
+ }
1598
+
1599
+ ops.length = [];
1600
+ ReactNoop.render(
1601
+ <Intl locale="fr">
1602
+ <ShowLocale />
1603
+ <div>
1604
+ <ShowBoth />
1605
+ </div>
1606
+ </Intl>
1607
+ );
1608
+ ReactNoop.flush();
1609
+ expect(ops).toEqual([
1610
+ 'Intl null',
1611
+ 'ShowLocale {"locale":"fr"}',
1612
+ 'ShowBoth {"locale":"fr"}',
1613
+ ]);
1614
+
1615
+ ops.length = [];
1616
+ ReactNoop.render(
1617
+ <Intl locale="de">
1618
+ <ShowLocale />
1619
+ <div>
1620
+ <ShowBoth />
1621
+ </div>
1622
+ </Intl>
1623
+ );
1624
+ ReactNoop.flush();
1625
+ expect(ops).toEqual([
1626
+ 'Intl null',
1627
+ 'ShowLocale {"locale":"de"}',
1628
+ 'ShowBoth {"locale":"de"}',
1629
+ ]);
1630
+
1631
+ ops.length = [];
1632
+ ReactNoop.render(
1633
+ <Intl locale="sv">
1634
+ <ShowLocale />
1635
+ <div>
1636
+ <ShowBoth />
1637
+ </div>
1638
+ </Intl>
1639
+ );
1640
+ ReactNoop.flushDeferredPri(15);
1641
+ expect(ops).toEqual([
1642
+ 'Intl null',
1643
+ ]);
1644
+
1645
+ ops.length = [];
1646
+ ReactNoop.render(
1647
+ <Intl locale="en">
1648
+ <ShowLocale />
1649
+ <Router route="/about">
1650
+ <Indirection />
1651
+ </Router>
1652
+ <ShowBoth />
1653
+ </Intl>
1654
+ );
1655
+ ReactNoop.flush();
1656
+ expect(ops).toEqual([
1657
+ 'Intl null',
1658
+ 'ShowLocale {"locale":"en"}',
1659
+ 'Router null',
1660
+ 'Indirection null',
1661
+ 'ShowLocale {"locale":"en"}',
1662
+ 'ShowRoute {"route":"/about"}',
1663
+ 'ShowNeither null',
1664
+ 'Intl null',
1665
+ 'ShowBoth {"locale":"ru","route":"/about"}',
1666
+ 'ShowBoth {"locale":"en","route":"/about"}',
1667
+ 'ShowBoth {"locale":"en"}',
1668
+ ]);
1669
+ });
1670
});