Skip to content
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
1 change: 0 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3385,7 +3385,6 @@ body {
);
});

// @gate FIXME
it('loading a stylesheet as part of an error boundary UI, during initial render', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
Expand Down
11 changes: 10 additions & 1 deletion packages/react-reconciler/src/ReactFiberThenable.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ export const SuspenseyCommitException: mixed = new Error(
// TODO: It would be better to refactor throwException into multiple functions
// so we can trigger a fallback directly without having to check the type. But
// for now this will do.
export const noopSuspenseyCommitThenable = {then() {}};
export const noopSuspenseyCommitThenable = {
then() {
if (__DEV__) {
console.error(
'Internal React error: A listener was unexpectedly attached to a ' +
'"noop" thenable. This is a bug in React. Please file an issue.',
);
}
},
};

export function createThenableState(): ThenableState {
// The ThenableState is created the first time a component suspends. If it
Expand Down
30 changes: 15 additions & 15 deletions packages/react-reconciler/src/ReactFiberThrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,15 @@ function throwException(
} else {
retryQueue.add(wakeable);
}

// We only attach ping listeners in concurrent mode. Legacy
// Suspense always commits fallbacks synchronously, so there are
// no pings.
if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes);
}
}
break;
return;
}
case OffscreenComponent: {
if (suspenseBoundary.mode & ConcurrentMode) {
Expand All @@ -466,24 +473,17 @@ function throwException(
retryQueue.add(wakeable);
}
}

attachPingListener(root, wakeable, rootRenderLanes);
}
break;
return;
}
// Fall through
}
default: {
throw new Error(
`Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
'is a bug in React.',
);
}
}
// We only attach ping listeners in concurrent mode. Legacy Suspense always
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this into the branches above to avoid attaching a redundant listener to the "noop" thenable. That way we can treat any listener that does get attached as a mistake and warn in dev.

// commits fallbacks synchronously, so there are no pings.
if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes);
}
return;
throw new Error(
`Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
'is a bug in React.',
);
} else {
// No boundary was found. Unless this is a sync update, this is OK.
// We can suspend and wait for more data to arrive.
Expand Down
49 changes: 26 additions & 23 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,16 @@ export function shouldRemainOnPreviousScreen(): boolean {
// takes into account both the priority of render and also whether showing a
// fallback would produce a desirable user experience.

const handler = getSuspenseHandler();
if (handler === null) {
// There's no Suspense boundary that can provide a fallback. We have no
// choice but to remain on the previous screen.
// NOTE: We do this even for sync updates, for lack of any better option. In
// the future, we may change how we handle this, like by putting the whole
// root into a "detached" mode.
return true;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix. It used to return false in this case, now it returns true.

}

// TODO: Once `use` has fully replaced the `throw promise` pattern, we should
// be able to remove the equivalent check in finishConcurrentRender, and rely
// just on this one.
Expand All @@ -1705,29 +1715,22 @@ export function shouldRemainOnPreviousScreen(): boolean {
}
}

const handler = getSuspenseHandler();
if (handler === null) {
// TODO: We should support suspending in the case where there's no
// parent Suspense boundary, even outside a transition. Somehow. Otherwise,
// an uncached promise can fall into an infinite loop.
} else {
if (
includesOnlyRetries(workInProgressRootRenderLanes) ||
// In this context, an OffscreenLane counts as a Retry
// TODO: It's become increasingly clear that Retries and Offscreen are
// deeply connected. They probably can be unified further.
includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)
) {
// During a retry, we can suspend rendering if the nearest Suspense boundary
// is the boundary of the "shell", because we're guaranteed not to block
// any new content from appearing.
//
// The reason we must check if this is a retry is because it guarantees
// that suspending the work loop won't block an actual update, because
// retries don't "update" anything; they fill in fallbacks that were left
// behind by a previous transition.
return handler === getShellBoundary();
}
if (
includesOnlyRetries(workInProgressRootRenderLanes) ||
// In this context, an OffscreenLane counts as a Retry
// TODO: It's become increasingly clear that Retries and Offscreen are
// deeply connected. They probably can be unified further.
includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)
) {
// During a retry, we can suspend rendering if the nearest Suspense boundary
// is the boundary of the "shell", because we're guaranteed not to block
// any new content from appearing.
//
// The reason we must check if this is a retry is because it guarantees
// that suspending the work loop won't block an actual update, because
// retries don't "update" anything; they fill in fallbacks that were left
// behind by a previous transition.
return handler === getShellBoundary();
}

// For all other Lanes besides Transitions and Retries, we should not wait
Expand Down