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

Update HydrationStreamProvider.tsx + Advanced SSR docs to fix unnecessary suspensions #6753

Merged
merged 10 commits into from
Feb 18, 2024

Conversation

mgreenw
Copy link
Contributor

@mgreenw mgreenw commented Jan 23, 2024

This PR is based on my findings in #6116 and fixes two cases of unnecessary suspensions during streaming SSR, one on code and the other in the advanced SSR docs. See the full writeup in this comment, which should have all the info needed for reviewing this PR.

fixes #6116

Copy link

vercel bot commented Jan 23, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Ignored Deployment
Name Status Preview Comments Updated (UTC)
query ⬜️ Ignored (Inspect) Visit Preview Feb 18, 2024 8:25am

Copy link

codesandbox-ci bot commented Jan 23, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e895537:

Sandbox Source
@tanstack/query-example-angular-basic Configuration
@tanstack/query-example-react-basic-typescript Configuration
@tanstack/query-example-solid-basic-typescript Configuration
@tanstack/query-example-svelte-basic Configuration
@tanstack/query-example-vue-basic Configuration

@TkDodo TkDodo requested a review from Ephem January 27, 2024 14:51
Copy link

nx-cloud bot commented Jan 27, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit e895537. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 1 target

Sent with 💌 from NxCloud.

Comment on lines 56 to 58
// Browser: make a new query client if we don't already have one
// This is very important so we don't re-make a new client if React
// supsends during the initial render
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess that's why a Suspense boundary is a good idea - so that you can create a queryClient in React State outside of it, and it won't be thrown away. I don't mind the makeQueryClient()function - it's good that it uses a singleton on the client and a new one every time on the server 👍 . I would still probably mention somewhere that this is only needed if you don't have an explicit suspense boundary further down the tree...

@TkDodo
Copy link
Collaborator

TkDodo commented Jan 27, 2024

@juliusmarminge would you mind taking a look here? Also I think the comment in the issue is super interesting:

Copy link
Collaborator

@Ephem Ephem left a comment

Choose a reason for hiding this comment

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

Nice find and thanks for submitting a PR! Suspense is a tricky beast.. I left a few comments but I do think this look good overall.

Comment on lines +146 to +149
// Setup and run the onEntries handler on the client only, but do it during
// the initial render so children have access to the data immediately
// This is important to avoid the client suspending during the initial render
// if the data has not yet been hydrated.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not super on top of the react-query-next-experimental library so not sure if use cases and edge cases align, but, I thought I'd note that it might be worth checking out this comment (and implementation) in the HydrationBoundary for how we handle hydration there: https://github.com/TanStack/query/blob/main/packages/react-query/src/HydrationBoundary.tsx#L35-L49

Short version, what we do there is that if data is not already in the cache, we hydrate in render, if it is, we hydrate in an effect so we don't cause side effects inside of render.

I'm not sure how likely this is to happen with the streaming library, if it can at all, but I thought I'd mention it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting callout! I think it's a different situation here, and what we're doing is safe. HydrationBoundary could happen at any part of the tree, whereas there will only be one ReactQueryStreamedHydration and it will be high up in the tree / wrapping the whole thing. This code will only be called once on boot, and we went any streamed data to be available ASAP on the initial render phase.

Definitely let me know if you see any problems with that logic!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I also think the whole hydration can only happen once, on the first render, where the cache is still empty. I'd give this a go as-is.

I added a small refactoring commit: bb51270

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for taking this across the finish line @TkDodo!

Comment on lines 37 to 49
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important so we don't re-make a new client if React
// supsends during the initial render
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}

export default function Providers({ children }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
Copy link
Collaborator

@Ephem Ephem Jan 27, 2024

Choose a reason for hiding this comment

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

I'm not sure these parts of the docs needs to change? Note that most of the examples on this page until the streaming library part uses useQuery and prefetching, which I don't think should be susceptible to this edge case? But I haven't thought about this as long as you have, so maybe I'm missing something?

Not something I feel strongly about though and happy to keep it as is to keep it aligned too.

If we do keep the original docs, I would still love a comment here along the lines of "if you are using useSuspenseQuery, initiating the QueryClient as state can have edge cases, see the docs on streaming further down".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's important to put this in the docs in both places. I'm betting that many folks (like myself) will copy-paste the first example that they see, and having the useState in there will lead to a broken example by default until they add a suspense boundary.

docs/react/guides/advanced-ssr.md Outdated Show resolved Hide resolved
@mgreenw mgreenw requested review from TkDodo and Ephem February 4, 2024 20:07
@codecov-commenter
Copy link

Codecov Report

Merging #6753 (c2d404b) into main (61f4d74) will not change coverage.
Report is 1 commits behind head on main.
The diff coverage is n/a.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #6753   +/-   ##
=======================================
  Coverage   41.78%   41.78%           
=======================================
  Files         178      178           
  Lines        7014     7014           
  Branches     1416     1416           
=======================================
  Hits         2931     2931           
  Misses       3721     3721           
  Partials      362      362           
Components Coverage Δ
@tanstack/angular-query-devtools-experimental ∅ <ø> (∅)
@tanstack/angular-query-experimental 81.75% <ø> (ø)
@tanstack/eslint-plugin-query 85.11% <ø> (ø)
@tanstack/query-async-storage-persister 48.43% <ø> (ø)
@tanstack/query-broadcast-client-experimental ∅ <ø> (∅)
@tanstack/query-codemods 0.00% <ø> (ø)
@tanstack/query-core 93.62% <ø> (ø)
@tanstack/query-devtools 4.46% <ø> (ø)
@tanstack/query-persist-client-core 57.73% <ø> (ø)
@tanstack/query-sync-storage-persister 82.50% <ø> (ø)
@tanstack/react-query 93.07% <ø> (ø)
@tanstack/react-query-devtools 0.00% <ø> (ø)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query 84.84% <ø> (ø)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client 100.00% <ø> (ø)
@tanstack/svelte-query 62.68% <ø> (ø)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client 100.00% <ø> (ø)
@tanstack/vue-query 70.81% <ø> (ø)
@tanstack/vue-query-devtools ∅ <ø> (∅)

@TkDodo TkDodo merged commit a1d12ab into TanStack:main Feb 18, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

useSuspenseQuery infinite refetch after SSR (Next.js App Router)
4 participants