30
// return {type: 'div', children, prop: undefined};
31
// }
32
33
+ function Text(props) {
34
+ ReactNoop.yield(props.text);
35
+ return <span prop={props.text} />;
36
+ }
37
+
38
function span(prop) {
39
return {type: 'span', children: [], prop};
40
}
41
37
- it('simple mount and update', () => {
38
- const Context = React.createContext(1);
39
-
40
- function Consumer(props) {
41
- return (
42
- <Context.Consumer>
43
- {value => <span prop={'Result: ' + value} />}
44
- </Context.Consumer>
45
- );
46
- }
47
-
48
- const Indirection = React.Fragment;
49
-
50
- function App(props) {
51
- return (
52
- <Context.Provider value={props.value}>
53
- <Indirection>
54
- <Indirection>
55
- <Consumer />
56
- </Indirection>
57
- </Indirection>
58
- </Context.Provider>
59
- );
60
- }
61
-
62
- ReactNoop.render(<App value={2} />);
63
- ReactNoop.flush();
64
- expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
65
-
66
- // Update
67
- ReactNoop.render(<App value={3} />);
68
- ReactNoop.flush();
69
- expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
70
- });
42
+ // We have several ways of reading from context. sharedContextTests runs
43
+ // a suite of tests for a given context consumer implementation.
44
+ sharedContextTests('Context.Consumer', Context => Context.Consumer);
45
+ sharedContextTests(
46
+ 'Context.unstable_read inside functional component',
47
+ Context =>
48
+ function Consumer(props) {
49
+ const observedBits = props.unstable_observedBits;
50
+ const contextValue = Context.unstable_read(observedBits);
51
+ const render = props.children;
52
+ return render(contextValue);
53
+ },
54
+ );
55
+ sharedContextTests(
56
+ 'Context.unstable_read inside class component',
57
+ Context =>
58
+ class Consumer extends React.Component {
59
+ render() {
60
+ const observedBits = this.props.unstable_observedBits;
61
+ const contextValue = Context.unstable_read(observedBits);
62
+ const render = this.props.children;
63
+ return render(contextValue);
64
+ }
65
+ },
66
+ );
67
72
- it('propagates through shouldComponentUpdate false', () => {
73
- const Context = React.createContext(1);
68
+ function sharedContextTests(label, getConsumer) {
69
+ describe(`reading context with ${label}`, () => {
70
+ it('simple mount and update', () => {
71
+ const Context = React.createContext(1);
72
+ const Consumer = getConsumer(Context);
73
75
- function Provider(props) {
76
- ReactNoop.yield('Provider');
77
- return (
78
- <Context.Provider value={props.value}>
79
- {props.children}
80
- </Context.Provider>
81
- );
82
- }
74
+ const Indirection = React.Fragment;
75
84
- function Consumer(props) {
85
- ReactNoop.yield('Consumer');
86
- return (
87
- <Context.Consumer>
88
- {value => {
89
- ReactNoop.yield('Consumer render prop');
90
- return <span prop={'Result: ' + value} />;
91
- }}
92
- </Context.Consumer>
93
- );
94
- }
76
+ function App(props) {
77
+ return (
78
+ <Context.Provider value={props.value}>
79
+ <Indirection>
80
+ <Indirection>
81
+ <Consumer>
82
+ {value => <span prop={'Result: ' + value} />}
83
+ </Consumer>
84
+ </Indirection>
85
+ </Indirection>
86
+ </Context.Provider>
87
+ );
88
+ }
89
96
- class Indirection extends React.Component {
97
- shouldComponentUpdate() {
98
- return false;
99
- }
100
- render() {
101
- ReactNoop.yield('Indirection');
102
- return this.props.children;
103
- }
104
- }
90
+ ReactNoop.render(<App value={2} />);
91
+ ReactNoop.flush();
92
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
93
106
- function App(props) {
107
- ReactNoop.yield('App');
108
- return (
109
- <Provider value={props.value}>
110
- <Indirection>
111
- <Indirection>
112
- <Consumer />
113
- </Indirection>
114
- </Indirection>
115
- </Provider>
116
- );
117
- }
94
+ // Update
95
+ ReactNoop.render(<App value={3} />);
96
+ ReactNoop.flush();
97
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
98
+ });
99
119
- ReactNoop.render(<App value={2} />);
120
- expect(ReactNoop.flush()).toEqual([
121
- 'App',
122
- 'Provider',
123
- 'Indirection',
124
- 'Indirection',
125
- 'Consumer',
126
- 'Consumer render prop',
127
- ]);
128
- expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
129
-
130
- // Update
131
- ReactNoop.render(<App value={3} />);
132
- expect(ReactNoop.flush()).toEqual([
133
- 'App',
134
- 'Provider',
135
- 'Consumer render prop',
136
- ]);
137
- expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
138
- });
100
+ it('propagates through shouldComponentUpdate false', () => {
101
+ const Context = React.createContext(1);
102
+ const ContextConsumer = getConsumer(Context);
103
140
- it('consumers bail out if context value is the same', () => {
141
- const Context = React.createContext(1);
104
+ function Provider(props) {
105
+ ReactNoop.yield('Provider');
106
+ return (
107
+ <Context.Provider value={props.value}>
108
+ {props.children}
109
+ </Context.Provider>
110
+ );
111
+ }
112
143
- function Provider(props) {
144
- ReactNoop.yield('Provider');
145
- return (
146
- <Context.Provider value={props.value}>
147
- {props.children}
148
- </Context.Provider>
149
- );
150
- }
113
+ function Consumer(props) {
114
+ ReactNoop.yield('Consumer');
115
+ return (
116
+ <ContextConsumer>
117
+ {value => {
118
+ ReactNoop.yield('Consumer render prop');
119
+ return <span prop={'Result: ' + value} />;
120
+ }}
121
+ </ContextConsumer>
122
+ );
123
+ }
124
152
- function Consumer(props) {
153
- ReactNoop.yield('Consumer');
154
- return (
155
- <Context.Consumer>
156
- {value => {
157
- ReactNoop.yield('Consumer render prop');
158
- return <span prop={'Result: ' + value} />;
159
- }}
160
- </Context.Consumer>
161
- );
162
- }
125
+ class Indirection extends React.Component {
126
+ shouldComponentUpdate() {
127
+ return false;
128
+ }
129
+ render() {
130
+ ReactNoop.yield('Indirection');
131
+ return this.props.children;
132
+ }
133
+ }
134
164
- class Indirection extends React.Component {
165
- shouldComponentUpdate() {
166
- return false;
167
- }
168
- render() {
169
- ReactNoop.yield('Indirection');
170
- return this.props.children;
171
- }
172
- }
135
+ function App(props) {
136
+ ReactNoop.yield('App');
137
+ return (
138
+ <Provider value={props.value}>
139
+ <Indirection>
140
+ <Indirection>
141
+ <Consumer />
142
+ </Indirection>
143
+ </Indirection>
144
+ </Provider>
145
+ );
146
+ }
147
174
- function App(props) {
175
- ReactNoop.yield('App');
176
- return (
177
- <Provider value={props.value}>
178
- <Indirection>
179
- <Indirection>
180
- <Consumer />
181
- </Indirection>
182
- </Indirection>
183
- </Provider>
184
- );
185
- }
148
+ ReactNoop.render(<App value={2} />);
149
+ expect(ReactNoop.flush()).toEqual([
150
+ 'App',
151
+ 'Provider',
152
+ 'Indirection',
153
+ 'Indirection',
154
+ 'Consumer',
155
+ 'Consumer render prop',
156
+ ]);
157
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
158
+
159
+ // Update
160
+ ReactNoop.render(<App value={3} />);
161
+ expect(ReactNoop.flush()).toEqual([
162
+ 'App',
163
+ 'Provider',
164
+ 'Consumer render prop',
165
+ ]);
166
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
167
+ });
168
+
169
+ it('consumers bail out if context value is the same', () => {
170
+ const Context = React.createContext(1);
171
+ const ContextConsumer = getConsumer(Context);
172
+
173
+ function Provider(props) {
174
+ ReactNoop.yield('Provider');
175
+ return (
176
+ <Context.Provider value={props.value}>
177
+ {props.children}
178
+ </Context.Provider>
179
+ );
180
+ }
181
187
- ReactNoop.render(<App value={2} />);
188
- expect(ReactNoop.flush()).toEqual([
189
- 'App',
190
- 'Provider',
191
- 'Indirection',
192
- 'Indirection',
193
- 'Consumer',
194
- 'Consumer render prop',
195
- ]);
196
- expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
197
-
198
- // Update with the same context value
199
- ReactNoop.render(<App value={2} />);
200
- expect(ReactNoop.flush()).toEqual([
201
- 'App',
202
- 'Provider',
203
- // Don't call render prop again
204
- ]);
205
- expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
206
- });
182
+ function Consumer(props) {
183
+ ReactNoop.yield('Consumer');
184
+ return (
185
+ <ContextConsumer>
186
+ {value => {
187
+ ReactNoop.yield('Consumer render prop');
188
+ return <span prop={'Result: ' + value} />;
189
+ }}
190
+ </ContextConsumer>
191
+ );
192
+ }
193
208
- it('nested providers', () => {
209
- const Context = React.createContext(1);
194
+ class Indirection extends React.Component {
195
+ shouldComponentUpdate() {
196
+ return false;
197
+ }
198
+ render() {
199
+ ReactNoop.yield('Indirection');
200
+ return this.props.children;
201
+ }
202
+ }
203
211
- function Provider(props) {
212
- return (
213
- <Context.Consumer>
214
- {contextValue => (
215
- // Multiply previous context value by 2, unless prop overrides
216
- <Context.Provider value={props.value || contextValue * 2}>
217
- {props.children}
218
- </Context.Provider>
219
- )}
220
- </Context.Consumer>
221
- );
222
- }
204
+ function App(props) {
205
+ ReactNoop.yield('App');
206
+ return (
207
+ <Provider value={props.value}>
208
+ <Indirection>
209
+ <Indirection>
210
+ <Consumer />
211
+ </Indirection>
212
+ </Indirection>
213
+ </Provider>
214
+ );
215
+ }
216
224
- function Consumer(props) {
225
- return (
226
- <Context.Consumer>
227
- {value => <span prop={'Result: ' + value} />}
228
- </Context.Consumer>
229
- );
230
- }
217
+ ReactNoop.render(<App value={2} />);
218
+ expect(ReactNoop.flush()).toEqual([
219
+ 'App',
220
+ 'Provider',
221
+ 'Indirection',
222
+ 'Indirection',
223
+ 'Consumer',
224
+ 'Consumer render prop',
225
+ ]);
226
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
227
+
228
+ // Update with the same context value
229
+ ReactNoop.render(<App value={2} />);
230
+ expect(ReactNoop.flush()).toEqual([
231
+ 'App',
232
+ 'Provider',
233
+ // Don't call render prop again
234
+ ]);
235
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
236
+ });
237
+
238
+ it('nested providers', () => {
239
+ const Context = React.createContext(1);
240
+ const Consumer = getConsumer(Context);
241
+
242
+ function Provider(props) {
243
+ return (
244
+ <Consumer>
245
+ {contextValue => (
246
+ // Multiply previous context value by 2, unless prop overrides
247
+ <Context.Provider value={props.value || contextValue * 2}>
248
+ {props.children}
249
+ </Context.Provider>
250
+ )}
251
+ </Consumer>
252
+ );
253
+ }
254
232
- class Indirection extends React.Component {
233
- shouldComponentUpdate() {
234
- return false;
235
- }
236
- render() {
237
- return this.props.children;
238
- }
239
- }
255
+ class Indirection extends React.Component {
256
+ shouldComponentUpdate() {
257
+ return false;
258
+ }
259
+ render() {
260
+ return this.props.children;
261
+ }
262
+ }
263
241
- function App(props) {
242
- return (
243
- <Provider value={props.value}>
244
- <Indirection>
245
- <Provider>
264
+ function App(props) {
265
+ return (
266
+ <Provider value={props.value}>
267
<Indirection>
268
<Provider>
269
<Indirection>
249
- <Consumer />
270
+ <Provider>
271
+ <Indirection>
272
+ <Consumer>
273
+ {value => <span prop={'Result: ' + value} />}
274
+ </Consumer>
275
+ </Indirection>
276
+ </Provider>
277
</Indirection>
278
</Provider>
279
</Indirection>
280
</Provider>
254
- </Indirection>
255
- </Provider>
256
- );
257
- }
281
+ );
282
+ }
283
259
- ReactNoop.render(<App value={2} />);
260
- ReactNoop.flush();
261
- expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
284
+ ReactNoop.render(<App value={2} />);
285
+ ReactNoop.flush();
286
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
287
263
- // Update
264
- ReactNoop.render(<App value={3} />);
265
- ReactNoop.flush();
266
- expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
267
- });
288
+ // Update
289
+ ReactNoop.render(<App value={3} />);
290
+ ReactNoop.flush();
291
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
292
+ });
293
269
- it('should provide the correct (default) values to consumers outside of a provider', () => {
270
- const FooContext = React.createContext({value: 'foo-initial'});
271
- const BarContext = React.createContext({value: 'bar-initial'});
294
+ it('should provide the correct (default) values to consumers outside of a provider', () => {
295
+ const FooContext = React.createContext({value: 'foo-initial'});
296
+ const BarContext = React.createContext({value: 'bar-initial'});
297
+ const FooConsumer = getConsumer(FooContext);
298
+ const BarConsumer = getConsumer(BarContext);
299
273
- const Verify = ({actual, expected}) => {
274
- expect(expected).toBe(actual);
275
- return null;
276
- };
300
+ const Verify = ({actual, expected}) => {
301
+ expect(expected).toBe(actual);
302
+ return null;
303
+ };
304
278
- ReactNoop.render(
279
- <React.Fragment>
280
- <BarContext.Provider value={{value: 'bar-updated'}}>
281
- <BarContext.Consumer>
282
- {({value}) => <Verify actual={value} expected="bar-updated" />}
283
- </BarContext.Consumer>
284
-
285
- <FooContext.Provider value={{value: 'foo-updated'}}>
286
- <FooContext.Consumer>
287
- {({value}) => <Verify actual={value} expected="foo-updated" />}
288
- </FooContext.Consumer>
289
- </FooContext.Provider>
290
- </BarContext.Provider>
291
-
292
- <FooContext.Consumer>
293
- {({value}) => <Verify actual={value} expected="foo-initial" />}
294
- </FooContext.Consumer>
295
- <BarContext.Consumer>
296
- {({value}) => <Verify actual={value} expected="bar-initial" />}
297
- </BarContext.Consumer>
298
- </React.Fragment>,
299
- );
300
- ReactNoop.flush();
301
- });
305
+ ReactNoop.render(
306
+ <React.Fragment>
307
+ <BarContext.Provider value={{value: 'bar-updated'}}>
308
+ <BarConsumer>
309
+ {({value}) => <Verify actual={value} expected="bar-updated" />}
310
+ </BarConsumer>
311
+
312
+ <FooContext.Provider value={{value: 'foo-updated'}}>
313
+ <FooConsumer>
314
+ {({value}) => (
315
+ <Verify actual={value} expected="foo-updated" />
316
+ )}
317
+ </FooConsumer>
318
+ </FooContext.Provider>
319
+ </BarContext.Provider>
320
+
321
+ <FooConsumer>
322
+ {({value}) => <Verify actual={value} expected="foo-initial" />}
323
+ </FooConsumer>
324
+ <BarConsumer>
325
+ {({value}) => <Verify actual={value} expected="bar-initial" />}
326
+ </BarConsumer>
327
+ </React.Fragment>,
328
+ );
329
+ ReactNoop.flush();
330
+ });
331
+
332
+ it('multiple consumers in different branches', () => {
333
+ const Context = React.createContext(1);
334
+ const Consumer = getConsumer(Context);
335
+
336
+ function Provider(props) {
337
+ return (
338
+ <Context.Consumer>
339
+ {contextValue => (
340
+ // Multiply previous context value by 2, unless prop overrides
341
+ <Context.Provider value={props.value || contextValue * 2}>
342
+ {props.children}
343
+ </Context.Provider>
344
+ )}
345
+ </Context.Consumer>
346
+ );
347
+ }
348
303
- it('multiple consumers in different branches', () => {
304
- const Context = React.createContext(1);
349
+ class Indirection extends React.Component {
350
+ shouldComponentUpdate() {
351
+ return false;
352
+ }
353
+ render() {
354
+ return this.props.children;
355
+ }
356
+ }
357
306
- function Provider(props) {
307
- return (
308
- <Context.Consumer>
309
- {contextValue => (
310
- // Multiply previous context value by 2, unless prop overrides
311
- <Context.Provider value={props.value || contextValue * 2}>
312
- {props.children}
313
- </Context.Provider>
314
- )}
315
- </Context.Consumer>
316
- );
317
- }
358
+ function App(props) {
359
+ return (
360
+ <Provider value={props.value}>
361
+ <Indirection>
362
+ <Indirection>
363
+ <Provider>
364
+ <Consumer>
365
+ {value => <span prop={'Result: ' + value} />}
366
+ </Consumer>
367
+ </Provider>
368
+ </Indirection>
369
+ <Indirection>
370
+ <Consumer>
371
+ {value => <span prop={'Result: ' + value} />}
372
+ </Consumer>
373
+ </Indirection>
374
+ </Indirection>
375
+ </Provider>
376
+ );
377
+ }
378
319
- function Consumer(props) {
320
- return (
321
- <Context.Consumer>
322
- {value => <span prop={'Result: ' + value} />}
323
- </Context.Consumer>
324
- );
325
- }
379
+ ReactNoop.render(<App value={2} />);
380
+ ReactNoop.flush();
381
+ expect(ReactNoop.getChildren()).toEqual([
382
+ span('Result: 4'),
383
+ span('Result: 2'),
384
+ ]);
385
327
- class Indirection extends React.Component {
328
- shouldComponentUpdate() {
329
- return false;
330
- }
331
- render() {
332
- return this.props.children;
333
- }
334
- }
386
+ // Update
387
+ ReactNoop.render(<App value={3} />);
388
+ ReactNoop.flush();
389
+ expect(ReactNoop.getChildren()).toEqual([
390
+ span('Result: 6'),
391
+ span('Result: 3'),
392
+ ]);
393
336
- function App(props) {
337
- return (
338
- <Provider value={props.value}>
339
- <Indirection>
340
- <Indirection>
341
- <Provider>
342
- <Consumer />
343
- </Provider>
344
- </Indirection>
345
- <Indirection>
346
- <Consumer />
347
- </Indirection>
348
- </Indirection>
349
- </Provider>
350
- );
351
- }
394
+ // Another update
395
+ ReactNoop.render(<App value={4} />);
396
+ ReactNoop.flush();
397
+ expect(ReactNoop.getChildren()).toEqual([
398
+ span('Result: 8'),
399
+ span('Result: 4'),
400
+ ]);
401
+ });
402
+
403
+ it('compares context values with Object.is semantics', () => {
404
+ const Context = React.createContext(1);
405
+ const ContextConsumer = getConsumer(Context);
406
+
407
+ function Provider(props) {
408
+ ReactNoop.yield('Provider');
409
+ return (
410
+ <Context.Provider value={props.value}>
411
+ {props.children}
412
+ </Context.Provider>
413
+ );
414
+ }
415
353
- ReactNoop.render(<App value={2} />);
354
- ReactNoop.flush();
355
- expect(ReactNoop.getChildren()).toEqual([
356
- span('Result: 4'),
357
- span('Result: 2'),
358
- ]);
416
+ function Consumer(props) {
417
+ ReactNoop.yield('Consumer');
418
+ return (
419
+ <ContextConsumer>
420
+ {value => {
421
+ ReactNoop.yield('Consumer render prop');
422
+ return <span prop={'Result: ' + value} />;
423
+ }}
424
+ </ContextConsumer>
425
+ );
426
+ }
427
360
- // Update
361
- ReactNoop.render(<App value={3} />);
362
- ReactNoop.flush();
363
- expect(ReactNoop.getChildren()).toEqual([
364
- span('Result: 6'),
365
- span('Result: 3'),
366
- ]);
428
+ class Indirection extends React.Component {
429
+ shouldComponentUpdate() {
430
+ return false;
431
+ }
432
+ render() {
433
+ ReactNoop.yield('Indirection');
434
+ return this.props.children;
435
+ }
436
+ }
437
368
- // Another update
369
- ReactNoop.render(<App value={4} />);
370
- ReactNoop.flush();
371
- expect(ReactNoop.getChildren()).toEqual([
372
- span('Result: 8'),
373
- span('Result: 4'),
374
- ]);
375
- });
438
+ function App(props) {
439
+ ReactNoop.yield('App');
440
+ return (
441
+ <Provider value={props.value}>
442
+ <Indirection>
443
+ <Indirection>
444
+ <Consumer />
445
+ </Indirection>
446
+ </Indirection>
447
+ </Provider>
448
+ );
449
+ }
450
377
- it('compares context values with Object.is semantics', () => {
378
- const Context = React.createContext(1);
451
+ ReactNoop.render(<App value={NaN} />);
452
+ expect(ReactNoop.flush()).toEqual([
453
+ 'App',
454
+ 'Provider',
455
+ 'Indirection',
456
+ 'Indirection',
457
+ 'Consumer',
458
+ 'Consumer render prop',
459
+ ]);
460
+ expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
461
+
462
+ // Update
463
+ ReactNoop.render(<App value={NaN} />);
464
+ expect(ReactNoop.flush()).toEqual([
465
+ 'App',
466
+ 'Provider',
467
+ // Consumer should not re-render again
468
+ // 'Consumer render prop',
469
+ ]);
470
+ expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
471
+ });
472
+
473
+ it('context unwinds when interrupted', () => {
474
+ const Context = React.createContext('Default');
475
+ const ContextConsumer = getConsumer(Context);
476
+
477
+ function Consumer(props) {
478
+ return (
479
+ <ContextConsumer>
480
+ {value => <span prop={'Result: ' + value} />}
481
+ </ContextConsumer>
482
+ );
483
+ }
484
380
- function Provider(props) {
381
- ReactNoop.yield('Provider');
382
- return (
383
- <Context.Provider value={props.value}>
384
- {props.children}
385
- </Context.Provider>
386
- );
387
- }
485
+ function BadRender() {
486
+ throw new Error('Bad render');
487
+ }
488
389
- function Consumer(props) {
390
- ReactNoop.yield('Consumer');
391
- return (
392
- <Context.Consumer>
393
- {value => {
394
- ReactNoop.yield('Consumer render prop');
395
- return <span prop={'Result: ' + value} />;
396
- }}
397
- </Context.Consumer>
398
- );
399
- }
489
+ class ErrorBoundary extends React.Component {
490
+ state = {error: null};
491
+ componentDidCatch(error) {
492
+ this.setState({error});
493
+ }
494
+ render() {
495
+ if (this.state.error) {
496
+ return null;
497
+ }
498
+ return this.props.children;
499
+ }
500
+ }
501
401
- class Indirection extends React.Component {
402
- shouldComponentUpdate() {
403
- return false;
404
- }
405
- render() {
406
- ReactNoop.yield('Indirection');
407
- return this.props.children;
408
- }
409
- }
502
+ function App(props) {
503
+ return (
504
+ <React.Fragment>
505
+ <Context.Provider value="Does not unwind">
506
+ <ErrorBoundary>
507
+ <Context.Provider value="Unwinds after BadRender throws">
508
+ <BadRender />
509
+ </Context.Provider>
510
+ </ErrorBoundary>
511
+ <Consumer />
512
+ </Context.Provider>
513
+ </React.Fragment>
514
+ );
515
+ }
516
411
- function App(props) {
412
- ReactNoop.yield('App');
413
- return (
414
- <Provider value={props.value}>
415
- <Indirection>
416
- <Indirection>
417
- <Consumer />
418
- </Indirection>
419
- </Indirection>
420
- </Provider>
421
- );
422
- }
517
+ ReactNoop.render(<App value="A" />);
518
+ ReactNoop.flush();
519
+ expect(ReactNoop.getChildren()).toEqual([
520
+ // The second provider should use the default value.
521
+ span('Result: Does not unwind'),
522
+ ]);
523
+ });
524
+
525
+ it('can skip consumers with bitmask', () => {
526
+ const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
527
+ let result = 0;
528
+ if (a.foo !== b.foo) {
529
+ result |= 0b01;
530
+ }
531
+ if (a.bar !== b.bar) {
532
+ result |= 0b10;
533
+ }
534
+ return result;
535
+ });
536
+ const Consumer = getConsumer(Context);
537
424
- ReactNoop.render(<App value={NaN} />);
425
- expect(ReactNoop.flush()).toEqual([
426
- 'App',
427
- 'Provider',
428
- 'Indirection',
429
- 'Indirection',
430
- 'Consumer',
431
- 'Consumer render prop',
432
- ]);
433
- expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
434
-
435
- // Update
436
- ReactNoop.render(<App value={NaN} />);
437
- expect(ReactNoop.flush()).toEqual([
438
- 'App',
439
- 'Provider',
440
- // Consumer should not re-render again
441
- // 'Consumer render prop',
442
- ]);
443
- expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
444
- });
538
+ function Provider(props) {
539
+ return (
540
+ <Context.Provider value={{foo: props.foo, bar: props.bar}}>
541
+ {props.children}
542
+ </Context.Provider>
543
+ );
544
+ }
545
446
- it('context unwinds when interrupted', () => {
447
- const Context = React.createContext('Default');
546
+ function Foo() {
547
+ return (
548
+ <Consumer unstable_observedBits={0b01}>
549
+ {value => {
550
+ ReactNoop.yield('Foo');
551
+ return <span prop={'Foo: ' + value.foo} />;
552
+ }}
553
+ </Consumer>
554
+ );
555
+ }
556
449
- function Consumer(props) {
450
- return (
451
- <Context.Consumer>
452
- {value => <span prop={'Result: ' + value} />}
453
- </Context.Consumer>
454
- );
455
- }
557
+ function Bar() {
558
+ return (
559
+ <Consumer unstable_observedBits={0b10}>
560
+ {value => {
561
+ ReactNoop.yield('Bar');
562
+ return <span prop={'Bar: ' + value.bar} />;
563
+ }}
564
+ </Consumer>
565
+ );
566
+ }
567
457
- function BadRender() {
458
- throw new Error('Bad render');
459
- }
568
+ class Indirection extends React.Component {
569
+ shouldComponentUpdate() {
570
+ return false;
571
+ }
572
+ render() {
573
+ return this.props.children;
574
+ }
575
+ }
576
461
- class ErrorBoundary extends React.Component {
462
- state = {error: null};
463
- componentDidCatch(error) {
464
- this.setState({error});
465
- }
466
- render() {
467
- if (this.state.error) {
468
- return null;
577
+ function App(props) {
578
+ return (
579
+ <Provider foo={props.foo} bar={props.bar}>
580
+ <Indirection>
581
+ <Indirection>
582
+ <Foo />
583
+ </Indirection>
584
+ <Indirection>
585
+ <Bar />
586
+ </Indirection>
587
+ </Indirection>
588
+ </Provider>
589
+ );
590
}
470
- return this.props.children;
471
- }
472
- }
591
474
- function App(props) {
475
- return (
476
- <React.Fragment>
477
- <Context.Provider value="Does not unwind">
478
- <ErrorBoundary>
479
- <Context.Provider value="Unwinds after BadRender throws">
480
- <BadRender />
481
- </Context.Provider>
482
- </ErrorBoundary>
483
- <Consumer />
484
- </Context.Provider>
485
- </React.Fragment>
486
- );
487
- }
592
+ ReactNoop.render(<App foo={1} bar={1} />);
593
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
594
+ expect(ReactNoop.getChildren()).toEqual([
595
+ span('Foo: 1'),
596
+ span('Bar: 1'),
597
+ ]);
598
+
599
+ // Update only foo
600
+ ReactNoop.render(<App foo={2} bar={1} />);
601
+ expect(ReactNoop.flush()).toEqual(['Foo']);
602
+ expect(ReactNoop.getChildren()).toEqual([
603
+ span('Foo: 2'),
604
+ span('Bar: 1'),
605
+ ]);
606
+
607
+ // Update only bar
608
+ ReactNoop.render(<App foo={2} bar={2} />);
609
+ expect(ReactNoop.flush()).toEqual(['Bar']);
610
+ expect(ReactNoop.getChildren()).toEqual([
611
+ span('Foo: 2'),
612
+ span('Bar: 2'),
613
+ ]);
614
+
615
+ // Update both
616
+ ReactNoop.render(<App foo={3} bar={3} />);
617
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
618
+ expect(ReactNoop.getChildren()).toEqual([
619
+ span('Foo: 3'),
620
+ span('Bar: 3'),
621
+ ]);
622
+ });
623
+
624
+ it('can skip parents with bitmask bailout while updating their children', () => {
625
+ const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
626
+ let result = 0;
627
+ if (a.foo !== b.foo) {
628
+ result |= 0b01;
629
+ }
630
+ if (a.bar !== b.bar) {
631
+ result |= 0b10;
632
+ }
633
+ return result;
634
+ });
635
+ const Consumer = getConsumer(Context);
636
489
- ReactNoop.render(<App value="A" />);
490
- ReactNoop.flush();
491
- expect(ReactNoop.getChildren()).toEqual([
492
- // The second provider should use the default value.
493
- span('Result: Does not unwind'),
494
- ]);
495
- });
637
+ function Provider(props) {
638
+ return (
639
+ <Context.Provider value={{foo: props.foo, bar: props.bar}}>
640
+ {props.children}
641
+ </Context.Provider>
642
+ );
643
+ }
644
497
- it('can skip consumers with bitmask', () => {
498
- const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
499
- let result = 0;
500
- if (a.foo !== b.foo) {
501
- result |= 0b01;
502
- }
503
- if (a.bar !== b.bar) {
504
- result |= 0b10;
505
- }
506
- return result;
507
- });
645
+ function Foo(props) {
646
+ return (
647
+ <Consumer unstable_observedBits={0b01}>
648
+ {value => {
649
+ ReactNoop.yield('Foo');
650
+ return (
651
+ <React.Fragment>
652
+ <span prop={'Foo: ' + value.foo} />
653
+ {props.children && props.children()}
654
+ </React.Fragment>
655
+ );
656
+ }}
657
+ </Consumer>
658
+ );
659
+ }
660
509
- function Provider(props) {
510
- return (
511
- <Context.Provider value={{foo: props.foo, bar: props.bar}}>
512
- {props.children}
513
- </Context.Provider>
514
- );
515
- }
661
+ function Bar(props) {
662
+ return (
663
+ <Consumer unstable_observedBits={0b10}>
664
+ {value => {
665
+ ReactNoop.yield('Bar');
666
+ return (
667
+ <React.Fragment>
668
+ <span prop={'Bar: ' + value.bar} />
669
+ {props.children && props.children()}
670
+ </React.Fragment>
671
+ );
672
+ }}
673
+ </Consumer>
674
+ );
675
+ }
676
517
- function Foo() {
518
- return (
519
- <Context.Consumer unstable_observedBits={0b01}>
520
- {value => {
521
- ReactNoop.yield('Foo');
522
- return <span prop={'Foo: ' + value.foo} />;
523
- }}
524
- </Context.Consumer>
525
- );
526
- }
677
+ class Indirection extends React.Component {
678
+ shouldComponentUpdate() {
679
+ return false;
680
+ }
681
+ render() {
682
+ return this.props.children;
683
+ }
684
+ }
685
528
- function Bar() {
529
- return (
530
- <Context.Consumer unstable_observedBits={0b10}>
531
- {value => {
532
- ReactNoop.yield('Bar');
533
- return <span prop={'Bar: ' + value.bar} />;
534
- }}
535
- </Context.Consumer>
536
- );
537
- }
686
+ function App(props) {
687
+ return (
688
+ <Provider foo={props.foo} bar={props.bar}>
689
+ <Indirection>
690
+ <Foo>
691
+ {/* Use a render prop so we don't test constant elements. */}
692
+ {() => (
693
+ <Indirection>
694
+ <Bar>
695
+ {() => (
696
+ <Indirection>
697
+ <Foo />
698
+ </Indirection>
699
+ )}
700
+ </Bar>
701
+ </Indirection>
702
+ )}
703
+ </Foo>
704
+ </Indirection>
705
+ </Provider>
706
+ );
707
+ }
708
539
- class Indirection extends React.Component {
540
- shouldComponentUpdate() {
541
- return false;
542
- }
543
- render() {
544
- return this.props.children;
545
- }
546
- }
709
+ ReactNoop.render(<App foo={1} bar={1} />);
710
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
711
+ expect(ReactNoop.getChildren()).toEqual([
712
+ span('Foo: 1'),
713
+ span('Bar: 1'),
714
+ span('Foo: 1'),
715
+ ]);
716
+
717
+ // Update only foo
718
+ ReactNoop.render(<App foo={2} bar={1} />);
719
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']);
720
+ expect(ReactNoop.getChildren()).toEqual([
721
+ span('Foo: 2'),
722
+ span('Bar: 1'),
723
+ span('Foo: 2'),
724
+ ]);
725
+
726
+ // Update only bar
727
+ ReactNoop.render(<App foo={2} bar={2} />);
728
+ expect(ReactNoop.flush()).toEqual(['Bar']);
729
+ expect(ReactNoop.getChildren()).toEqual([
730
+ span('Foo: 2'),
731
+ span('Bar: 2'),
732
+ span('Foo: 2'),
733
+ ]);
734
+
735
+ // Update both
736
+ ReactNoop.render(<App foo={3} bar={3} />);
737
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
738
+ expect(ReactNoop.getChildren()).toEqual([
739
+ span('Foo: 3'),
740
+ span('Bar: 3'),
741
+ span('Foo: 3'),
742
+ ]);
743
+ });
744
+
745
+ it("does not re-render if there's an update in a child", () => {
746
+ const Context = React.createContext(0);
747
+ const Consumer = getConsumer(Context);
748
+
749
+ let child;
750
+ class Child extends React.Component {
751
+ state = {step: 0};
752
+ render() {
753
+ ReactNoop.yield('Child');
754
+ return (
755
+ <span
756
+ prop={`Context: ${this.props.context}, Step: ${
757
+ this.state.step
758
+ }`}
759
+ />
760
+ );
761
+ }
762
+ }
763
548
- function App(props) {
549
- return (
550
- <Provider foo={props.foo} bar={props.bar}>
551
- <Indirection>
552
- <Indirection>
553
- <Foo />
554
- </Indirection>
555
- <Indirection>
556
- <Bar />
557
- </Indirection>
558
- </Indirection>
559
- </Provider>
560
- );
561
- }
764
+ function App(props) {
765
+ return (
766
+ <Context.Provider value={props.value}>
767
+ <Consumer>
768
+ {value => {
769
+ ReactNoop.yield('Consumer render prop');
770
+ return <Child ref={inst => (child = inst)} context={value} />;
771
+ }}
772
+ </Consumer>
773
+ </Context.Provider>
774
+ );
775
+ }
776
563
- ReactNoop.render(<App foo={1} bar={1} />);
564
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
565
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 1'), span('Bar: 1')]);
777
+ // Initial mount
778
+ ReactNoop.render(<App value={1} />);
779
+ expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']);
780
+ expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]);
781
567
- // Update only foo
568
- ReactNoop.render(<App foo={2} bar={1} />);
569
- expect(ReactNoop.flush()).toEqual(['Foo']);
570
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 1')]);
782
+ child.setState({step: 1});
783
+ expect(ReactNoop.flush()).toEqual(['Child']);
784
+ expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]);
785
+ });
786
572
- // Update only bar
573
- ReactNoop.render(<App foo={2} bar={2} />);
574
- expect(ReactNoop.flush()).toEqual(['Bar']);
575
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 2')]);
787
+ it('consumer bails out if value is unchanged and something above bailed out', () => {
788
+ const Context = React.createContext(0);
789
+ const Consumer = getConsumer(Context);
790
577
- // Update both
578
- ReactNoop.render(<App foo={3} bar={3} />);
579
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
580
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]);
581
- });
791
+ function renderChildValue(value) {
792
+ ReactNoop.yield('Consumer');
793
+ return <span prop={value} />;
794
+ }
795
583
- it('can skip parents with bitmask bailout while updating their children', () => {
584
- const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
585
- let result = 0;
586
- if (a.foo !== b.foo) {
587
- result |= 0b01;
588
- }
589
- if (a.bar !== b.bar) {
590
- result |= 0b10;
591
- }
592
- return result;
593
- });
796
+ function ChildWithInlineRenderCallback() {
797
+ ReactNoop.yield('ChildWithInlineRenderCallback');
798
+ // Note: we are intentionally passing an inline arrow. Don't refactor.
799
+ return <Consumer>{value => renderChildValue(value)}</Consumer>;
800
+ }
801
595
- function Provider(props) {
596
- return (
597
- <Context.Provider value={{foo: props.foo, bar: props.bar}}>
598
- {props.children}
599
- </Context.Provider>
600
- );
601
- }
802
+ function ChildWithCachedRenderCallback() {
803
+ ReactNoop.yield('ChildWithCachedRenderCallback');
804
+ return <Consumer>{renderChildValue}</Consumer>;
805
+ }
806
603
- function Foo(props) {
604
- return (
605
- <Context.Consumer unstable_observedBits={0b01}>
606
- {value => {
607
- ReactNoop.yield('Foo');
807
+ class PureIndirection extends React.PureComponent {
808
+ render() {
809
+ ReactNoop.yield('PureIndirection');
810
return (
811
<React.Fragment>
610
- <span prop={'Foo: ' + value.foo} />
611
- {props.children && props.children()}
812
+ <ChildWithInlineRenderCallback />
813
+ <ChildWithCachedRenderCallback />
814
</React.Fragment>
815
);
614
- }}
615
- </Context.Consumer>
616
- );
617
- }
816
+ }
817
+ }
818
619
- function Bar(props) {
620
- return (
621
- <Context.Consumer unstable_observedBits={0b10}>
622
- {value => {
623
- ReactNoop.yield('Bar');
819
+ class App extends React.Component {
820
+ render() {
821
+ ReactNoop.yield('App');
822
return (
625
- <React.Fragment>
626
- <span prop={'Bar: ' + value.bar} />
627
- {props.children && props.children()}
628
- </React.Fragment>
823
+ <Context.Provider value={this.props.value}>
824
+ <PureIndirection />
825
+ </Context.Provider>
826
);
630
- }}
631
- </Context.Consumer>
632
- );
633
- }
827
+ }
828
+ }
829
635
- class Indirection extends React.Component {
636
- shouldComponentUpdate() {
637
- return false;
638
- }
639
- render() {
640
- return this.props.children;
641
- }
642
- }
830
+ // Initial mount
831
+ ReactNoop.render(<App value={1} />);
832
+ expect(ReactNoop.flush()).toEqual([
833
+ 'App',
834
+ 'PureIndirection',
835
+ 'ChildWithInlineRenderCallback',
836
+ 'Consumer',
837
+ 'ChildWithCachedRenderCallback',
838
+ 'Consumer',
839
+ ]);
840
+ expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
841
+
842
+ // Update (bailout)
843
+ ReactNoop.render(<App value={1} />);
844
+ expect(ReactNoop.flush()).toEqual(['App']);
845
+ expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
846
+
847
+ // Update (no bailout)
848
+ ReactNoop.render(<App value={2} />);
849
+ expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
850
+ expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
851
+ });
852
+
853
+ // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
854
+ // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
855
+ // If we bailed out on referential equality, it would be confusing that you
856
+ // can call this.setState(), but an autobound render callback "blocked" the update.
857
+ // https://github.com/facebook/react/pull/12470#issuecomment-376917711
858
+ it('consumer does not bail out if there were no bailouts above it', () => {
859
+ const Context = React.createContext(0);
860
+ const Consumer = getConsumer(Context);
861
+
862
+ class App extends React.Component {
863
+ state = {
864
+ text: 'hello',
865
+ };
866
+
867
+ renderConsumer = context => {
868
+ ReactNoop.yield('App#renderConsumer');
869
+ return <span prop={this.state.text} />;
870
+ };
871
+
872
+ render() {
873
+ ReactNoop.yield('App');
874
+ return (
875
+ <Context.Provider value={this.props.value}>
876
+ <Consumer>{this.renderConsumer}</Consumer>
877
+ </Context.Provider>
878
+ );
879
+ }
880
+ }
881
644
- function App(props) {
645
- return (
646
- <Provider foo={props.foo} bar={props.bar}>
647
- <Indirection>
648
- <Foo>
649
- {/* Use a render prop so we don't test constant elements. */}
650
- {() => (
651
- <Indirection>
652
- <Bar>
653
- {() => (
654
- <Indirection>
655
- <Foo />
656
- </Indirection>
657
- )}
658
- </Bar>
659
- </Indirection>
660
- )}
661
- </Foo>
662
- </Indirection>
663
- </Provider>
664
- );
665
- }
882
+ // Initial mount
883
+ let inst;
884
+ ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
885
+ expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
886
+ expect(ReactNoop.getChildren()).toEqual([span('hello')]);
887
+
888
+ // Update
889
+ inst.setState({text: 'goodbye'});
890
+ expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
891
+ expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
892
+ });
893
+
894
+ // This is a regression case for https://github.com/facebook/react/issues/12389.
895
+ it('does not run into an infinite loop', () => {
896
+ const Context = React.createContext(null);
897
+ const Consumer = getConsumer(Context);
898
+
899
+ class App extends React.Component {
900
+ renderItem(id) {
901
+ return (
902
+ <span key={id}>
903
+ <Consumer>{() => <span>inner</span>}</Consumer>
904
+ <span>outer</span>
905
+ </span>
906
+ );
907
+ }
908
+ renderList() {
909
+ const list = [1, 2].map(id => this.renderItem(id));
910
+ if (this.props.reverse) {
911
+ list.reverse();
912
+ }
913
+ return list;
914
+ }
915
+ render() {
916
+ return (
917
+ <Context.Provider value={{}}>
918
+ {this.renderList()}
919
+ </Context.Provider>
920
+ );
921
+ }
922
+ }
923
667
- ReactNoop.render(<App foo={1} bar={1} />);
668
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
669
- expect(ReactNoop.getChildren()).toEqual([
670
- span('Foo: 1'),
671
- span('Bar: 1'),
672
- span('Foo: 1'),
673
- ]);
674
-
675
- // Update only foo
676
- ReactNoop.render(<App foo={2} bar={1} />);
677
- expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']);
678
- expect(ReactNoop.getChildren()).toEqual([
679
- span('Foo: 2'),
680
- span('Bar: 1'),
681
- span('Foo: 2'),
682
- ]);
683
-
684
- // Update only bar
685
- ReactNoop.render(<App foo={2} bar={2} />);
686
- expect(ReactNoop.flush()).toEqual(['Bar']);
687
- expect(ReactNoop.getChildren()).toEqual([
688
- span('Foo: 2'),
689
- span('Bar: 2'),
690
- span('Foo: 2'),
691
- ]);
692
-
693
- // Update both
694
- ReactNoop.render(<App foo={3} bar={3} />);
695
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
696
- expect(ReactNoop.getChildren()).toEqual([
697
- span('Foo: 3'),
698
- span('Bar: 3'),
699
- span('Foo: 3'),
700
- ]);
701
- });
924
+ ReactNoop.render(<App reverse={false} />);
925
+ ReactNoop.flush();
926
+ ReactNoop.render(<App reverse={true} />);
927
+ ReactNoop.flush();
928
+ ReactNoop.render(<App reverse={false} />);
929
+ ReactNoop.flush();
930
+ });
931
703
- it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
704
- const Context = React.createContext(
705
- 0,
706
- (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
707
- );
932
+ // This is a regression case for https://github.com/facebook/react/issues/12686
933
+ it('does not skip some siblings', () => {
934
+ const Context = React.createContext(0);
935
+ const ContextConsumer = getConsumer(Context);
936
709
- function App(props) {
710
- return <Context.Provider value={props.value} />;
711
- }
937
+ class App extends React.Component {
938
+ state = {
939
+ step: 0,
940
+ };
941
713
- ReactNoop.render(<App value={1} />);
714
- ReactNoop.flush();
942
+ render() {
943
+ ReactNoop.yield('App');
944
+ return (
945
+ <Context.Provider value={this.state.step}>
946
+ <StaticContent />
947
+ {this.state.step > 0 && <Indirection />}
948
+ </Context.Provider>
949
+ );
950
+ }
951
+ }
952
716
- // Update
717
- ReactNoop.render(<App value={2} />);
718
- expect(ReactNoop.flush).toWarnDev(
719
- 'calculateChangedBits: Expected the return value to be a 31-bit ' +
720
- 'integer. Instead received: 4294967295',
721
- );
722
- });
953
+ class StaticContent extends React.PureComponent {
954
+ render() {
955
+ return (
956
+ <React.Fragment>
957
+ <React.Fragment>
958
+ <span prop="static 1" />
959
+ <span prop="static 2" />
960
+ </React.Fragment>
961
+ </React.Fragment>
962
+ );
963
+ }
964
+ }
965
724
- it('warns if multiple renderers concurrently render the same context', () => {
725
- spyOnDev(console, 'error');
726
- const Context = React.createContext(0);
966
+ class Indirection extends React.PureComponent {
967
+ render() {
968
+ return (
969
+ <ContextConsumer>
970
+ {value => {
971
+ ReactNoop.yield('Consumer');
972
+ return <span prop={value} />;
973
+ }}
974
+ </ContextConsumer>
975
+ );
976
+ }
977
+ }
978
728
- function Foo(props) {
729
- ReactNoop.yield('Foo');
730
- return null;
731
- }
979
+ // Initial mount
980
+ let inst;
981
+ ReactNoop.render(<App ref={ref => (inst = ref)} />);
982
+ expect(ReactNoop.flush()).toEqual(['App']);
983
+ expect(ReactNoop.getChildren()).toEqual([
984
+ span('static 1'),
985
+ span('static 2'),
986
+ ]);
987
+ // Update the first time
988
+ inst.setState({step: 1});
989
+ expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
990
+ expect(ReactNoop.getChildren()).toEqual([
991
+ span('static 1'),
992
+ span('static 2'),
993
+ span(1),
994
+ ]);
995
+ // Update the second time
996
+ inst.setState({step: 2});
997
+ expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
998
+ expect(ReactNoop.getChildren()).toEqual([
999
+ span('static 1'),
1000
+ span('static 2'),
1001
+ span(2),
1002
+ ]);
1003
+ });
1004
+ });
1005
+ }
1006
733
- function App(props) {
734
- return (
735
- <Context.Provider value={props.value}>
736
- <Foo />
737
- <Foo />
738
- </Context.Provider>
1007
+ describe('Context.Provider', () => {
1008
+ it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
1009
+ const Context = React.createContext(
1010
+ 0,
1011
+ (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
1012
);
740
- }
1013
742
- ReactNoop.render(<App value={1} />);
743
- // Render past the Provider, but don't commit yet
744
- ReactNoop.flushThrough(['Foo']);
745
-
746
- // Get a new copy of ReactNoop
747
- jest.resetModules();
748
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
749
- React = require('react');
750
- ReactNoop = require('react-noop-renderer');
1014
+ function App(props) {
1015
+ return <Context.Provider value={props.value} />;
1016
+ }
1017
752
- // Render the provider again using a different renderer
753
- ReactNoop.render(<App value={1} />);
754
- ReactNoop.flush();
1018
+ ReactNoop.render(<App value={1} />);
1019
+ ReactNoop.flush();
1020
756
- if (__DEV__) {
757
- expect(console.error.calls.argsFor(0)[0]).toContain(
758
- 'Detected multiple renderers concurrently rendering the same ' +
759
- 'context provider. This is currently unsupported',
1021
+ // Update
1022
+ ReactNoop.render(<App value={2} />);
1023
+ expect(ReactNoop.flush).toWarnDev(
1024
+ 'calculateChangedBits: Expected the return value to be a 31-bit ' +
1025
+ 'integer. Instead received: 4294967295',
1026
);
761
- }
762
- });
1027
+ });
1028
764
- it('warns if consumer child is not a function', () => {
765
- spyOnDev(console, 'error');
766
- const Context = React.createContext(0);
767
- ReactNoop.render(<Context.Consumer />);
768
- expect(ReactNoop.flush).toThrow('render is not a function');
769
- if (__DEV__) {
770
- expect(console.error.calls.argsFor(0)[0]).toContain(
771
- 'A context consumer was rendered with multiple children, or a child ' +
772
- "that isn't a function",
773
- );
774
- }
775
- });
1029
+ it('warns if multiple renderers concurrently render the same context', () => {
1030
+ spyOnDev(console, 'error');
1031
+ const Context = React.createContext(0);
1032
777
- it("does not re-render if there's an update in a child", () => {
778
- const Context = React.createContext(0);
1033
+ function Foo(props) {
1034
+ ReactNoop.yield('Foo');
1035
+ return null;
1036
+ }
1037
780
- let child;
781
- class Child extends React.Component {
782
- state = {step: 0};
783
- render() {
784
- ReactNoop.yield('Child');
1038
+ function App(props) {
1039
return (
786
- <span
787
- prop={`Context: ${this.props.context}, Step: ${this.state.step}`}
788
- />
1040
+ <Context.Provider value={props.value}>
1041
+ <Foo />
1042
+ <Foo />
1043
+ </Context.Provider>
1044
);
1045
}
791
- }
792
-
793
- function App(props) {
794
- return (
795
- <Context.Provider value={props.value}>
796
- <Context.Consumer>
797
- {value => {
798
- ReactNoop.yield('Consumer render prop');
799
- return <Child ref={inst => (child = inst)} context={value} />;
800
- }}
801
- </Context.Consumer>
802
- </Context.Provider>
803
- );
804
- }
805
-
806
- // Initial mount
807
- ReactNoop.render(<App value={1} />);
808
- expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']);
809
- expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]);
810
-
811
- child.setState({step: 1});
812
- expect(ReactNoop.flush()).toEqual(['Child']);
813
- expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]);
814
- });
815
-
816
- it('provider bails out if children and value are unchanged (like sCU)', () => {
817
- const Context = React.createContext(0);
1046
819
- function Child() {
820
- ReactNoop.yield('Child');
821
- return <span prop="Child" />;
822
- }
823
-
824
- const children = <Child />;
825
-
826
- function App(props) {
827
- ReactNoop.yield('App');
828
- return (
829
- <Context.Provider value={props.value}>{children}</Context.Provider>
830
- );
831
- }
1047
+ ReactNoop.render(<App value={1} />);
1048
+ // Render past the Provider, but don't commit yet
1049
+ ReactNoop.flushThrough(['Foo']);
1050
833
- // Initial mount
834
- ReactNoop.render(<App value={1} />);
835
- expect(ReactNoop.flush()).toEqual(['App', 'Child']);
836
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
837
-
838
- // Update
839
- ReactNoop.render(<App value={1} />);
840
- expect(ReactNoop.flush()).toEqual([
841
- 'App',
842
- // Child does not re-render
843
- ]);
844
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
845
- });
1051
+ // Get a new copy of ReactNoop
1052
+ jest.resetModules();
1053
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
1054
+ React = require('react');
1055
+ ReactNoop = require('react-noop-renderer');
1056
847
- it('provider does not bail out if legacy context changed above', () => {
848
- const Context = React.createContext(0);
1057
+ // Render the provider again using a different renderer
1058
+ ReactNoop.render(<App value={1} />);
1059
+ ReactNoop.flush();
1060
850
- function Child() {
851
- ReactNoop.yield('Child');
852
- return <span prop="Child" />;
853
- }
1061
+ if (__DEV__) {
1062
+ expect(console.error.calls.argsFor(0)[0]).toContain(
1063
+ 'Detected multiple renderers concurrently rendering the same ' +
1064
+ 'context provider. This is currently unsupported',
1065
+ );
1066
+ }
1067
+ });
1068
855
- const children = <Child />;
1069
+ it('provider bails out if children and value are unchanged (like sCU)', () => {
1070
+ const Context = React.createContext(0);
1071
857
- class LegacyProvider extends React.Component {
858
- static childContextTypes = {
859
- legacyValue: () => {},
860
- };
861
- state = {legacyValue: 1};
862
- getChildContext() {
863
- return {legacyValue: this.state.legacyValue};
864
- }
865
- render() {
866
- ReactNoop.yield('LegacyProvider');
867
- return this.props.children;
1072
+ function Child() {
1073
+ ReactNoop.yield('Child');
1074
+ return <span prop="Child" />;
1075
}
869
- }
1076
871
- class App extends React.Component {
872
- state = {value: 1};
873
- render() {
1077
+ const children = <Child />;
1078
+
1079
+ function App(props) {
1080
ReactNoop.yield('App');
1081
return (
876
- <Context.Provider value={this.state.value}>
877
- {this.props.children}
878
- </Context.Provider>
1082
+ <Context.Provider value={props.value}>{children}</Context.Provider>
1083
);
1084
}
881
- }
882
-
883
- const legacyProviderRef = React.createRef();
884
- const appRef = React.createRef();
885
-
886
- // Initial mount
887
- ReactNoop.render(
888
- <LegacyProvider ref={legacyProviderRef}>
889
- <App ref={appRef} value={1}>
890
- {children}
891
- </App>
892
- </LegacyProvider>,
893
- );
894
- expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
895
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
896
-
897
- // Update App with same value (should bail out)
898
- appRef.current.setState({value: 1});
899
- expect(ReactNoop.flush()).toEqual(['App']);
900
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
901
-
902
- // Update LegacyProvider (should not bail out)
903
- legacyProviderRef.current.setState({value: 1});
904
- expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
905
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
906
-
907
- // Update App with same value (should bail out)
908
- appRef.current.setState({value: 1});
909
- expect(ReactNoop.flush()).toEqual(['App']);
910
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
911
- });
1085
913
- it('consumer bails out if value is unchanged and something above bailed out', () => {
914
- const Context = React.createContext(0);
1086
+ // Initial mount
1087
+ ReactNoop.render(<App value={1} />);
1088
+ expect(ReactNoop.flush()).toEqual(['App', 'Child']);
1089
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1090
+
1091
+ // Update
1092
+ ReactNoop.render(<App value={1} />);
1093
+ expect(ReactNoop.flush()).toEqual([
1094
+ 'App',
1095
+ // Child does not re-render
1096
+ ]);
1097
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1098
+ });
1099
916
- function renderChildValue(value) {
917
- ReactNoop.yield('Consumer');
918
- return <span prop={value} />;
919
- }
1100
+ it('provider does not bail out if legacy context changed above', () => {
1101
+ const Context = React.createContext(0);
1102
921
- function ChildWithInlineRenderCallback() {
922
- ReactNoop.yield('ChildWithInlineRenderCallback');
923
- // Note: we are intentionally passing an inline arrow. Don't refactor.
924
- return (
925
- <Context.Consumer>{value => renderChildValue(value)}</Context.Consumer>
926
- );
927
- }
1103
+ function Child() {
1104
+ ReactNoop.yield('Child');
1105
+ return <span prop="Child" />;
1106
+ }
1107
929
- function ChildWithCachedRenderCallback() {
930
- ReactNoop.yield('ChildWithCachedRenderCallback');
931
- return <Context.Consumer>{renderChildValue}</Context.Consumer>;
932
- }
1108
+ const children = <Child />;
1109
934
- class PureIndirection extends React.PureComponent {
935
- render() {
936
- ReactNoop.yield('PureIndirection');
937
- return (
938
- <React.Fragment>
939
- <ChildWithInlineRenderCallback />
940
- <ChildWithCachedRenderCallback />
941
- </React.Fragment>
942
- );
1110
+ class LegacyProvider extends React.Component {
1111
+ static childContextTypes = {
1112
+ legacyValue: () => {},
1113
+ };
1114
+ state = {legacyValue: 1};
1115
+ getChildContext() {
1116
+ return {legacyValue: this.state.legacyValue};
1117
+ }
1118
+ render() {
1119
+ ReactNoop.yield('LegacyProvider');
1120
+ return this.props.children;
1121
+ }
1122
}
944
- }
1123
946
- class App extends React.Component {
947
- render() {
948
- ReactNoop.yield('App');
949
- return (
950
- <Context.Provider value={this.props.value}>
951
- <PureIndirection />
952
- </Context.Provider>
953
- );
1124
+ class App extends React.Component {
1125
+ state = {value: 1};
1126
+ render() {
1127
+ ReactNoop.yield('App');
1128
+ return (
1129
+ <Context.Provider value={this.state.value}>
1130
+ {this.props.children}
1131
+ </Context.Provider>
1132
+ );
1133
+ }
1134
}
955
- }
1135
957
- // Initial mount
958
- ReactNoop.render(<App value={1} />);
959
- expect(ReactNoop.flush()).toEqual([
960
- 'App',
961
- 'PureIndirection',
962
- 'ChildWithInlineRenderCallback',
963
- 'Consumer',
964
- 'ChildWithCachedRenderCallback',
965
- 'Consumer',
966
- ]);
967
- expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
968
-
969
- // Update (bailout)
970
- ReactNoop.render(<App value={1} />);
971
- expect(ReactNoop.flush()).toEqual(['App']);
972
- expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
973
-
974
- // Update (no bailout)
975
- ReactNoop.render(<App value={2} />);
976
- expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
977
- expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
978
- });
979
-
980
- // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
981
- // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
982
- // If we bailed out on referential equality, it would be confusing that you
983
- // can call this.setState(), but an autobound render callback "blocked" the update.
984
- // https://github.com/facebook/react/pull/12470#issuecomment-376917711
985
- it('consumer does not bail out if there were no bailouts above it', () => {
986
- const Context = React.createContext(0);
1136
+ const legacyProviderRef = React.createRef();
1137
+ const appRef = React.createRef();
1138
988
- class App extends React.Component {
989
- state = {
990
- text: 'hello',
991
- };
992
-
993
- renderConsumer = context => {
994
- ReactNoop.yield('App#renderConsumer');
995
- return <span prop={this.state.text} />;
996
- };
1139
+ // Initial mount
1140
+ ReactNoop.render(
1141
+ <LegacyProvider ref={legacyProviderRef}>
1142
+ <App ref={appRef} value={1}>
1143
+ {children}
1144
+ </App>
1145
+ </LegacyProvider>,
1146
+ );
1147
+ expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
1148
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1149
+
1150
+ // Update App with same value (should bail out)
1151
+ appRef.current.setState({value: 1});
1152
+ expect(ReactNoop.flush()).toEqual(['App']);
1153
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1154
+
1155
+ // Update LegacyProvider (should not bail out)
1156
+ legacyProviderRef.current.setState({value: 1});
1157
+ expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
1158
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1159
+
1160
+ // Update App with same value (should bail out)
1161
+ appRef.current.setState({value: 1});
1162
+ expect(ReactNoop.flush()).toEqual(['App']);
1163
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1164
+ });
1165
+ });
1166
998
- render() {
999
- ReactNoop.yield('App');
1000
- return (
1001
- <Context.Provider value={this.props.value}>
1002
- <Context.Consumer>{this.renderConsumer}</Context.Consumer>
1003
- </Context.Provider>
1167
+ describe('Context.Consumer', () => {
1168
+ it('warns if child is not a function', () => {
1169
+ spyOnDev(console, 'error');
1170
+ const Context = React.createContext(0);
1171
+ ReactNoop.render(<Context.Consumer />);
1172
+ expect(ReactNoop.flush).toThrow('render is not a function');
1173
+ if (__DEV__) {
1174
+ expect(console.error.calls.argsFor(0)[0]).toContain(
1175
+ 'A context consumer was rendered with multiple children, or a child ' +
1176
+ "that isn't a function",
1177
);
1178
}
1006
- }
1007
-
1008
- // Initial mount
1009
- let inst;
1010
- ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1011
- expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1012
- expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1013
-
1014
- // Update
1015
- inst.setState({text: 'goodbye'});
1016
- expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1017
- expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1018
- });
1179
+ });
1180
1020
- // This is a regression case for https://github.com/facebook/react/issues/12389.
1021
- it('does not run into an infinite loop', () => {
1022
- const Context = React.createContext(null);
1181
+ it('can read other contexts inside consumer render prop', () => {
1182
+ const FooContext = React.createContext(0);
1183
+ const BarContext = React.createContext(0);
1184
1024
- class App extends React.Component {
1025
- renderItem(id) {
1185
+ function FooAndBar() {
1186
return (
1027
- <span key={id}>
1028
- <Context.Consumer>{() => <span>inner</span>}</Context.Consumer>
1029
- <span>outer</span>
1030
- </span>
1187
+ <FooContext>
1188
+ {foo => {
1189
+ const bar = BarContext.unstable_read();
1190
+ return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1191
+ }}
1192
+ </FooContext>
1193
);
1194
}
1033
- renderList() {
1034
- const list = [1, 2].map(id => this.renderItem(id));
1035
- if (this.props.reverse) {
1036
- list.reverse();
1195
+
1196
+ class Indirection extends React.Component {
1197
+ shouldComponentUpdate() {
1198
+ return false;
1199
+ }
1200
+ render() {
1201
+ return this.props.children;
1202
}
1038
- return list;
1203
}
1040
- render() {
1204
+
1205
+ function App(props) {
1206
return (
1042
- <Context.Provider value={{}}>{this.renderList()}</Context.Provider>
1207
+ <FooContext.Provider value={props.foo}>
1208
+ <BarContext.Provider value={props.bar}>
1209
+ <Indirection>
1210
+ <FooAndBar />
1211
+ </Indirection>
1212
+ </BarContext.Provider>
1213
+ </FooContext.Provider>
1214
);
1215
}
1045
- }
1216
1047
- ReactNoop.render(<App reverse={false} />);
1048
- ReactNoop.flush();
1049
- ReactNoop.render(<App reverse={true} />);
1050
- ReactNoop.flush();
1051
- ReactNoop.render(<App reverse={false} />);
1052
- ReactNoop.flush();
1053
- });
1217
+ ReactNoop.render(<App foo={1} bar={1} />);
1218
+ expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1']);
1219
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 1, Bar: 1')]);
1220
1055
- // This is a regression case for https://github.com/facebook/react/issues/12686
1056
- it('does not skip some siblings', () => {
1057
- const Context = React.createContext(0);
1221
+ // Update foo
1222
+ ReactNoop.render(<App foo={2} bar={1} />);
1223
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
1224
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 1')]);
1225
1059
- class App extends React.Component {
1060
- state = {
1061
- step: 0,
1062
- };
1226
+ // Update bar
1227
+ ReactNoop.render(<App foo={2} bar={2} />);
1228
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
1229
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 2')]);
1230
+ });
1231
+ });
1232
1064
- render() {
1065
- ReactNoop.yield('App');
1233
+ describe('unstable_readContext', () => {
1234
+ it('can use the same context multiple times in the same function', () => {
1235
+ const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
1236
+ let result = 0;
1237
+ if (a.foo !== b.foo) {
1238
+ result |= 0b001;
1239
+ }
1240
+ if (a.bar !== b.bar) {
1241
+ result |= 0b010;
1242
+ }
1243
+ if (a.baz !== b.baz) {
1244
+ result |= 0b100;
1245
+ }
1246
+ return result;
1247
+ });
1248
+
1249
+ function Provider(props) {
1250
return (
1067
- <Context.Provider value={this.state.step}>
1068
- <StaticContent />
1069
- {this.state.step > 0 && <Indirection />}
1251
+ <Context.Provider
1252
+ value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
1253
+ {props.children}
1254
</Context.Provider>
1255
);
1256
}
1073
- }
1257
1075
- class StaticContent extends React.PureComponent {
1076
- render() {
1077
- return (
1078
- <React.Fragment>
1079
- <React.Fragment>
1080
- <span prop="static 1" />
1081
- <span prop="static 2" />
1082
- </React.Fragment>
1083
- </React.Fragment>
1084
- );
1258
+ function FooAndBar() {
1259
+ const {foo} = Context.unstable_read(0b001);
1260
+ const {bar} = Context.unstable_read(0b010);
1261
+ return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1262
}
1086
- }
1263
1088
- class Indirection extends React.PureComponent {
1089
- render() {
1090
- return <Consumer />;
1264
+ function Baz() {
1265
+ const {baz} = Context.unstable_read(0b100);
1266
+ return <Text text={'Baz: ' + baz} />;
1267
}
1092
- }
1268
1094
- function Consumer() {
1095
- return (
1096
- <Context.Consumer>
1097
- {value => {
1098
- ReactNoop.yield('Consumer');
1099
- return <span prop={value} />;
1100
- }}
1101
- </Context.Consumer>
1102
- );
1103
- }
1269
+ class Indirection extends React.Component {
1270
+ shouldComponentUpdate() {
1271
+ return false;
1272
+ }
1273
+ render() {
1274
+ return this.props.children;
1275
+ }
1276
+ }
1277
1105
- // Initial mount
1106
- let inst;
1107
- ReactNoop.render(<App ref={ref => (inst = ref)} />);
1108
- expect(ReactNoop.flush()).toEqual(['App']);
1109
- expect(ReactNoop.getChildren()).toEqual([
1110
- span('static 1'),
1111
- span('static 2'),
1112
- ]);
1113
- // Update the first time
1114
- inst.setState({step: 1});
1115
- expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
1116
- expect(ReactNoop.getChildren()).toEqual([
1117
- span('static 1'),
1118
- span('static 2'),
1119
- span(1),
1120
- ]);
1121
- // Update the second time
1122
- inst.setState({step: 2});
1123
- expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
1124
- expect(ReactNoop.getChildren()).toEqual([
1125
- span('static 1'),
1126
- span('static 2'),
1127
- span(2),
1128
- ]);
1278
+ function App(props) {
1279
+ return (
1280
+ <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
1281
+ <Indirection>
1282
+ <Indirection>
1283
+ <FooAndBar />
1284
+ </Indirection>
1285
+ <Indirection>
1286
+ <Baz />
1287
+ </Indirection>
1288
+ </Indirection>
1289
+ </Provider>
1290
+ );
1291
+ }
1292
+
1293
+ ReactNoop.render(<App foo={1} bar={1} baz={1} />);
1294
+ expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']);
1295
+ expect(ReactNoop.getChildren()).toEqual([
1296
+ span('Foo: 1, Bar: 1'),
1297
+ span('Baz: 1'),
1298
+ ]);
1299
+
1300
+ // Update only foo
1301
+ ReactNoop.render(<App foo={2} bar={1} baz={1} />);
1302
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
1303
+ expect(ReactNoop.getChildren()).toEqual([
1304
+ span('Foo: 2, Bar: 1'),
1305
+ span('Baz: 1'),
1306
+ ]);
1307
+
1308
+ // Update only bar
1309
+ ReactNoop.render(<App foo={2} bar={2} baz={1} />);
1310
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
1311
+ expect(ReactNoop.getChildren()).toEqual([
1312
+ span('Foo: 2, Bar: 2'),
1313
+ span('Baz: 1'),
1314
+ ]);
1315
+
1316
+ // Update only baz
1317
+ ReactNoop.render(<App foo={2} bar={2} baz={2} />);
1318
+ expect(ReactNoop.flush()).toEqual(['Baz: 2']);
1319
+ expect(ReactNoop.getChildren()).toEqual([
1320
+ span('Foo: 2, Bar: 2'),
1321
+ span('Baz: 2'),
1322
+ ]);
1323
+ });
1324
});
1325
1326
it('unwinds after errors in complete phase', () => {