Context improvements (#10334)
Refactored fiber context to fix a nested updates bug
Brian Vaughn committed
Aug 1, 2017 at 08:38 UTC
d947654913527b85a7eba3da5cf7d75eae43c1d6
3 files changed
+293
-18
src/renderers/shared/fiber/ReactFiberBeginWork.js
+7
-1
@@ -277,6 +277,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
277
markRef(current, workInProgress);
278
279
if (!shouldUpdate) {
280
+ // Context providers should defer to sCU for rendering
281
+ if (hasContext) {
282
+ invalidateContextProvider(workInProgress, false);
283
+ }
284
+
285
return bailoutOnAlreadyFinishedWork(current, workInProgress);
286
}
287
@@ -302,8 +307,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
307
308
// The context might have changed so we need to recalculate it.
309
if (hasContext) {
305
- invalidateContextProvider(workInProgress);
310
+ invalidateContextProvider(workInProgress, true);
311
}
312
+
313
return workInProgress.child;
314
}
315
src/renderers/shared/fiber/ReactFiberContext.js
+32
-17
@@ -234,14 +234,22 @@ exports.pushContextProvider = function(workInProgress: Fiber): boolean {
234
emptyObject;
235
236
// Remember the parent context so we can merge with it later.
237
+ // Inherit the parent's did-perform-work value to avoid inadvertantly blocking updates.
238
previousContext = contextStackCursor.current;
239
push(contextStackCursor, memoizedMergedChildContext, workInProgress);
239
- push(didPerformWorkStackCursor, false, workInProgress);
240
+ push(
241
+ didPerformWorkStackCursor,
242
+ didPerformWorkStackCursor.current,
243
+ workInProgress,
244
+ );
245
246
return true;
247
};
248
244
-exports.invalidateContextProvider = function(workInProgress: Fiber): void {
249
+exports.invalidateContextProvider = function(
250
+ workInProgress: Fiber,
251
+ didChange: boolean,
252
+): void {
253
const instance = workInProgress.stateNode;
254
invariant(
255
instance,
@@ -249,21 +257,28 @@ exports.invalidateContextProvider = function(workInProgress: Fiber): void {
257
'This error is likely caused by a bug in React. Please file an issue.',
258
);
259
252
- // Merge parent and own context.
253
- const mergedContext = processChildContext(
254
- workInProgress,
255
- previousContext,
256
- true,
257
- );
258
- instance.__reactInternalMemoizedMergedChildContext = mergedContext;
259
-
260
- // Replace the old (or empty) context with the new one.
261
- // It is important to unwind the context in the reverse order.
262
- pop(didPerformWorkStackCursor, workInProgress);
263
- pop(contextStackCursor, workInProgress);
264
- // Now push the new context and mark that it has changed.
265
- push(contextStackCursor, mergedContext, workInProgress);
266
- push(didPerformWorkStackCursor, true, workInProgress);
260
+ if (didChange) {
261
+ // Merge parent and own context.
262
+ // Skip this if we're not updating due to sCU.
263
+ // This avoids unnecessarily recomputing memoized values.
264
+ const mergedContext = processChildContext(
265
+ workInProgress,
266
+ previousContext,
267
+ true,
268
+ );
269
+ instance.__reactInternalMemoizedMergedChildContext = mergedContext;
270
+
271
+ // Replace the old (or empty) context with the new one.
272
+ // It is important to unwind the context in the reverse order.
273
+ pop(didPerformWorkStackCursor, workInProgress);
274
+ pop(contextStackCursor, workInProgress);
275
+ // Now push the new context and mark that it has changed.
276
+ push(contextStackCursor, mergedContext, workInProgress);
277
+ push(didPerformWorkStackCursor, didChange, workInProgress);
278
+ } else {
279
+ pop(didPerformWorkStackCursor, workInProgress);
280
+ push(didPerformWorkStackCursor, didChange, workInProgress);
281
+ }
282
};
283
284
exports.resetContext = function(): void {
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+254
@@ -2395,4 +2395,258 @@ describe('ReactIncremental', () => {
2395
expect(cduNextProps).toEqual([{children: 'B'}]);
2396
},
2397
);
2398
+
2399
+ it('updates descendants with new context values', () => {
2400
+ let rendered = [];
2401
+ let instance;
2402
+
2403
+ class TopContextProvider extends React.Component {
2404
+ static childContextTypes = {
2405
+ count: PropTypes.number,
2406
+ };
2407
+ constructor() {
2408
+ super();
2409
+ this.state = {count: 0};
2410
+ instance = this;
2411
+ }
2412
+ getChildContext = () => ({
2413
+ count: this.state.count,
2414
+ });
2415
+ render = () => this.props.children;
2416
+ updateCount = () =>
2417
+ this.setState(state => ({
2418
+ count: state.count + 1,
2419
+ }));
2420
+ }
2421
+
2422
+ class Middle extends React.Component {
2423
+ render = () => this.props.children;
2424
+ }
2425
+
2426
+ class Child extends React.Component {
2427
+ static contextTypes = {
2428
+ count: PropTypes.number,
2429
+ };
2430
+ render = () => {
2431
+ rendered.push(`count:${this.context.count}`);
2432
+ return null;
2433
+ };
2434
+ }
2435
+
2436
+ ReactNoop.render(
2437
+ <TopContextProvider><Middle><Child /></Middle></TopContextProvider>,
2438
+ );
2439
+
2440
+ ReactNoop.flush();
2441
+ expect(rendered).toEqual(['count:0']);
2442
+ instance.updateCount();
2443
+ ReactNoop.flush();
2444
+ expect(rendered).toEqual(['count:0', 'count:1']);
2445
+ });
2446
+
2447
+ it('updates descendants with multiple context-providing ancestors with new context values', () => {
2448
+ let rendered = [];
2449
+ let instance;
2450
+
2451
+ class TopContextProvider extends React.Component {
2452
+ static childContextTypes = {
2453
+ count: PropTypes.number,
2454
+ };
2455
+ constructor() {
2456
+ super();
2457
+ this.state = {count: 0};
2458
+ instance = this;
2459
+ }
2460
+ getChildContext = () => ({
2461
+ count: this.state.count,
2462
+ });
2463
+ render = () => this.props.children;
2464
+ updateCount = () =>
2465
+ this.setState(state => ({
2466
+ count: state.count + 1,
2467
+ }));
2468
+ }
2469
+
2470
+ class MiddleContextProvider extends React.Component {
2471
+ static childContextTypes = {
2472
+ name: PropTypes.string,
2473
+ };
2474
+ getChildContext = () => ({
2475
+ name: 'brian',
2476
+ });
2477
+ render = () => this.props.children;
2478
+ }
2479
+
2480
+ class Child extends React.Component {
2481
+ static contextTypes = {
2482
+ count: PropTypes.number,
2483
+ };
2484
+ render = () => {
2485
+ rendered.push(`count:${this.context.count}`);
2486
+ return null;
2487
+ };
2488
+ }
2489
+
2490
+ ReactNoop.render(
2491
+ <TopContextProvider>
2492
+ <MiddleContextProvider>
2493
+ <Child />
2494
+ </MiddleContextProvider>
2495
+ </TopContextProvider>,
2496
+ );
2497
+
2498
+ ReactNoop.flush();
2499
+ expect(rendered).toEqual(['count:0']);
2500
+ instance.updateCount();
2501
+ ReactNoop.flush();
2502
+ expect(rendered).toEqual(['count:0', 'count:1']);
2503
+ });
2504
+
2505
+ it('should not update descendants with new context values if shouldComponentUpdate returns false', () => {
2506
+ let rendered = [];
2507
+ let instance;
2508
+
2509
+ class TopContextProvider extends React.Component {
2510
+ static childContextTypes = {
2511
+ count: PropTypes.number,
2512
+ };
2513
+ constructor() {
2514
+ super();
2515
+ this.state = {count: 0};
2516
+ instance = this;
2517
+ }
2518
+ getChildContext = () => ({
2519
+ count: this.state.count,
2520
+ });
2521
+ render = () => this.props.children;
2522
+ updateCount = () =>
2523
+ this.setState(state => ({
2524
+ count: state.count + 1,
2525
+ }));
2526
+ }
2527
+
2528
+ class MiddleScu extends React.Component {
2529
+ shouldComponentUpdate() {
2530
+ return false;
2531
+ }
2532
+ render = () => this.props.children;
2533
+ }
2534
+
2535
+ class MiddleContextProvider extends React.Component {
2536
+ static childContextTypes = {
2537
+ name: PropTypes.string,
2538
+ };
2539
+ getChildContext = () => ({
2540
+ name: 'brian',
2541
+ });
2542
+ render = () => this.props.children;
2543
+ }
2544
+
2545
+ class Child extends React.Component {
2546
+ static contextTypes = {
2547
+ count: PropTypes.number,
2548
+ };
2549
+ render = () => {
2550
+ rendered.push(`count:${this.context.count}`);
2551
+ return null;
2552
+ };
2553
+ }
2554
+
2555
+ ReactNoop.render(
2556
+ <TopContextProvider>
2557
+ <MiddleScu>
2558
+ <MiddleContextProvider><Child /></MiddleContextProvider>
2559
+ </MiddleScu>
2560
+ </TopContextProvider>,
2561
+ );
2562
+
2563
+ ReactNoop.flush();
2564
+ expect(rendered).toEqual(['count:0']);
2565
+ instance.updateCount();
2566
+ ReactNoop.flush();
2567
+ expect(rendered).toEqual(['count:0']);
2568
+ });
2569
+
2570
+ it('should update descendants with new context values if setState() is called in the middle of the tree', () => {
2571
+ let rendered = [];
2572
+ let middleInstance;
2573
+ let topInstance;
2574
+
2575
+ class TopContextProvider extends React.Component {
2576
+ static childContextTypes = {
2577
+ count: PropTypes.number,
2578
+ };
2579
+ constructor() {
2580
+ super();
2581
+ this.state = {count: 0};
2582
+ topInstance = this;
2583
+ }
2584
+ getChildContext = () => ({
2585
+ count: this.state.count,
2586
+ });
2587
+ render = () => this.props.children;
2588
+ updateCount = () =>
2589
+ this.setState(state => ({
2590
+ count: state.count + 1,
2591
+ }));
2592
+ }
2593
+
2594
+ class MiddleScu extends React.Component {
2595
+ shouldComponentUpdate() {
2596
+ return false;
2597
+ }
2598
+ render = () => this.props.children;
2599
+ }
2600
+
2601
+ class MiddleContextProvider extends React.Component {
2602
+ static childContextTypes = {
2603
+ name: PropTypes.string,
2604
+ };
2605
+ constructor() {
2606
+ super();
2607
+ this.state = {name: 'brian'};
2608
+ middleInstance = this;
2609
+ }
2610
+ getChildContext = () => ({
2611
+ name: this.state.name,
2612
+ });
2613
+ updateName = name => {
2614
+ this.setState({name});
2615
+ };
2616
+ render = () => this.props.children;
2617
+ }
2618
+
2619
+ class Child extends React.Component {
2620
+ static contextTypes = {
2621
+ count: PropTypes.number,
2622
+ name: PropTypes.string,
2623
+ };
2624
+ render = () => {
2625
+ rendered.push(`count:${this.context.count}, name:${this.context.name}`);
2626
+ return null;
2627
+ };
2628
+ }
2629
+
2630
+ ReactNoop.render(
2631
+ <TopContextProvider>
2632
+ <MiddleScu>
2633
+ <MiddleContextProvider>
2634
+ <Child />
2635
+ </MiddleContextProvider>
2636
+ </MiddleScu>
2637
+ </TopContextProvider>,
2638
+ );
2639
+
2640
+ ReactNoop.flush();
2641
+ expect(rendered).toEqual(['count:0, name:brian']);
2642
+ topInstance.updateCount();
2643
+ ReactNoop.flush();
2644
+ expect(rendered).toEqual(['count:0, name:brian']);
2645
+ middleInstance.updateName('not brian');
2646
+ ReactNoop.flush();
2647
+ expect(rendered).toEqual([
2648
+ 'count:0, name:brian',
2649
+ 'count:1, name:not brian',
2650
+ ]);
2651
+ });
2652
});