@samitouri / QOS-React / commits / d947654913

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