Skip to content

Commit

Permalink
Bugfix: Legacy Mode + DevTools "force fallback" (#19164)
Browse files Browse the repository at this point in the history
DevTools has a feature to force a Suspense boundary to show a fallback.
This feature causes us to skip the first render pass (where we render
the primary children) and go straight to rendering the fallback.

There's a Legacy Mode-only codepath that failed to take this scenario
into account, instead assuming that whenever a fallback is being
rendered, it was preceded by an attempt to render the primary children.

SuspenseList can also cause us to skip the first pass, but the relevant
branch is Legacy Mode-only, and SuspenseList is not supported in
Legacy Mode.

Fixes a test that I had temporarily disabled when upstreaming the Lanes
implementation in #19108.
  • Loading branch information
acdlite committed Jun 19, 2020
1 parent d1d9054 commit 6ba25b9
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,82 @@ exports[`Store collapseNodesByDefault:false should support nested Suspense nodes
<Component key="Unrelated at End">
`;

exports[`Store collapseNodesByDefault:false should support nested Suspense nodes: 8: first and third child are suspended 1`] = `
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`;

exports[`Store collapseNodesByDefault:false should support nested Suspense nodes: 9: parent is suspended 1`] = `
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading key="Parent Fallback">
`;

exports[`Store collapseNodesByDefault:false should support nested Suspense nodes: 10: parent is suspended 1`] = `
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading key="Parent Fallback">
`;

exports[`Store collapseNodesByDefault:false should support nested Suspense nodes: 11: all children are suspended 1`] = `
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`;

exports[`Store collapseNodesByDefault:false should support nested Suspense nodes: 12: all children are suspended 1`] = `
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`;

exports[`Store collapseNodesByDefault:false should support nested Suspense nodes: 13: third child is suspended 1`] = `
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Component key="Suspense 1 Content">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`;

exports[`Store collapseNodesByDefault:false should support reordering of children: 1: mount 1`] = `
[root]
▾ <Root>
Expand Down
122 changes: 55 additions & 67 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,73 +285,61 @@ describe('Store', () => {
);
expect(store).toMatchSnapshot('7: only third child is suspended');

// FIXME: The rest of the test fails. This was introduced as part of
// the Lanes refactor. I'm fairly certain it's related to the layout of
// the Suspense fiber: we no longer conditionally wrap the primary
// children. They are always wrapped in an extra fiber.
//
// This landed in the new fork without triggering the test run
// because we don't run the DevTools tests against both forks. I only
// discovered the failure once I upstreamed the changes.
//
// Since this has been running in www for weeks without major issues, I'll
// defer fixing this to a follow up.
//
// const rendererID = getRendererID();
// act(() =>
// agent.overrideSuspense({
// id: store.getElementIDAtIndex(4),
// rendererID,
// forceFallback: true,
// }),
// );
// expect(store).toMatchSnapshot('8: first and third child are suspended');
// act(() =>
// agent.overrideSuspense({
// id: store.getElementIDAtIndex(2),
// rendererID,
// forceFallback: true,
// }),
// );
// expect(store).toMatchSnapshot('9: parent is suspended');
// act(() =>
// ReactDOM.render(
// <Wrapper
// suspendParent={false}
// suspendFirst={true}
// suspendSecond={true}
// />,
// container,
// ),
// );
// expect(store).toMatchSnapshot('10: parent is suspended');
// act(() =>
// agent.overrideSuspense({
// id: store.getElementIDAtIndex(2),
// rendererID,
// forceFallback: false,
// }),
// );
// expect(store).toMatchSnapshot('11: all children are suspended');
// act(() =>
// agent.overrideSuspense({
// id: store.getElementIDAtIndex(4),
// rendererID,
// forceFallback: false,
// }),
// );
// expect(store).toMatchSnapshot('12: all children are suspended');
// act(() =>
// ReactDOM.render(
// <Wrapper
// suspendParent={false}
// suspendFirst={false}
// suspendSecond={false}
// />,
// container,
// ),
// );
// expect(store).toMatchSnapshot('13: third child is suspended');
const rendererID = getRendererID();
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchSnapshot('8: first and third child are suspended');
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchSnapshot('9: parent is suspended');
act(() =>
ReactDOM.render(
<Wrapper
suspendParent={false}
suspendFirst={true}
suspendSecond={true}
/>,
container,
),
);
expect(store).toMatchSnapshot('10: parent is suspended');
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchSnapshot('11: all children are suspended');
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchSnapshot('12: all children are suspended');
act(() =>
ReactDOM.render(
<Wrapper
suspendParent={false}
suspendFirst={false}
suspendSecond={false}
/>,
container,
),
);
expect(store).toMatchSnapshot('13: third child is suspended');
});

it('should display a partially rendered SuspenseList', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -2082,9 +2082,18 @@ function updateSuspenseFallbackChildren(
};

let primaryChildFragment;
if ((mode & BlockingMode) === NoMode) {
if (
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
(mode & BlockingMode) === NoMode &&
// Make sure we're on the second pass, i.e. the primary child fragment was
// already cloned. In legacy mode, the only case where this isn't true is
// when DevTools forces us to display a fallback; we skip the first render
// pass entirely and go straight to rendering the fallback. (In Concurrent
// Mode, SuspenseList can also trigger this scenario, but this is a legacy-
// only codepath.)
workInProgress.child !== currentPrimaryChildFragment
) {
const progressedPrimaryFragment: Fiber = (workInProgress.child: any);
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childLanes = NoLanes;
Expand Down
11 changes: 10 additions & 1 deletion packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -2082,9 +2082,18 @@ function updateSuspenseFallbackChildren(
};

let primaryChildFragment;
if ((mode & BlockingMode) === NoMode) {
if (
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
(mode & BlockingMode) === NoMode &&
// Make sure we're on the second pass, i.e. the primary child fragment was
// already cloned. In legacy mode, the only case where this isn't true is
// when DevTools forces us to display a fallback; we skip the first render
// pass entirely and go straight to rendering the fallback. (In Concurrent
// Mode, SuspenseList can also trigger this scenario, but this is a legacy-
// only codepath.)
workInProgress.child !== currentPrimaryChildFragment
) {
const progressedPrimaryFragment: Fiber = (workInProgress.child: any);
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childLanes = NoLanes;
Expand Down

0 comments on commit 6ba25b9

Please sign in to comment.