diff --git a/.changeset/solid-query-ssr-disabled-hang.md b/.changeset/solid-query-ssr-disabled-hang.md new file mode 100644 index 00000000000..150f7264c76 --- /dev/null +++ b/.changeset/solid-query-ssr-disabled-hang.md @@ -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. diff --git a/packages/solid-query/src/__tests__/ssr.test.tsx b/packages/solid-query/src/__tests__/ssr.test.tsx new file mode 100644 index 00000000000..2df17479278 --- /dev/null +++ b/packages/solid-query/src/__tests__/ssr.test.tsx @@ -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() + 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 | undefined + + function Page() { + const query = useQuery(() => ({ + queryKey: ['disabled-ssr'], + queryFn: () => Promise.resolve('data'), + enabled: false, + })) + state = query + return
data: {String(query.data)}
+ } + + const rendered = render(() => ( + + + + )) + + 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 | undefined + + function Page() { + const query = useQuery(() => ({ + queryKey: ['enabled-ssr'], + queryFn: () => Promise.resolve('server data'), + })) + state = query + return
data: {String(query.data)}
+ } + + const rendered = render(() => ( + + + + )) + + 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() + }) +}) diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 773d0719e0c..734b784daba 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -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