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

experimental_promise: A component was suspended by an uncached promise #8384

Open
TkDodo opened this issue Dec 2, 2024 · 7 comments
Open

Comments

@TkDodo
Copy link
Collaborator

TkDodo commented Dec 2, 2024

Describe the bug

using the new experimental promise with use in React 19 leads to an error when multiple queries are used in the same transition. The error is:

A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework. 
    at Example2 (https://7g9lfw.csb.app/src/App.js:51:5)
    at Suspense
    at div
    at SuspenseBoundary (https://7g9lfw.csb.app/src/App.js:73:49)
    at QueryClientProvider (https://7g9lfw.csb.app/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.cjs:55:3)
    at div
    at Example

Your minimal, reproducible example

https://codesandbox.io/p/sandbox/suspense-transitions-7g9lfw

Steps to reproduce

  • click decrease
  • look at the error in the devtools
  • you can also see that 2.1 shows the right value, while 2.2 shows the wrong (outdated) value

everything works fine if we remove startTransition

Expected behavior

Both components (2.1 and 2.2) should show the correct state values

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

React 19RC, ReactQuery 5.62.0

Tanstack Query adapter

react-query

TanStack Query version

5.62.0

TypeScript version

Additional context

Initially, I wanted to test if placeholderData: keepPreviousData works inside transitions. And it does - but I found those weird errors, and noticed they have nothing to do with placeholderData, but rather always happen if use the same transition value in multiple queries. It doesn’t matter if they are in the same suspense boundary or not.

@TkDodo
Copy link
Collaborator Author

TkDodo commented Dec 2, 2024

@KATT could you maybe have a look here?

Here’s a similar reproduction that shows multiple combinations, all working fine without transitions but once startTransition is in the mix, it starts to get weird IF we use the same transition value for multiple queries. It’s fine if we only use the value for one query 🤔

https://codesandbox.io/p/sandbox/suspense-transitions-forked-m7vmrv

I’m thinking it might be because we use useSyncExternalStore under the hood, and it de-opts the transition? I’m not sure, but that error definitely looks worth investigating I think.

@KATT
Copy link
Contributor

KATT commented Dec 4, 2024

I wish it was documented what a "a Suspense-compatible library or framework." means, it feels like we're doing something wrong but kinda fumbling in the dark.

I will try to mess around with this this week 🥲

@tom-sherman
Copy link

I can't reproduce the error consistently (proof in the video 😅)

Screen.Recording.2024-12-06.at.10.04.03.mov

@TkDodo
Copy link
Collaborator Author

TkDodo commented Dec 6, 2024

@tom-sherman yeah I just got it myself only after like 4 fetches :/

Image

@KATT
Copy link
Contributor

KATT commented Dec 6, 2024

Life and other commitments got in the way of me wrestling with this today, but I will try to have another stab next Friday

https://react.dev/blog/2024/12/05/react-19#new-feature-use is a bit vague on what we need to do

We create the promises in useSyncExternalStore() which feels like the right place, but maybe we can poke the right person to get us more guidance

@tom-sherman
Copy link

I think uSES getSnapshot can be called multiple times during the same render, it needs to return the same promise each time.

@TkDodo
Copy link
Collaborator Author

TkDodo commented Dec 6, 2024

Yeah we cache the whole result, including the promise and the uSES snapshot is just:
() => observer.getCurrentResult()

Imo it should get the same promise every time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants