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

Fix context propagation for offscreen/fallback trees #23095

Merged
merged 7 commits into from Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Expand Up @@ -173,7 +173,7 @@ import {
checkIfContextChanged,
readContext,
prepareToReadContext,
scheduleWorkOnParentPath,
scheduleContextWorkOnParentPath,
} from './ReactFiberNewContext.new';
import {
renderWithHooks,
Expand Down Expand Up @@ -2754,13 +2754,15 @@ function updateDehydratedSuspenseComponent(
}
}

function scheduleWorkOnFiber(fiber: Fiber, renderLanes: Lanes) {
function scheduleSuspenseWorkOnFiber(fiber: Fiber, renderLanes: Lanes) {
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(fiber.return, renderLanes);
// Guaranteed to be non-empty since the fiber is not a root.
const parentFiber: Fiber = (fiber.return: any);
scheduleContextWorkOnParentPath(parentFiber, renderLanes, parentFiber);
gaearon marked this conversation as resolved.
Show resolved Hide resolved
}

function propagateSuspenseContextChange(
Expand All @@ -2776,15 +2778,15 @@ function propagateSuspenseContextChange(
if (node.tag === SuspenseComponent) {
const state: SuspenseState | null = node.memoizedState;
if (state !== null) {
scheduleWorkOnFiber(node, renderLanes);
scheduleSuspenseWorkOnFiber(node, renderLanes);
}
} else if (node.tag === SuspenseListComponent) {
// If the tail is hidden there might not be an Suspense boundaries
// to schedule work on. In this case we have to schedule it on the
// list itself.
// We don't have to traverse to the children of the list since
// the list will propagate the change when it rerenders.
scheduleWorkOnFiber(node, renderLanes);
scheduleSuspenseWorkOnFiber(node, renderLanes);
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
Expand Down
12 changes: 7 additions & 5 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Expand Up @@ -173,7 +173,7 @@ import {
checkIfContextChanged,
readContext,
prepareToReadContext,
scheduleWorkOnParentPath,
scheduleContextWorkOnParentPath,
} from './ReactFiberNewContext.old';
import {
renderWithHooks,
Expand Down Expand Up @@ -2754,13 +2754,15 @@ function updateDehydratedSuspenseComponent(
}
}

function scheduleWorkOnFiber(fiber: Fiber, renderLanes: Lanes) {
function scheduleSuspenseWorkOnFiber(fiber: Fiber, renderLanes: Lanes) {
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(fiber.return, renderLanes);
// Guaranteed to be non-empty since the fiber is not a root.
const parentFiber: Fiber = (fiber.return: any);
scheduleContextWorkOnParentPath(parentFiber, renderLanes, parentFiber);
gaearon marked this conversation as resolved.
Show resolved Hide resolved
}

function propagateSuspenseContextChange(
Expand All @@ -2776,15 +2778,15 @@ function propagateSuspenseContextChange(
if (node.tag === SuspenseComponent) {
const state: SuspenseState | null = node.memoizedState;
if (state !== null) {
scheduleWorkOnFiber(node, renderLanes);
scheduleSuspenseWorkOnFiber(node, renderLanes);
}
} else if (node.tag === SuspenseListComponent) {
// If the tail is hidden there might not be an Suspense boundaries
// to schedule work on. In this case we have to schedule it on the
// list itself.
// We don't have to traverse to the children of the list since
// the list will propagate the change when it rerenders.
scheduleWorkOnFiber(node, renderLanes);
scheduleSuspenseWorkOnFiber(node, renderLanes);
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
Expand Down
43 changes: 37 additions & 6 deletions packages/react-reconciler/src/ReactFiberNewContext.new.js
Expand Up @@ -138,9 +138,10 @@ export function popProvider(
}
}

export function scheduleWorkOnParentPath(
export function scheduleContextWorkOnParentPath(
parent: Fiber | null,
renderLanes: Lanes,
propagationRoot: Fiber,
) {
// Update the child lanes of all the ancestors, including the alternates.
let node = parent;
Expand All @@ -157,10 +158,24 @@ export function scheduleWorkOnParentPath(
) {
alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
} else {
// Neither alternate was updated, which means the rest of the
// Neither alternate was updated.
// Normally, this would mean that the rest of the
// ancestor path already has sufficient priority.
// However, this is not necessarily true inside offscreen
// or fallback trees because childLanes may be inconsistent
// with the surroundings. This is why we continue the loop.
}
if (node === propagationRoot) {
break;
}
if (__DEV__) {
if (node === propagationRoot.alternate) {
gaearon marked this conversation as resolved.
Show resolved Hide resolved
console.error(
'Did not expect to encounter a propagation root alternate when scheduling context work. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
node = node.return;
}
}
Expand Down Expand Up @@ -246,7 +261,11 @@ function propagateContextChange_eager<T>(
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(fiber.return, renderLanes);
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress,
);

// Mark the updated lanes on the list, too.
list.lanes = mergeLanes(list.lanes, renderLanes);
Expand Down Expand Up @@ -284,7 +303,11 @@ function propagateContextChange_eager<T>(
// because we want to schedule this fiber as having work
// on its children. We'll use the childLanes on
// this fiber to indicate that a context has changed.
scheduleWorkOnParentPath(parentSuspense, renderLanes);
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress,
);
nextFiber = fiber.sibling;
} else {
// Traverse down.
Expand Down Expand Up @@ -365,7 +388,11 @@ function propagateContextChanges<T>(
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(consumer.return, renderLanes);
scheduleContextWorkOnParentPath(
consumer.return,
renderLanes,
workInProgress,
);

if (!forcePropagateEntireTree) {
// During lazy propagation, when we find a match, we can defer
Expand Down Expand Up @@ -406,7 +433,11 @@ function propagateContextChanges<T>(
// because we want to schedule this fiber as having work
// on its children. We'll use the childLanes on
// this fiber to indicate that a context has changed.
scheduleWorkOnParentPath(parentSuspense, renderLanes);
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress,
);
nextFiber = null;
} else {
// Traverse down.
Expand Down
43 changes: 37 additions & 6 deletions packages/react-reconciler/src/ReactFiberNewContext.old.js
Expand Up @@ -138,9 +138,10 @@ export function popProvider(
}
}

export function scheduleWorkOnParentPath(
export function scheduleContextWorkOnParentPath(
parent: Fiber | null,
renderLanes: Lanes,
propagationRoot: Fiber,
) {
// Update the child lanes of all the ancestors, including the alternates.
let node = parent;
Expand All @@ -157,10 +158,24 @@ export function scheduleWorkOnParentPath(
) {
alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
} else {
// Neither alternate was updated, which means the rest of the
// Neither alternate was updated.
// Normally, this would mean that the rest of the
// ancestor path already has sufficient priority.
// However, this is not necessarily true inside offscreen
// or fallback trees because childLanes may be inconsistent
// with the surroundings. This is why we continue the loop.
}
if (node === propagationRoot) {
break;
}
if (__DEV__) {
if (node === propagationRoot.alternate) {
console.error(
'Did not expect to encounter a propagation root alternate when scheduling context work. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
node = node.return;
}
}
Expand Down Expand Up @@ -246,7 +261,11 @@ function propagateContextChange_eager<T>(
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(fiber.return, renderLanes);
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress,
);

// Mark the updated lanes on the list, too.
list.lanes = mergeLanes(list.lanes, renderLanes);
Expand Down Expand Up @@ -284,7 +303,11 @@ function propagateContextChange_eager<T>(
// because we want to schedule this fiber as having work
// on its children. We'll use the childLanes on
// this fiber to indicate that a context has changed.
scheduleWorkOnParentPath(parentSuspense, renderLanes);
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress,
);
nextFiber = fiber.sibling;
} else {
// Traverse down.
Expand Down Expand Up @@ -365,7 +388,11 @@ function propagateContextChanges<T>(
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(consumer.return, renderLanes);
scheduleContextWorkOnParentPath(
consumer.return,
renderLanes,
workInProgress,
);

if (!forcePropagateEntireTree) {
// During lazy propagation, when we find a match, we can defer
Expand Down Expand Up @@ -406,7 +433,11 @@ function propagateContextChanges<T>(
// because we want to schedule this fiber as having work
// on its children. We'll use the childLanes on
// this fiber to indicate that a context has changed.
scheduleWorkOnParentPath(parentSuspense, renderLanes);
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress,
);
nextFiber = null;
} else {
// Traverse down.
Expand Down
Expand Up @@ -1532,5 +1532,68 @@ describe('ReactSuspense', () => {
expect(Scheduler).toFlushUntilNextPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});

it('updates context consumer within child of suspended suspense component when context updates', () => {
const {createContext, useState} = React;

const ValueContext = createContext(null);

const promiseThatNeverResolves = new Promise(() => {});
function Child() {
return (
<ValueContext.Consumer>
{value => {
Scheduler.unstable_yieldValue(
`Received context value [${value}]`,
);
if (value === 'default') return <Text text="default" />;
throw promiseThatNeverResolves;
}}
</ValueContext.Consumer>
);
}

let setValue;
function Wrapper({children}) {
const [value, _setValue] = useState('default');
setValue = _setValue;
return (
<ValueContext.Provider value={value}>
{children}
</ValueContext.Provider>
);
}

function App() {
return (
<Wrapper>
<Suspense fallback={<Text text="Loading..." />}>
<Child />
</Suspense>
</Wrapper>
);
}

const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded([
'Received context value [default]',
'default',
]);
expect(root).toMatchRenderedOutput('default');

act(() => setValue('new value'));
expect(Scheduler).toHaveYielded([
'Received context value [new value]',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...');

act(() => setValue('default'));
expect(Scheduler).toHaveYielded([
'Received context value [default]',
'default',
]);
expect(root).toMatchRenderedOutput('default');
});
});
});