Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] Lazily propagate context changes #20890

Merged
merged 5 commits into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,15 @@ describe('ReactLegacyContextDisabled', () => {
container,
);
expect(container.textContent).toBe('bbb');
expect(lifecycleContextLog).toEqual(['b', 'b']); // sCU skipped due to changed context value.
if (gate(flags => flags.enableLazyContextPropagation)) {
// In the lazy propagation implementation, we don't check if context
// changed until after shouldComponentUpdate is run.
expect(lifecycleContextLog).toEqual(['b', 'b', 'b']);
} else {
// In the eager implementation, a dirty flag was set when the parent
// changed, so we skipped sCU.
expect(lifecycleContextLog).toEqual(['b', 'b']);
}
ReactDOM.unmountComponentAtNode(container);
});
});
134 changes: 111 additions & 23 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableCache,
enableLazyContextPropagation,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
Expand Down Expand Up @@ -154,6 +155,9 @@ import {findFirstSuspended} from './ReactFiberSuspenseComponent.new';
import {
pushProvider,
propagateContextChange,
lazilyPropagateParentContextChanges,
propagateParentContextChangesToDeferredTree,
checkIfContextChanged,
readContext,
prepareToReadContext,
calculateChangedBits,
Expand Down Expand Up @@ -646,6 +650,18 @@ function updateOffscreenComponent(
// We're about to bail out, but we need to push this to the stack anyway
// to avoid a push/pop misalignment.
pushRenderLanes(workInProgress, nextBaseLanes);

if (enableLazyContextPropagation && current !== null) {
// Since this tree will resume rendering in a separate render, we need
// to propagate parent contexts now so we don't lose track of which
// ones changed.
propagateParentContextChangesToDeferredTree(
current,
workInProgress,
renderLanes,
);
}

return null;
} else {
// This is the second render. The surrounding visible content has already
Expand Down Expand Up @@ -2445,6 +2461,19 @@ function updateDehydratedSuspenseComponent(
renderLanes,
);
}

if (
enableLazyContextPropagation &&
// TODO: Factoring is a little weird, since we check this right below, too.
// But don't want to re-arrange the if-else chain until/unless this
// feature lands.
!didReceiveUpdate
) {
// We need to check if any children have context before we decide to bail
// out, so propagate the changes now.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
}

// We use lanes to indicate that a child might depend on context, so if
// any context has changed, we need to treat is as if the input might have changed.
const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
Expand Down Expand Up @@ -2970,25 +2999,37 @@ function updateContextProvider(

pushProvider(workInProgress, context, newValue);

if (oldProps !== null) {
const oldValue = oldProps.value;
const changedBits = calculateChangedBits(context, newValue, oldValue);
if (changedBits === 0) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
if (enableLazyContextPropagation) {
// In the lazy propagation implementation, we don't scan for matching
// consumers until something bails out, because until something bails out
// we're going to visit those nodes, anyway. The trade-off is that it shifts
// responsibility to the consumer to track whether something has changed.
} else {
if (oldProps !== null) {
const oldValue = oldProps.value;
const changedBits = calculateChangedBits(context, newValue, oldValue);
if (changedBits === 0) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(
workInProgress,
context,
changedBits,
renderLanes,
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, changedBits, renderLanes);
}
}

Expand Down Expand Up @@ -3074,6 +3115,10 @@ export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}

export function checkIfWorkInProgressReceivedUpdate() {
return didReceiveUpdate;
}

function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
Expand All @@ -3096,13 +3141,23 @@ function bailoutOnAlreadyFinishedWork(
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
return null;
} else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;

if (enableLazyContextPropagation && current !== null) {
// Before bailing out, check if there are any context changes in
// the children.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
return null;
}
}

// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}

function remountFiber(
Expand Down Expand Up @@ -3171,7 +3226,7 @@ function beginWork(
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;
let updateLanes = workInProgress.lanes;

if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
Expand All @@ -3192,6 +3247,17 @@ function beginWork(
}

if (current !== null) {
// TODO: The factoring of this block is weird.
if (
enableLazyContextPropagation &&
!includesSomeLane(renderLanes, updateLanes)
) {
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
updateLanes = mergeLanes(updateLanes, renderLanes);
}
}

const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

Expand Down Expand Up @@ -3312,6 +3378,9 @@ function beginWork(
// primary children and work on the fallback.
return child.sibling;
} else {
// Note: We can return `null` here because we already checked
// whether there were nested context consumers, via the call to
// `bailoutOnAlreadyFinishedWork` above.
return null;
}
}
Expand All @@ -3326,11 +3395,30 @@ function beginWork(
case SuspenseListComponent: {
const didSuspendBefore = (current.flags & DidCapture) !== NoFlags;

const hasChildWork = includesSomeLane(
let hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);

if (enableLazyContextPropagation && !hasChildWork) {
// Context changes may not have been propagated yet. We need to do
// that now, before we can decide whether to bail out.
// TODO: We use `childLanes` as a heuristic for whether there is
// remaining work in a few places, including
// `bailoutOnAlreadyFinishedWork` and
// `updateDehydratedSuspenseComponent`. We should maybe extract this
// into a dedicated function.
lazilyPropagateParentContextChanges(
current,
workInProgress,
renderLanes,
);
hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);
}

if (didSuspendBefore) {
if (hasChildWork) {
// If something was in fallback state last time, and we have all the
Expand Down
Loading