Skip to content

Commit 258b375

Browse files
committed
Move context comparison to consumer
In the lazy context implementation, not all context changes are propagated from the provider, so we can't rely on the propagation alone to mark the consumer as dirty. The consumer needs to compare to the previous value, like we do for state and context. I added a `memoizedValue` field to the context dependency type. Then in the consumer, we iterate over the current dependencies to see if something changed. We only do this iteration after props and state has already bailed out, so it's a relatively uncommon path, except at the root of a changed subtree. Alternatively, we could move these comparisons into `readContext`, but that's a much hotter path, so I think this is an appropriate trade off.
1 parent 7df6572 commit 258b375

22 files changed

+507
-83
lines changed

packages/react-dom/src/__tests__/ReactLegacyContextDisabled-test.internal.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,15 @@ describe('ReactLegacyContextDisabled', () => {
226226
container,
227227
);
228228
expect(container.textContent).toBe('bbb');
229-
expect(lifecycleContextLog).toEqual(['b', 'b']); // sCU skipped due to changed context value.
229+
if (gate(flags => flags.enableLazyContextPropagation)) {
230+
// In the lazy propagation implementation, we don't check if context
231+
// changed until after shouldComponentUpdate is run.
232+
expect(lifecycleContextLog).toEqual(['b', 'b', 'b']);
233+
} else {
234+
// In the eager implementation, a dirty flag was set when the parent
235+
// changed, so we skipped sCU.
236+
expect(lifecycleContextLog).toEqual(['b', 'b']);
237+
}
230238
ReactDOM.unmountComponentAtNode(container);
231239
});
232240
});

packages/react-reconciler/src/ReactFiberBeginWork.new.js

+4
Original file line numberDiff line numberDiff line change
@@ -3074,6 +3074,10 @@ export function markWorkInProgressReceivedUpdate() {
30743074
didReceiveUpdate = true;
30753075
}
30763076

3077+
export function checkIfWorkInProgressReceivedUpdate() {
3078+
return didReceiveUpdate;
3079+
}
3080+
30773081
function bailoutOnAlreadyFinishedWork(
30783082
current: Fiber | null,
30793083
workInProgress: Fiber,

packages/react-reconciler/src/ReactFiberBeginWork.old.js

+4
Original file line numberDiff line numberDiff line change
@@ -3074,6 +3074,10 @@ export function markWorkInProgressReceivedUpdate() {
30743074
didReceiveUpdate = true;
30753075
}
30763076

3077+
export function checkIfWorkInProgressReceivedUpdate() {
3078+
return didReceiveUpdate;
3079+
}
3080+
30773081
function bailoutOnAlreadyFinishedWork(
30783082
current: Fiber | null,
30793083
workInProgress: Fiber,

packages/react-reconciler/src/ReactFiberClassComponent.new.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
enableSchedulingProfiler,
2121
warnAboutDeprecatedLifecycles,
2222
enableStrictEffects,
23+
enableLazyContextPropagation,
2324
} from 'shared/ReactFeatureFlags';
2425
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
2526
import {isMounted} from './ReactFiberTreeReflection';
@@ -58,7 +59,7 @@ import {
5859
hasContextChanged,
5960
emptyContextObject,
6061
} from './ReactFiberContext.new';
61-
import {readContext} from './ReactFiberNewContext.new';
62+
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
6263
import {
6364
requestEventTime,
6465
requestUpdateLane,
@@ -1150,7 +1151,13 @@ function updateClassInstance(
11501151
unresolvedOldProps === unresolvedNewProps &&
11511152
oldState === newState &&
11521153
!hasContextChanged() &&
1153-
!checkHasForceUpdateAfterProcessing()
1154+
!checkHasForceUpdateAfterProcessing() &&
1155+
!(
1156+
enableLazyContextPropagation &&
1157+
current !== null &&
1158+
current.dependencies !== null &&
1159+
checkIfContextChanged(current.dependencies)
1160+
)
11541161
) {
11551162
// If an update was already in progress, we should schedule an Update
11561163
// effect even though we're bailing out, so that cWU/cDU are called.
@@ -1193,7 +1200,15 @@ function updateClassInstance(
11931200
oldState,
11941201
newState,
11951202
nextContext,
1196-
);
1203+
) ||
1204+
// TODO: In some cases, we'll end up checking if context has changed twice,
1205+
// both before and after `shouldComponentUpdate` has been called. Not ideal,
1206+
// but I'm loath to refactor this function. This only happens for memoized
1207+
// components so it's not that common.
1208+
(enableLazyContextPropagation &&
1209+
current !== null &&
1210+
current.dependencies !== null &&
1211+
checkIfContextChanged(current.dependencies));
11971212

11981213
if (shouldUpdate) {
11991214
// In order to support react-lifecycles-compat polyfilled components,

packages/react-reconciler/src/ReactFiberClassComponent.old.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
enableSchedulingProfiler,
2121
warnAboutDeprecatedLifecycles,
2222
enableStrictEffects,
23+
enableLazyContextPropagation,
2324
} from 'shared/ReactFeatureFlags';
2425
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
2526
import {isMounted} from './ReactFiberTreeReflection';
@@ -58,7 +59,7 @@ import {
5859
hasContextChanged,
5960
emptyContextObject,
6061
} from './ReactFiberContext.old';
61-
import {readContext} from './ReactFiberNewContext.old';
62+
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old';
6263
import {
6364
requestEventTime,
6465
requestUpdateLane,
@@ -1150,7 +1151,13 @@ function updateClassInstance(
11501151
unresolvedOldProps === unresolvedNewProps &&
11511152
oldState === newState &&
11521153
!hasContextChanged() &&
1153-
!checkHasForceUpdateAfterProcessing()
1154+
!checkHasForceUpdateAfterProcessing() &&
1155+
!(
1156+
enableLazyContextPropagation &&
1157+
current !== null &&
1158+
current.dependencies !== null &&
1159+
checkIfContextChanged(current.dependencies)
1160+
)
11541161
) {
11551162
// If an update was already in progress, we should schedule an Update
11561163
// effect even though we're bailing out, so that cWU/cDU are called.
@@ -1193,7 +1200,15 @@ function updateClassInstance(
11931200
oldState,
11941201
newState,
11951202
nextContext,
1196-
);
1203+
) ||
1204+
// TODO: In some cases, we'll end up checking if context has changed twice,
1205+
// both before and after `shouldComponentUpdate` has been called. Not ideal,
1206+
// but I'm loath to refactor this function. This only happens for memoized
1207+
// components so it's not that common.
1208+
(enableLazyContextPropagation &&
1209+
current !== null &&
1210+
current.dependencies !== null &&
1211+
checkIfContextChanged(current.dependencies));
11971212

11981213
if (shouldUpdate) {
11991214
// In order to support react-lifecycles-compat polyfilled components,

packages/react-reconciler/src/ReactFiberHooks.new.js

+27-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
decoupleUpdatePriorityFromScheduler,
3131
enableUseRefAccessWarning,
3232
enableStrictEffects,
33+
enableLazyContextPropagation,
3334
} from 'shared/ReactFeatureFlags';
3435

3536
import {
@@ -54,7 +55,7 @@ import {
5455
higherLanePriority,
5556
DefaultLanePriority,
5657
} from './ReactFiberLane.new';
57-
import {readContext} from './ReactFiberNewContext.new';
58+
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
5859
import {HostRoot, CacheComponent} from './ReactWorkTags';
5960
import {
6061
Update as UpdateEffect,
@@ -83,7 +84,10 @@ import {
8384
import invariant from 'shared/invariant';
8485
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
8586
import is from 'shared/objectIs';
86-
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new';
87+
import {
88+
markWorkInProgressReceivedUpdate,
89+
checkIfWorkInProgressReceivedUpdate,
90+
} from './ReactFiberBeginWork.new';
8791
import {
8892
UserBlockingPriority,
8993
NormalPriority,
@@ -496,6 +500,27 @@ export function renderWithHooks<Props, SecondArg>(
496500
'early return statement.',
497501
);
498502

503+
if (enableLazyContextPropagation) {
504+
if (current !== null) {
505+
if (!checkIfWorkInProgressReceivedUpdate()) {
506+
// If there were no changes to props or state, we need to check if there
507+
// was a context change. We didn't already do this because there's no
508+
// 1:1 correspondence between dependencies and hooks. Although, because
509+
// there almost always is in the common case (`readContext` is an
510+
// internal API), we could compare in there. OTOH, we only hit this case
511+
// if everything else bails out, so on the whole it might be better to
512+
// keep the comparison out of the common path.
513+
const currentDependencies = current.dependencies;
514+
if (
515+
currentDependencies !== null &&
516+
checkIfContextChanged(currentDependencies)
517+
) {
518+
markWorkInProgressReceivedUpdate();
519+
}
520+
}
521+
}
522+
}
523+
499524
return children;
500525
}
501526

packages/react-reconciler/src/ReactFiberHooks.old.js

+27-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
decoupleUpdatePriorityFromScheduler,
3131
enableUseRefAccessWarning,
3232
enableStrictEffects,
33+
enableLazyContextPropagation,
3334
} from 'shared/ReactFeatureFlags';
3435

3536
import {
@@ -54,7 +55,7 @@ import {
5455
higherLanePriority,
5556
DefaultLanePriority,
5657
} from './ReactFiberLane.old';
57-
import {readContext} from './ReactFiberNewContext.old';
58+
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old';
5859
import {HostRoot, CacheComponent} from './ReactWorkTags';
5960
import {
6061
Update as UpdateEffect,
@@ -83,7 +84,10 @@ import {
8384
import invariant from 'shared/invariant';
8485
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
8586
import is from 'shared/objectIs';
86-
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.old';
87+
import {
88+
markWorkInProgressReceivedUpdate,
89+
checkIfWorkInProgressReceivedUpdate,
90+
} from './ReactFiberBeginWork.old';
8791
import {
8892
UserBlockingPriority,
8993
NormalPriority,
@@ -496,6 +500,27 @@ export function renderWithHooks<Props, SecondArg>(
496500
'early return statement.',
497501
);
498502

503+
if (enableLazyContextPropagation) {
504+
if (current !== null) {
505+
if (!checkIfWorkInProgressReceivedUpdate()) {
506+
// If there were no changes to props or state, we need to check if there
507+
// was a context change. We didn't already do this because there's no
508+
// 1:1 correspondence between dependencies and hooks. Although, because
509+
// there almost always is in the common case (`readContext` is an
510+
// internal API), we could compare in there. OTOH, we only hit this case
511+
// if everything else bails out, so on the whole it might be better to
512+
// keep the comparison out of the common path.
513+
const currentDependencies = current.dependencies;
514+
if (
515+
currentDependencies !== null &&
516+
checkIfContextChanged(currentDependencies)
517+
) {
518+
markWorkInProgressReceivedUpdate();
519+
}
520+
}
521+
}
522+
}
523+
499524
return children;
500525
}
501526

0 commit comments

Comments
 (0)