@samitouri / QOS-React / commits / 12bee76ff7

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