Skip to content

Commit d947654

Browse files
authored
Context improvements (#10334)
Refactored fiber context to fix a nested updates bug
1 parent 7816527 commit d947654

File tree

3 files changed

+293
-18
lines changed

3 files changed

+293
-18
lines changed

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
277277
markRef(current, workInProgress);
278278

279279
if (!shouldUpdate) {
280+
// Context providers should defer to sCU for rendering
281+
if (hasContext) {
282+
invalidateContextProvider(workInProgress, false);
283+
}
284+
280285
return bailoutOnAlreadyFinishedWork(current, workInProgress);
281286
}
282287

@@ -302,8 +307,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
302307

303308
// The context might have changed so we need to recalculate it.
304309
if (hasContext) {
305-
invalidateContextProvider(workInProgress);
310+
invalidateContextProvider(workInProgress, true);
306311
}
312+
307313
return workInProgress.child;
308314
}
309315

src/renderers/shared/fiber/ReactFiberContext.js

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -234,36 +234,51 @@ exports.pushContextProvider = function(workInProgress: Fiber): boolean {
234234
emptyObject;
235235

236236
// 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.
237238
previousContext = contextStackCursor.current;
238239
push(contextStackCursor, memoizedMergedChildContext, workInProgress);
239-
push(didPerformWorkStackCursor, false, workInProgress);
240+
push(
241+
didPerformWorkStackCursor,
242+
didPerformWorkStackCursor.current,
243+
workInProgress,
244+
);
240245

241246
return true;
242247
};
243248

244-
exports.invalidateContextProvider = function(workInProgress: Fiber): void {
249+
exports.invalidateContextProvider = function(
250+
workInProgress: Fiber,
251+
didChange: boolean,
252+
): void {
245253
const instance = workInProgress.stateNode;
246254
invariant(
247255
instance,
248256
'Expected to have an instance by this point. ' +
249257
'This error is likely caused by a bug in React. Please file an issue.',
250258
);
251259

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+
}
267282
};
268283

269284
exports.resetContext = function(): void {

src/renderers/shared/fiber/__tests__/ReactIncremental-test.js

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,4 +2395,258 @@ describe('ReactIncremental', () => {
23952395
expect(cduNextProps).toEqual([{children: 'B'}]);
23962396
},
23972397
);
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+
});
23982652
});

0 commit comments

Comments
 (0)