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/curried-query-ssr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/solid-query': patch
---

Wait for curried queries before rendering Solid query status branches on the server.
91 changes: 91 additions & 0 deletions packages/solid-query/src/__tests__/fixtures/ssr-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Match, Suspense, Switch } from 'solid-js'
import { renderToStringAsync } from 'solid-js/web'
import {
QueryClient,
QueryClientProvider,
queryOptions,
useQuery,
} from '../../index'

const QUERY_NAME = 'shell-data'
const QUERY_SLUG = '/'
export const expectedQueryResult = `ok: ${QUERY_SLUG}`
export const initialQueryResult = 'initial data'
export const successLabel = 'Success:'

export function getCoverage() {
return Reflect.get(globalThis, '__VITEST_COVERAGE__')
}

export async function render() {
const queryClient = new QueryClient()
let queryFnCalls = 0
const getOptions = (slug: string) =>
queryOptions({
queryKey: [QUERY_NAME, slug],
queryFn: () => {
queryFnCalls += 1
return Promise.resolve(`ok: ${slug}`)
},
})

function Page() {
const query = useQuery(() => getOptions(QUERY_SLUG))

return (
<Switch>
<Match when={query.isPending}>PENDING</Match>
<Match when={query.isError}>ERROR</Match>
<Match when={query.isSuccess}>
{successLabel} {query.data}
</Match>
</Switch>
)
}

try {
const markup = await renderToStringAsync(() => (
<QueryClientProvider client={queryClient}>
<Suspense fallback="LOADING">
<Page />
</Suspense>
</QueryClientProvider>
))

return { markup, queryFnCalls }
} finally {
queryClient.clear()
}
}

export async function renderWithInitialData() {
const queryClient = new QueryClient()
let queryFnCalls = 0

function Page() {
const query = useQuery(() => ({
queryKey: [QUERY_NAME, 'initial'],
queryFn: () => {
queryFnCalls += 1
return Promise.resolve(expectedQueryResult)
},
initialData: initialQueryResult,
}))

return query.isSuccess ? `${successLabel} ${query.data}` : 'PENDING'
}

try {
const markup = await renderToStringAsync(() => (
<QueryClientProvider client={queryClient}>
<Suspense fallback="LOADING">
<Page />
</Suspense>
</QueryClientProvider>
))

return { markup, queryFnCalls }
} finally {
queryClient.clear()
}
}
123 changes: 123 additions & 0 deletions packages/solid-query/src/__tests__/ssr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/** @vitest-environment node */
// cspell:ignore instrumenter

import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createServer } from 'vite'
import solid from 'vite-plugin-solid'
import type { Plugin, ViteDevServer } from 'vite'
import type * as SsrEntry from './fixtures/ssr-entry'

const PACKAGE_ROOT = fileURLToPath(new URL('../..', import.meta.url))
const SSR_ENTRY_PATH = '/src/__tests__/fixtures/ssr-entry.tsx'
const USE_BASE_QUERY_PATH = fileURLToPath(
new URL('../useBaseQuery.ts', import.meta.url),
)
const SCRIPT_TAG = '<script>'
const COVERAGE_GLOBAL = '__VITEST_COVERAGE__'
const moduleResolver = createRequire(import.meta.url)
const coverageModuleResolver = createRequire(
moduleResolver.resolve('@vitest/coverage-istanbul'),
)
const { createInstrumenter } = coverageModuleResolver('istanbul-lib-instrument')
const SOLID_SERVER_ENTRY = moduleResolver.resolve('solid-js/dist/server.js')
const SOLID_STORE_SERVER_ENTRY = moduleResolver.resolve(
'solid-js/store/dist/server.js',
)
const SOLID_WEB_SERVER_ENTRY = moduleResolver.resolve(
'solid-js/web/dist/server.js',
)

function createSsrCoveragePlugin(): Plugin {
const instrumenter = createInstrumenter({
autoWrap: false,
compact: false,
coverageGlobalScope: 'globalThis',
coverageGlobalScopeFunc: false,
coverageVariable: COVERAGE_GLOBAL,
esModules: true,
produceSourceMap: true,
})

return {
name: 'solid-query-ssr-coverage',
enforce: 'post',
transform(sourceCode, id) {
if (id.split('?', 1)[0] !== USE_BASE_QUERY_PATH) {
return
}

const sourceMap = this.getCombinedSourcemap()
const code = instrumenter.instrumentSync(sourceCode, id, sourceMap)

return { code, map: instrumenter.lastSourceMap() }
},
}
}

describe('server rendering', () => {
let viteServer: ViteDevServer

beforeAll(async () => {
viteServer = await createServer({
root: PACKAGE_ROOT,
appType: 'custom',
logLevel: 'silent',
plugins: [solid(), createSsrCoveragePlugin()],
resolve: {
alias: [
{ find: /^solid-js$/, replacement: SOLID_SERVER_ENTRY },
{ find: /^solid-js\/store$/, replacement: SOLID_STORE_SERVER_ENTRY },
{ find: /^solid-js\/web$/, replacement: SOLID_WEB_SERVER_ENTRY },
],
conditions: ['node', '@tanstack/custom-condition'],
},
ssr: {
noExternal: ['solid-js', '@tanstack/query-core'],
},
server: {
middlewareMode: true,
},
})
})

afterAll(async () => {
await viteServer.close()
})

it('waits for a curried query when rendering status branches', async () => {
const { expectedQueryResult, getCoverage, render, successLabel } =
(await viteServer.ssrLoadModule(SSR_ENTRY_PATH)) as typeof SsrEntry

const { markup, queryFnCalls } = await render()
const parentCoverage = Reflect.get(globalThis, COVERAGE_GLOBAL)
const ssrCoverage = getCoverage()

expect(ssrCoverage).toHaveProperty(USE_BASE_QUERY_PATH)

// Vite's SSR runner has its own global scope, so forward its counters to
// Vitest when coverage is enabled in the parent test process.
if (parentCoverage && ssrCoverage) {
Object.assign(parentCoverage, ssrCoverage)
}

const renderedContent = markup.split(SCRIPT_TAG, 1)[0]

expect(renderedContent).toContain(successLabel)
expect(renderedContent).toContain(expectedQueryResult)
expect(queryFnCalls).toBe(1)
})

it('renders initial data without waiting for a server refetch', async () => {
const { initialQueryResult, renderWithInitialData, successLabel } =
(await viteServer.ssrLoadModule(SSR_ENTRY_PATH)) as typeof SsrEntry

const { markup, queryFnCalls } = await renderWithInitialData()
const renderedContent = markup.split(SCRIPT_TAG, 1)[0]

expect(renderedContent).toContain(successLabel)
expect(renderedContent).toContain(initialQueryResult)
expect(queryFnCalls).toBe(1)
})
})
9 changes: 8 additions & 1 deletion packages/solid-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,14 @@ export function useBaseQuery<
}
return queryResource()?.data
}
return Reflect.get(target, prop)

const value = Reflect.get(target, prop)

if (isServer && state.isPending && typeof value !== 'function') {
queryResource()
}

return value
},
}

Expand Down