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

Bug: Error message "Uncaught Error: A component suspended while responding to synchronous input." may be misleading #25629

Closed
bmwebster opened this issue Nov 3, 2022 · 8 comments
Labels
Resolution: Stale Automatically closed due to inactivity Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@bmwebster
Copy link

React version: 18.2.0

Steps To Reproduce

Do anything that triggers the error:

Uncaught Error: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.

This seems to occur when a synchronous input causes a component to suspend, without a suspense boundary defined.

To create the specific situation where I saw the error:

  1. Set up react-router with two routes, each containing a component that suspends, using a Relay hook for a graphql query. Do not provide a Suspend around either component.
  2. Navigate from route A to route B.
  3. Refresh the page so the Relay cache is cleared.
  4. Use the browser back button to navigate back from B to A, causing the component in A to suspend as it performs the graphql query.

Link to code example:

Can provide if needed, however the issue seems to be that the wording of the error doesn't match the conditions that cause the error to be thrown in React code (see below), so a code example might not be necessary?

The current behavior

The following error message is displayed:

Uncaught Error: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.

stacktrace

This implies that the only way to fix the error is to use "startTransition". While this did fix my error, looking at the code in question it seems that the more obvious problem is that there was no suspense boundary around the component in question. This problem is also much easier to fix - in my case could add transitions easily for some cases (around navigateTo calls to react-router), but catching every way of navigating and adding a transition seems to be quite difficult. I added a Suspense around the suspending component, and this resolved the error. Using "startTransition" does have extra advantages of allowing the old state of the suspended component to display instead of a fallback, but this seems like it should be more of an "information" notice than an error, so I'm assuming it's only the lack of both a suspense boundary and a synchronous input that is intended to be flagged as an error?

Looking at the React code seems to confirm this:

    var suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);

    if (suspenseBoundary !== null) {
      suspenseBoundary.flags &= ~ForceClientRender;
      markSuspenseBoundaryShouldCapture(suspenseBoundary, returnFiber, sourceFiber, root, rootRenderLanes); // 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);
      }

      attachRetryListener(suspenseBoundary, root, wakeable);
      return;
    } else {
      // No boundary was found. Unless this is a sync update, this is OK.
      // We can suspend and wait for more data to arrive.
      if (!includesSyncLane(rootRenderLanes)) {
        // This is not a sync update. Suspend. Since we're not activating a
        // Suspense boundary, this will unwind all the way to the root without
        // performing a second pass to render a fallback. (This is arguably how
        // refresh transitions should work, too, since we're not going to commit
        // the fallbacks anyway.)
        //
        // This case also applies to initial hydration.
        attachPingListener(root, wakeable, rootRenderLanes);
        renderDidSuspendDelayIfPossible();
        return;
      } // This is a sync/discrete update. We treat this case like an error
      // because discrete renders are expected to produce a complete tree
      // synchronously to maintain consistency with external state.


      var uncaughtSuspenseError = new Error('A component suspended while responding to synchronous input. This ' + 'will cause the UI to be replaced with a loading indicator. To ' + 'fix, updates that suspend should be wrapped ' + 'with startTransition.'); // If we're outside a transition, fall through to the regular error path.
      // The error will be caught by the nearest suspense boundary.

      value = uncaughtSuspenseError;
    }
  } else {
    // This is a regular error, not a Suspense wakeable.

I could be reading this wrongly, but it looks like the error I'm seeing is only raised when there is no boundary found, and we have a sync update. If there is a suspense boundary, it looks like the error doesn't apply, and there is a comment specifically covering that if there is no boundary, only sync updates are an error ("Unless this is a sync update, this is OK.").

In addition, the component does not seem to re-render when data is received, the output remains empty, presumably because this is an unrecoverable error?

The expected behavior

An error is displayed which covers both options for resolving, e.g.:

Uncaught Error: A component suspended while responding to synchronous input, and no suspense boundary was provided. To fix, provide a suspense boundary, or ensure that updates that suspend are wrapped with startTransition, or both. Note that providing a suspense boundary but omitting startTransition will cause the UI to be replaced with a loading indicator.

This is a little wordy, so maybe it should be a link to a documentation page instead?

@bmwebster bmwebster added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Nov 3, 2022
@eps1lon
Copy link
Collaborator

eps1lon commented Nov 4, 2022

Can provide if needed, however the issue seems to be that the wording of the error doesn't match the conditions that cause the error to be thrown in React code (see below), so a code example might not be necessary?

Please do provide a minimal example where the error would be misleading.

@bmwebster
Copy link
Author

Sorry for the delay - just back from holiday!

I've put together a very simple replication as a sandbox here:

https://codesandbox.io/s/charming-jang-y5evsf

The core of this is just having a component suspend without a suspense boundary:

import { Suspense } from "react";

const SuspenseTrigger = () => {
  throw new Promise(() => {});
};

export default function App() {
  return (
    <div className="App">
      <h1>Suspense example</h1>
      {/* <Suspense fallback={<div>loading...</div>}> */}
      <SuspenseTrigger />
      {/* </Suspense> */}
    </div>
  );
}

The sandbox seems to give slightly odd behaviour on reloading, so you'll need to do these steps in order to show the issue:

  1. Open the sandbox - this starts with no suspense happening (the suspending component is commented out), and should just display "Suspense Example"

    Screenshot 2022-11-28 at 13 03 56
  2. In App.js, uncomment the <SuspenseTrigger /> line. This suspends on render but there is no suspense boundary so we see the misleading error. This replicates what I'm seeing when Relay suspends to wait for a query result. The error should be as below:

    Screenshot 2022-11-28 at 13 04 29
  3. The simplest fix is to add a suspense boundary, rather than using startTransition as described in the error message. To check this, uncomment the remaining two commented lines in App.js. This gives a fallback component as expected, and resolves the error without requiring startTransition:

    Screenshot 2022-11-28 at 13 13 44

Adding startTransition in cases where it makes sense seems to be a nice-to-have addition after a suspense boundary is in place, in that it will delay the transition until the suspending component is ready. However a) this doesn't seem to be the primary problem - that's the lack of suspense boundary, b) the suspense boundary is much easier to add and more general, and c) it seems like it may not always be possible to use a transition. For example where the problem occurs on first load as in the minimal example, or in my react-router case where we can benefit from using startTransition in some places, but it's difficult to make sure any possible navigation route is done as a transition, and missing any out will lead to an error unless we also add a suspense boundary.

So my feeling is that if the error message also suggested adding a suspense boundary, it would reinforce good practice, and in my case would also have got me much more quickly to the real issue of a misplaced <Suspense> component. There may well be other approaches I'm missing, in which case they could be added to the message or in a referenced docs page?

@fthebaud
Copy link

I also ran into this error message when testing a component that uses Tanstack Query (React Query).
The component itself doesn't have a <Suspense>, the <Suspense> is located higher up in the tree.
When rendering the component with React Testing Library, I would sometimes get the errror message telling me I needed to wrap my component with startTransition, which left me pretty confused... In the end, wrapping my tests with <Suspense> to make sure I always have a suspsense boudary got rid of this error.

@bmwebster
Copy link
Author

@eps1lon Hi, I just wanted to check whether the example given was enough to show the issue? Just let me know if there's anything else that would be useful.

@Abeinevincent
Copy link

You can wrap your component in a boundary and provide a fall back as well

@drecali
Copy link

drecali commented Jun 14, 2023

I had this issue and like @bmwebster and @fthebaud, I think the error message is misleading. In my case I was able to fix the error without using <Suspense> or startTransition. Unfortunately, the app is private and complex so I can't post an example, but here are the basics:

  1. The app uses recoil for state management
  2. The error occurred while running unit tests with react-testing-library (and jest under the hood) and a custom render method to use recoil state during testing.
  3. The cause of the error was in a custom hook that accessed the same Recoil state in 2 different ways:
    • Originally, the state was accessed using useRecoilStateLoadable. This did not cause an error.
    • The error was caused later when a line of code was added to access the state using useRecoilValue. The error happened even if this 2nd instance of the same state was not used.

In this case, I just removed the 2nd instance of the same state and it fixed the error.

I hope this might help someone troubleshooting. The error message led me down the wrong path for a while. I first tried using <Suspense> and startTransition. The app and testing code are both complex so it took some time to exhaust reasonable possibilities using <Suspense> and startTransition 😔

Copy link

github-actions bot commented Apr 9, 2024

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@github-actions github-actions bot added the Resolution: Stale Automatically closed due to inactivity label Apr 9, 2024
Copy link

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Apr 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution: Stale Automatically closed due to inactivity Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

5 participants