react-spectrum
react-spectrum copied to clipboard
[RAC] Table components do not play well with Suspense
Provide a general summary of the issue here
I started building a design system on top of React Aria Components. The application that uses it is built using TanStack Router and Apollo Client. Both of them use Suspense for data fetching. At first this seems to work fine but things are starting to get very weird when it comes to things like infinite scrolling (meaning loading more data on scroll) for example. While wrapper components properly re-render with the updated data these updates aren't displayed by RAC components. It gets even weirder: When a component got into this broken state, simply clicking anywhere on the screen causes the table to be simply emptied. No errors or warnings.
~I'm still trying to come up with a minimal reproducible example but wanted to open this issue already as you might be aware of undocumented shortcomings with RAC and Suspense already.~ I suspect that the fake DOM implementation that tables use might the culprit here but I'm still collecting details.
๐ค Expected Behavior?
Things to not break. More details on reproduction case below.
๐ฏ Current Behavior
As described in summary.
๐ Possible Solution
No response
๐ฆ Context
No response
๐ฅ๏ธ Steps to Reproduce
https://codesandbox.io/p/sandbox/tender-resonance-76wlzv?file=%2Fsrc%2FLocationsTable.jsx%3A47%2C15
https://github.com/adobe/react-spectrum/assets/9491603/b1ded7ca-7f1c-435e-9a80-99549a877e12
fetchMore of Apollo Client re-suspends by default, meaning it should show the nearest Suspense fallback. Which it does but instead of replacing all rows it only shows it (Loading...) in addition. While this might seem correct from a UX perspective it's not what would be expected from a technical viewpoint. You can try the same with raw HTML tags and it will replace the whole body by Loading... instead of temporarily appending a new row.
It gets even worse when wrapping the fetchMore call in startTransition. This should be done to avoid suspending to the nearest fallback and instead keep the previous UI mounted while the fetch is ongoing. At first this seems to work but as soon as the component re-renders the table simply doesn't display the newly fetched rows. The wrapping component gets correctly re-rendered, also the new rows are there (see console.log).
Version
1.1.1
What browsers are you seeing the problem on?
Microsoft Edge
If other, please specify.
No response
What operating system are you using?
macOS
๐งข Your Company/Team
No response
๐ท Tracking Issue
No response
Added Codesandbox and screen recording of a minimal reproducible example.
Will need to do some debugging in the collection implementation to determine what's going on. In the meantime, can you move the Suspense boundary outside the Table instead?
@devongovett Yep. This seems to work fine.
I also already tried to debug the collection internals but I think I'm lacking too much knowledge of what's going in there actually ๐
I believe I'm running into the same problem but moving the Suspense boundary doesn't seem to be of any help in my case: It's already outside of the component rendering the table (in main.tsx). I'm using useSuspenseQuery from @tanstack/react-query.
See the repro or see it on StackBlitz. It renders a ul element on the right to show the expected output.
As an added data point, wrapping the setState call in startTransition seems to fix it. But I can't do it in my real case because the data query key actually comes from the URL and I don't have control over the router.
I have no knowledge of the RAC internals but React.useRef is usually the culprit when used for "caching" some data: It doesn't play nice with interrupted renders.
Another potential workaround has been found https://github.com/adobe/react-spectrum/issues/6801#issuecomment-2261024008
I think I'm experiencing something similar with Next.js and Breadcrumbs: https://codesandbox.io/p/devbox/dry-night-97f3nx No querying, just suspense-wrapped navigation.
I'm also experiencing issues with Suspense, Next.js and the collection API, in this case Tabs.
Naive implementation with loading.tsx and Next Links: Sandbox
- initial tab switching shows the loading state
- the selected tab is switched right away (optimistically) before loading its content
Using Tabs with href, wrapping the TabContent in Suspense: Sandbox
- Tab is not switched immediately
โ ๏ธ everything else looks fine as long as in dev mode. but when running npm run build && npm run start and looking at the production build, it suddenly is not working anymore:
- no loading state is shown
- Suspense with a key not working as mitigation
We're having the same issue when using GridList in combination with Jotai. Interestingly while the GridList is originally rendered inside Suspense boundary, the jotai atom update is client only and shouldn't afaik cause another suspension.