Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/solid-query-ssr-disabled-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/solid-query': patch
---

Fix `renderToStringAsync` hanging during SSR when a query is disabled: strip the unserializable `experimental_prefetchInRender` promise from the hydratable observer result, since a disabled query's promise never settles and the SSR serializer would await it forever.
72 changes: 72 additions & 0 deletions packages/solid-query/src/__tests__/ssr.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it, vi } from 'vitest'
import { render, waitFor } from '@solidjs/testing-library'
import { QueryClient, QueryClientProvider, useQuery } from '..'
import type * as SolidWeb from 'solid-js/web'
import type { UseQueryResult } from '..'

// Force the server code path: on the server `useBaseQuery` enables
// `experimental_prefetchInRender` and resolves the resource with a
// serializable `hydratableObserverResult()`.
vi.mock('solid-js/web', async (importOriginal) => {
const mod = await importOriginal<typeof SolidWeb>()
return { ...mod, isServer: true }
})

describe('useQuery on the server', () => {
it('resolves a disabled query without leaking the prefetch promise (#10907)', async () => {
const client = new QueryClient()
let state: UseQueryResult<string> | undefined

function Page() {
const query = useQuery(() => ({
queryKey: ['disabled-ssr'],
queryFn: () => Promise.resolve('data'),
enabled: false,
}))
state = query
return <div>data: {String(query.data)}</div>
}

const rendered = render(() => (
<QueryClientProvider client={client}>
<Page />
</QueryClientProvider>
))

await waitFor(() => rendered.getByText('data: undefined'))

// The resolved result is serialized into the SSR payload, so it must not
// carry functions or promises. The `experimental_prefetchInRender`
// promise of a disabled query never settles, which would hang
// `renderToStringAsync` while the serializer awaits it.
expect(state!.refetch).toBeUndefined()
expect(state!.promise).toBeUndefined()
})

it('resolves an enabled query with data and without unserializable fields', async () => {
const client = new QueryClient()
let state: UseQueryResult<string> | undefined

function Page() {
const query = useQuery(() => ({
queryKey: ['enabled-ssr'],
queryFn: () => Promise.resolve('server data'),
}))
state = query
return <div>data: {String(query.data)}</div>
}

const rendered = render(() => (
<QueryClientProvider client={client}>
<Page />
</QueryClientProvider>
))

await waitFor(() => rendered.getByText('data: server data'))

expect(state!.data).toBe('server data')
expect(state!.isSuccess).toBe(true)
expect(state!.refetch).toBeUndefined()
expect(state!.promise).toBeUndefined()
})
})
4 changes: 4 additions & 0 deletions packages/solid-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ const hydratableObserverResult = <
// During SSR, functions cannot be serialized, so we need to remove them
// This is safe because we will add these functions back when the query is hydrated
refetch: undefined,
// The `experimental_prefetchInRender` promise cannot be serialized either.
// For a disabled query it never settles, which would hang stream/async
// SSR while the serializer awaits it (#10907).
promise: undefined,
}

// If the query is an infinite query, we need to remove additional properties
Expand Down