diff --git a/docs/router/config.json b/docs/router/config.json index 9aeb8139ad..0b56fa735d 100644 --- a/docs/router/config.json +++ b/docs/router/config.json @@ -461,6 +461,25 @@ } ] }, + { + "label": "Integrations", + "children": [], + "frameworks": [ + { + "label": "react", + "children": [ + { + "label": "TanStack Query", + "to": "integrations/query" + } + ] + }, + { + "label": "solid", + "children": [] + } + ] + }, { "label": "ESLint", "children": [ diff --git a/docs/router/framework/react/guide/tanstack-start.md b/docs/router/framework/react/guide/tanstack-start.md deleted file mode 100644 index a44ebd195a..0000000000 --- a/docs/router/framework/react/guide/tanstack-start.md +++ /dev/null @@ -1,252 +0,0 @@ ---- -id: tanstack-start -title: TanStack Start ---- - -TanStack Start is a full-stack framework for building server-rendered React applications built on top of [TanStack Router](https://tanstack.com/router). - -To set up a TanStack Start project, you'll need to: - -1. Install the dependencies -2. Add a configuration file -3. Create required templating - -Follow this guide to build a basic TanStack Start web application. Together, we will use TanStack Start to: - -- Serve an index page... -- Which displays a counter... -- With a button to increment the counter persistently. - -[Here is what that will look like](https://stackblitz.com/github/tanstack/router/tree/main/examples/react/start-basic-counter) - -Create a new project if you're starting fresh. - -```shell -mkdir myApp -cd myApp -npm init -y -``` - -Create a `tsconfig.json` file with at least the following settings: - -```jsonc -{ - "compilerOptions": { - "jsx": "react-jsx", - "moduleResolution": "Bundler", - "module": "Preserve", - "target": "ES2022", - "skipLibCheck": true, - }, -} -``` - -# Install Dependencies - -TanStack Start is powered by the following packages and need to be installed as dependencies: - -- [@tanstack/start](https://github.com/tanstack/start) -- [@tanstack/react-router](https://tanstack.com/router) -- [Vite](https://vite.dev/) - -To install them, run: - -```shell -npm i @tanstack/react-start @tanstack/react-router vite -``` - -You'll also need React and the Vite React plugin, so install their dependencies as well: - -```shell -npm i react react-dom @vitejs/plugin-react -``` - -Please, for you, your fellow developers, and your users' sake, use TypeScript: - -```shell -npm i -D typescript @types/react @types/react-dom -``` - -# Update Configuration Files - -We'll then update our `package.json` to use Vite's CLI and set `"type": "module"`: - -```jsonc -{ - // ... - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "start": "vite start", - }, -} -``` - -Then configure TanStack Start's `app.config.ts` file: - -```typescript -// app.config.ts -import { defineConfig } from '@tanstack/react-start/config' - -export default defineConfig({}) -``` - -# Add the Basic Templating - -There are 2 required files for TanStack Start usage: - -1. The router configuration -2. The root of your application - -Once configuration is done, we'll have a file tree that looks like the following: - -``` -. -├── app/ -│ ├── routes/ -│ │ └── `__root.tsx` -│ ├── `router.tsx` -│ ├── `routeTree.gen.ts` -├── `.gitignore` -├── `app.config.ts` -├── `package.json` -└── `tsconfig.json` -``` - -## The Router Configuration - -This is the file that will dictate the behavior of TanStack Router used within Start for both the server and the client. Here, you can configure everything -from the default [preloading functionality](../preloading.md) to [caching staleness](../data-loading.md). - -```tsx -// app/router.tsx -import { createRouter as createTanStackRouter } from '@tanstack/react-router' -import { routeTree } from './routeTree.gen' - -export function createRouter() { - const router = createTanStackRouter({ - routeTree, - }) - - return router -} - -declare module '@tanstack/react-router' { - interface Register { - router: ReturnType - } -} -``` - -> `routeTree.gen.ts` is not a file you're expected to have at this point. -> It will be generated when you run TanStack Start (via `npm run dev` or `npm run start`) for the first time. - -## The Root of Your Application - -Finally, we need to create the root of our application. This is the entry point for all application routes. The code in this file will wrap all other routes in the application. - -```tsx -// app/routes/__root.tsx -import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router' -import { Outlet } from '@tanstack/react-router' -import * as React from 'react' - -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charSet: 'utf-8', - }, - { - name: 'viewport', - content: 'width=device-width, initial-scale=1', - }, - { - title: 'TanStack Start Starter', - }, - ], - }), - component: RootComponent, -}) - -function RootComponent() { - return ( - - - - ) -} - -function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - - - - - {children} - - - - ) -} -``` - -# Writing Your First Route - -Now that we have the basic templating setup, we can write our first route. This is done by creating a new file in the `app/routes` directory. - -```tsx -// app/routes/index.tsx -import * as fs from 'fs' -import { createFileRoute, useRouter } from '@tanstack/react-router' -import { createServerFn } from '@tanstack/react-start' - -const filePath = 'count.txt' - -async function readCount() { - return parseInt( - await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'), - ) -} - -const getCount = createServerFn({ - method: 'GET', -}).handler(() => { - return readCount() -}) - -const updateCount = createServerFn({ method: 'POST' }) - .validator((d: number) => d) - .handler(async ({ data }) => { - const count = await readCount() - await fs.promises.writeFile(filePath, `${count + data}`) - }) - -export const Route = createFileRoute('/')({ - component: Home, - loader: async () => await getCount(), -}) - -function Home() { - const router = useRouter() - const state = Route.useLoaderData() - - return ( - - ) -} -``` - -That's it! 🤯 You've now set up a TanStack Start project and written your first route. 🎉 - -You can now run `npm run dev` to start your server and navigate to `http://localhost:3000` to see your route in action. diff --git a/e2e/react-start/basic-react-query/package.json b/e2e/react-start/basic-react-query/package.json index 911392321c..284e0626f7 100644 --- a/e2e/react-start/basic-react-query/package.json +++ b/e2e/react-start/basic-react-query/package.json @@ -15,7 +15,7 @@ "@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-router": "workspace:^", "@tanstack/react-router-devtools": "workspace:^", - "@tanstack/react-router-with-query": "workspace:^", + "@tanstack/react-router-ssr-query": "workspace:^", "@tanstack/react-start": "workspace:^", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/e2e/react-start/basic-react-query/src/router.tsx b/e2e/react-start/basic-react-query/src/router.tsx index 7e155cd75e..6f934c4760 100644 --- a/e2e/react-start/basic-react-query/src/router.tsx +++ b/e2e/react-start/basic-react-query/src/router.tsx @@ -1,6 +1,6 @@ import { QueryClient } from '@tanstack/react-query' import { createRouter as createTanStackRouter } from '@tanstack/react-router' -import { routerWithQueryClient } from '@tanstack/react-router-with-query' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import { routeTree } from './routeTree.gen' import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' import { NotFound } from './components/NotFound' @@ -11,18 +11,19 @@ import { NotFound } from './components/NotFound' export function createRouter() { const queryClient = new QueryClient() - - return routerWithQueryClient( - createTanStackRouter({ - routeTree, - context: { queryClient }, - scrollRestoration: true, - defaultPreload: 'intent', - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , - }), + const router = createTanStackRouter({ + routeTree, + context: { queryClient }, + scrollRestoration: true, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + }) + setupRouterSsrQueryIntegration({ + router, queryClient, - ) + }) + return router } declare module '@tanstack/react-router' { diff --git a/e2e/react-start/query-integration/.gitignore b/e2e/react-start/query-integration/.gitignore new file mode 100644 index 0000000000..ca63f49885 --- /dev/null +++ b/e2e/react-start/query-integration/.gitignore @@ -0,0 +1,18 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +/build/ +/api/ +/server/build +/public/build# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-start/query-integration/.prettierignore b/e2e/react-start/query-integration/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/e2e/react-start/query-integration/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/react-start/query-integration/package.json b/e2e/react-start/query-integration/package.json new file mode 100644 index 0000000000..7916c849f6 --- /dev/null +++ b/e2e/react-start/query-integration/package.json @@ -0,0 +1,39 @@ +{ + "name": "tanstack-react-start-e2e-query-integration", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "start": "node .output/server/index.mjs", + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", + "@tanstack/react-router": "workspace:^", + "@tanstack/react-router-devtools": "workspace:^", + "@tanstack/react-router-ssr-query": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.6.0", + "vite": "^6.3.5", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/react-start/query-integration/playwright.config.ts b/e2e/react-start/query-integration/playwright.config.ts new file mode 100644 index 0000000000..4d86a1dbe7 --- /dev/null +++ b/e2e/react-start/query-integration/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await derivePort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-start/query-integration/postcss.config.mjs b/e2e/react-start/query-integration/postcss.config.mjs new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/e2e/react-start/query-integration/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/react-start/query-integration/src/queryOptions.ts b/e2e/react-start/query-integration/src/queryOptions.ts new file mode 100644 index 0000000000..6ddbb8fed5 --- /dev/null +++ b/e2e/react-start/query-integration/src/queryOptions.ts @@ -0,0 +1,16 @@ +import { queryOptions } from '@tanstack/react-query' + +export const makeQueryOptions = (key: string) => + queryOptions({ + queryKey: ['e2e-test-query-integration', key], + queryFn: async () => { + console.log('fetching query data') + await new Promise((resolve) => { + setTimeout(resolve, 500) + }) + const result = typeof window !== 'undefined' ? 'client' : 'server' + console.log('query data result', result) + return result + }, + staleTime: Infinity, + }) diff --git a/e2e/react-start/query-integration/src/routeTree.gen.ts b/e2e/react-start/query-integration/src/routeTree.gen.ts new file mode 100644 index 0000000000..e98a780a1e --- /dev/null +++ b/e2e/react-start/query-integration/src/routeTree.gen.ts @@ -0,0 +1,122 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as UseSuspenseQueryRouteImport } from './routes/useSuspenseQuery' +import { Route as UseQueryRouteImport } from './routes/useQuery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as LoaderFetchQueryTypeRouteImport } from './routes/loader-fetchQuery/$type' + +const UseSuspenseQueryRoute = UseSuspenseQueryRouteImport.update({ + id: '/useSuspenseQuery', + path: '/useSuspenseQuery', + getParentRoute: () => rootRouteImport, +} as any) +const UseQueryRoute = UseQueryRouteImport.update({ + id: '/useQuery', + path: '/useQuery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LoaderFetchQueryTypeRoute = LoaderFetchQueryTypeRouteImport.update({ + id: '/loader-fetchQuery/$type', + path: '/loader-fetchQuery/$type', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/useQuery': typeof UseQueryRoute + '/useSuspenseQuery': typeof UseSuspenseQueryRoute + '/loader-fetchQuery/$type': typeof LoaderFetchQueryTypeRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/useQuery': typeof UseQueryRoute + '/useSuspenseQuery': typeof UseSuspenseQueryRoute + '/loader-fetchQuery/$type': typeof LoaderFetchQueryTypeRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/useQuery': typeof UseQueryRoute + '/useSuspenseQuery': typeof UseSuspenseQueryRoute + '/loader-fetchQuery/$type': typeof LoaderFetchQueryTypeRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/useQuery' + | '/useSuspenseQuery' + | '/loader-fetchQuery/$type' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/useQuery' | '/useSuspenseQuery' | '/loader-fetchQuery/$type' + id: + | '__root__' + | '/' + | '/useQuery' + | '/useSuspenseQuery' + | '/loader-fetchQuery/$type' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + UseQueryRoute: typeof UseQueryRoute + UseSuspenseQueryRoute: typeof UseSuspenseQueryRoute + LoaderFetchQueryTypeRoute: typeof LoaderFetchQueryTypeRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/useSuspenseQuery': { + id: '/useSuspenseQuery' + path: '/useSuspenseQuery' + fullPath: '/useSuspenseQuery' + preLoaderRoute: typeof UseSuspenseQueryRouteImport + parentRoute: typeof rootRouteImport + } + '/useQuery': { + id: '/useQuery' + path: '/useQuery' + fullPath: '/useQuery' + preLoaderRoute: typeof UseQueryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/loader-fetchQuery/$type': { + id: '/loader-fetchQuery/$type' + path: '/loader-fetchQuery/$type' + fullPath: '/loader-fetchQuery/$type' + preLoaderRoute: typeof LoaderFetchQueryTypeRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + UseQueryRoute: UseQueryRoute, + UseSuspenseQueryRoute: UseSuspenseQueryRoute, + LoaderFetchQueryTypeRoute: LoaderFetchQueryTypeRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/react-start/query-integration/src/router.tsx b/e2e/react-start/query-integration/src/router.tsx new file mode 100644 index 0000000000..e30458c549 --- /dev/null +++ b/e2e/react-start/query-integration/src/router.tsx @@ -0,0 +1,25 @@ +import { QueryClient } from '@tanstack/react-query' +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' +import { routeTree } from './routeTree.gen' + +export function createRouter() { + const queryClient = new QueryClient() + const router = createTanStackRouter({ + routeTree, + context: { queryClient }, + scrollRestoration: true, + defaultPreload: 'intent', + }) + setupRouterSsrQueryIntegration({ + router, + queryClient, + }) + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/react-start/query-integration/src/routes/__root.tsx b/e2e/react-start/query-integration/src/routes/__root.tsx new file mode 100644 index 0000000000..3f887f9491 --- /dev/null +++ b/e2e/react-start/query-integration/src/routes/__root.tsx @@ -0,0 +1,87 @@ +/// +import { + HeadContent, + Link, + Scripts, + createRootRouteWithContext, +} from '@tanstack/react-router' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { TanStackRouterDevtoolsInProd } from '@tanstack/react-router-devtools' +import * as React from 'react' +import type { QueryClient } from '@tanstack/react-query' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {' '} + + fetchQuery (sync) + {' '} + + fetchQuery (async) + {' '} + + useQuery + {' '} + + useSuspenseQuery + {' '} +
+
+ {children} + + + + + + ) +} diff --git a/e2e/react-start/query-integration/src/routes/index.tsx b/e2e/react-start/query-integration/src/routes/index.tsx new file mode 100644 index 0000000000..5f749a7821 --- /dev/null +++ b/e2e/react-start/query-integration/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Query Integration E2E tests

+
+ ) +} diff --git a/e2e/react-start/query-integration/src/routes/loader-fetchQuery/$type.tsx b/e2e/react-start/query-integration/src/routes/loader-fetchQuery/$type.tsx new file mode 100644 index 0000000000..cc5de34e91 --- /dev/null +++ b/e2e/react-start/query-integration/src/routes/loader-fetchQuery/$type.tsx @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' +import z from 'zod' +import { makeQueryOptions } from '~/queryOptions' + +export const Route = createFileRoute('/loader-fetchQuery/$type')({ + component: RouteComponent, + params: { + parse: ({ type }) => + z + .object({ + type: z.union([z.literal('sync'), z.literal('async')]), + }) + .parse({ type }), + }, + context: ({ params }) => ({ + queryOptions: makeQueryOptions(`loader-fetchQuery-${params.type}`), + }), + loader: ({ context, params }) => { + const queryPromise = context.queryClient.fetchQuery(context.queryOptions) + if (params.type === 'sync') { + return queryPromise + } + }, + ssr: 'data-only', +}) + +function RouteComponent() { + const loaderData = Route.useLoaderData() + const context = Route.useRouteContext() + const query = useQuery(context.queryOptions) + return ( +
+
+ loader data:{' '} +
{loaderData ?? 'undefined'}
+
+
+ query data:{' '} +
{query.data ?? 'loading...'}
+
+
+ ) +} diff --git a/e2e/react-start/query-integration/src/routes/useQuery.tsx b/e2e/react-start/query-integration/src/routes/useQuery.tsx new file mode 100644 index 0000000000..223189bda1 --- /dev/null +++ b/e2e/react-start/query-integration/src/routes/useQuery.tsx @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' +import { makeQueryOptions } from '~/queryOptions' + +const qOptions = makeQueryOptions('useQuery') + +export const Route = createFileRoute('/useQuery')({ + component: RouteComponent, + ssr: true, +}) + +function RouteComponent() { + const query = useQuery(qOptions) + return ( +
+
+ query data:{' '} +
{query.data ?? 'loading...'}
+
+
+ ) +} diff --git a/e2e/react-start/query-integration/src/routes/useSuspenseQuery.tsx b/e2e/react-start/query-integration/src/routes/useSuspenseQuery.tsx new file mode 100644 index 0000000000..4c4ae3a710 --- /dev/null +++ b/e2e/react-start/query-integration/src/routes/useSuspenseQuery.tsx @@ -0,0 +1,20 @@ +import { useSuspenseQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' +import { makeQueryOptions } from '~/queryOptions' + +const qOptions = makeQueryOptions('useSuspenseQuery') + +export const Route = createFileRoute('/useSuspenseQuery')({ + component: RouteComponent, +}) + +function RouteComponent() { + const query = useSuspenseQuery(qOptions) + return ( +
+
+ query data:
{query.data}
+
+
+ ) +} diff --git a/e2e/react-start/query-integration/src/styles/app.css b/e2e/react-start/query-integration/src/styles/app.css new file mode 100644 index 0000000000..c53c870665 --- /dev/null +++ b/e2e/react-start/query-integration/src/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/react-start/query-integration/tailwind.config.mjs b/e2e/react-start/query-integration/tailwind.config.mjs new file mode 100644 index 0000000000..e49f4eb776 --- /dev/null +++ b/e2e/react-start/query-integration/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/react-start/query-integration/tests/app.spec.ts b/e2e/react-start/query-integration/tests/app.spec.ts new file mode 100644 index 0000000000..5c78722d20 --- /dev/null +++ b/e2e/react-start/query-integration/tests/app.spec.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test' +import { test } from './fixture' + +// if the query would not be streamed to the client, it would re-execute on the client +// and thus cause a hydration mismatch since the query function returns 'client' when executed on the client +test.describe('queries are streamed from the server', () => { + test('direct visit - loader on server runs fetchQuery and awaits it', async ({ + page, + }) => { + await page.goto('/loader-fetchQuery/sync') + + // wait for the query data to be streamed from the server + const queryData = page.getByTestId('query-data') + await expect(queryData).toHaveText('server') + + // the loader data should be the same as the query data + const loaderData = page.getByTestId('loader-data') + await expect(loaderData).toHaveText('server') + }) + test('direct visit - loader on server runs fetchQuery and does not await it', async ({ + page, + }) => { + await page.goto('/loader-fetchQuery/async') + + // wait for the query data to be streamed from the server + const queryData = page.getByTestId('query-data') + await expect(queryData).toHaveText('server') + + const loaderData = page.getByTestId('loader-data') + await expect(loaderData).toHaveText('undefined') + }) + + test('useSuspenseQuery', async ({ page }) => { + await page.goto('/useSuspenseQuery') + + const queryData = page.getByTestId('query-data') + await expect(queryData).toHaveText('server') + }) +}) + +test('useQuery does not execute on the server and therefore does not stream data to the client', async ({ + page, +}) => { + await page.goto('/useQuery') + + const queryData = page.getByTestId('query-data') + await expect(queryData).toHaveText('client') +}) diff --git a/e2e/react-start/query-integration/tests/fixture.ts b/e2e/react-start/query-integration/tests/fixture.ts new file mode 100644 index 0000000000..abb7b1d564 --- /dev/null +++ b/e2e/react-start/query-integration/tests/fixture.ts @@ -0,0 +1,28 @@ +import { test as base, expect } from '@playwright/test' + +export interface TestFixtureOptions { + whitelistErrors: Array +} +export const test = base.extend({ + whitelistErrors: [[], { option: true }], + page: async ({ page, whitelistErrors }, use) => { + const errorMessages: Array = [] + page.on('console', (m) => { + if (m.type() === 'error') { + const text = m.text() + for (const whitelistError of whitelistErrors) { + if ( + (typeof whitelistError === 'string' && + text.includes(whitelistError)) || + (whitelistError instanceof RegExp && whitelistError.test(text)) + ) { + return + } + } + errorMessages.push(text) + } + }) + await use(page) + expect(errorMessages).toEqual([]) + }, +}) diff --git a/e2e/react-start/query-integration/tsconfig.json b/e2e/react-start/query-integration/tsconfig.json new file mode 100644 index 0000000000..3a9fb7cd71 --- /dev/null +++ b/e2e/react-start/query-integration/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/react-start/query-integration/vite.config.ts b/e2e/react-start/query-integration/vite.config.ts new file mode 100644 index 0000000000..1df337cd40 --- /dev/null +++ b/e2e/react-start/query-integration/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart(), + ], +}) diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index d62b5625d2..a0ea568b18 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -12,7 +12,7 @@ "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-router": "^1.130.2", - "@tanstack/react-router-with-query": "^1.130.2", + "@tanstack/react-router-ssr-query": "^1.130.2", "@tanstack/react-router-devtools": "^1.130.2", "@tanstack/react-start": "^1.130.3", "react": "^19.0.0", diff --git a/examples/react/start-basic-react-query/src/router.tsx b/examples/react/start-basic-react-query/src/router.tsx index ee35b01cbf..fe06f5cd68 100644 --- a/examples/react/start-basic-react-query/src/router.tsx +++ b/examples/react/start-basic-react-query/src/router.tsx @@ -1,27 +1,26 @@ import { QueryClient } from '@tanstack/react-query' import { createRouter as createTanStackRouter } from '@tanstack/react-router' -import { routerWithQueryClient } from '@tanstack/react-router-with-query' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import { routeTree } from './routeTree.gen' import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' import { NotFound } from './components/NotFound' -// NOTE: Most of the integration code found here is experimental and will -// definitely end up in a more streamlined API in the future. This is just -// to show what's possible with the current APIs. - export function createRouter() { const queryClient = new QueryClient() - return routerWithQueryClient( - createTanStackRouter({ - routeTree, - context: { queryClient }, - defaultPreload: 'intent', - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , - }), + const router = createTanStackRouter({ + routeTree, + context: { queryClient }, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + }) + setupRouterSsrQueryIntegration({ + router, queryClient, - ) + }) + + return router } declare module '@tanstack/react-router' { diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json index 8138bd0c0f..102f8b8156 100644 --- a/examples/react/start-convex-trellaux/package.json +++ b/examples/react/start-convex-trellaux/package.json @@ -15,7 +15,7 @@ "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-router": "^1.130.2", - "@tanstack/react-router-with-query": "^1.130.2", + "@tanstack/react-router-ssr-query": "^1.130.2", "@tanstack/react-router-devtools": "^1.130.2", "@tanstack/react-start": "^1.130.3", "concurrently": "^8.2.2", diff --git a/examples/react/start-convex-trellaux/src/router.tsx b/examples/react/start-convex-trellaux/src/router.tsx index 21444891bf..c289f8b75c 100644 --- a/examples/react/start-convex-trellaux/src/router.tsx +++ b/examples/react/start-convex-trellaux/src/router.tsx @@ -4,7 +4,7 @@ import { QueryClient, notifyManager, } from '@tanstack/react-query' -import { routerWithQueryClient } from '@tanstack/react-router-with-query' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import toast from 'react-hot-toast' import { ConvexQueryClient } from '@convex-dev/react-query' import { ConvexProvider } from 'convex/react' @@ -38,22 +38,23 @@ export function createRouter() { }) convexQueryClient.connect(queryClient) - const router = routerWithQueryClient( - createTanStackRouter({ - routeTree, - defaultPreload: 'intent', - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , - context: { queryClient }, - Wrap: ({ children }) => ( - - {children} - - ), - scrollRestoration: true, - }), + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + context: { queryClient }, + Wrap: ({ children }) => ( + + {children} + + ), + scrollRestoration: true, + }) + setupRouterSsrQueryIntegration({ + router, queryClient, - ) + }) return router } diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json index 074b35ca9d..192dc0a769 100644 --- a/examples/react/start-trellaux/package.json +++ b/examples/react/start-trellaux/package.json @@ -12,7 +12,7 @@ "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-router": "^1.130.2", - "@tanstack/react-router-with-query": "^1.130.2", + "@tanstack/react-router-ssr-query": "^1.130.2", "@tanstack/react-router-devtools": "^1.130.2", "@tanstack/react-start": "^1.130.3", "ky": "^1.7.4", diff --git a/examples/react/start-trellaux/src/router.tsx b/examples/react/start-trellaux/src/router.tsx index e31d571dc6..1a6a98172d 100644 --- a/examples/react/start-trellaux/src/router.tsx +++ b/examples/react/start-trellaux/src/router.tsx @@ -4,7 +4,7 @@ import { QueryClient, notifyManager, } from '@tanstack/react-query' -import { routerWithQueryClient } from '@tanstack/react-router-with-query' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import toast from 'react-hot-toast' import { routeTree } from './routeTree.gen' import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' @@ -33,19 +33,20 @@ export function createRouter() { }), }) - const router = routerWithQueryClient( - createTanStackRouter({ - routeTree, - defaultPreload: 'intent', - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , - scrollRestoration: true, - context: { - queryClient, - }, - }), + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + context: { + queryClient, + }, + }) + setupRouterSsrQueryIntegration({ + router, queryClient, - ) + }) return router } diff --git a/labeler-config.yml b/labeler-config.yml index 202c627274..015ae4ec3a 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -16,9 +16,9 @@ 'package: react-router-devtools': - changed-files: - any-glob-to-any-file: 'packages/react-router-devtools/**/*' -'package: react-router-with-query': +'package: react-router-ssr-query': - changed-files: - - any-glob-to-any-file: 'packages/react-router-with-query/**/*' + - any-glob-to-any-file: 'packages/react-router-ssr-query/**/*' 'package: react-start': - changed-files: - any-glob-to-any-file: 'packages/react-start/**/*' @@ -49,6 +49,9 @@ 'package: router-plugin': - changed-files: - any-glob-to-any-file: 'packages/router-plugin/**/*' +'package: router-ssr-query-core': + - changed-files: + - any-glob-to-any-file: 'packages/router-ssr-query-core/**/*' 'package: router-utils': - changed-files: - any-glob-to-any-file: 'packages/router-utils/**/*' diff --git a/package.json b/package.json index a715abe9ec..946a57a86c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@playwright/test": "^1.52.0", "@tanstack/config": "^0.16.1", "@tanstack/react-query": "5.66.0", + "@tanstack/query-core": "5.66.0", "@types/node": "^22.10.2", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", @@ -77,7 +78,8 @@ "eslint": "$eslint", "vite": "$vite", "@playwright/test": "$@playwright/test", - "@tanstack/react-query": "5.66.0", + "@tanstack/react-query": "$@tanstack/react-query", + "@tanstack/query-core": "$@tanstack/query-core", "@tanstack/history": "workspace:*", "@tanstack/router-core": "workspace:*", "@tanstack/react-router": "workspace:*", @@ -90,7 +92,8 @@ "@tanstack/virtual-file-routes": "workspace:*", "@tanstack/router-plugin": "workspace:*", "@tanstack/router-vite-plugin": "workspace:*", - "@tanstack/react-router-with-query": "workspace:*", + "@tanstack/router-ssr-query-core": "workspace:*", + "@tanstack/react-router-ssr-query": "workspace:*", "@tanstack/zod-adapter": "workspace:*", "@tanstack/valibot-adapter": "workspace:*", "@tanstack/arktype-adapter": "workspace:*", diff --git a/packages/react-router-with-query/README.md b/packages/react-router-ssr-query/README.md similarity index 100% rename from packages/react-router-with-query/README.md rename to packages/react-router-ssr-query/README.md diff --git a/packages/react-router-with-query/eslint.config.js b/packages/react-router-ssr-query/eslint.config.js similarity index 100% rename from packages/react-router-with-query/eslint.config.js rename to packages/react-router-ssr-query/eslint.config.js diff --git a/packages/react-router-with-query/package.json b/packages/react-router-ssr-query/package.json similarity index 87% rename from packages/react-router-with-query/package.json rename to packages/react-router-ssr-query/package.json index fbd7356992..b98f4ca3b8 100644 --- a/packages/react-router-with-query/package.json +++ b/packages/react-router-ssr-query/package.json @@ -1,5 +1,5 @@ { - "name": "@tanstack/react-router-with-query", + "name": "@tanstack/react-router-ssr-query", "version": "1.130.2", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", @@ -7,7 +7,7 @@ "repository": { "type": "git", "url": "https://github.com/TanStack/router.git", - "directory": "packages/react-router-with-query" + "directory": "packages/react-router-ssr-query" }, "homepage": "https://tanstack.com/router", "funding": { @@ -33,7 +33,7 @@ "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", "test:types:ts58": "tsc", - "test:unit": "vitest", + "test:unit": "exit 0; vitest", "test:unit:dev": "pnpm run test:unit --watch", "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", "build": "vite build" @@ -63,19 +63,21 @@ "engines": { "node": ">=12" }, + "dependencies": { + "@tanstack/router-ssr-query-core": "workspace:*" + }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "react": ">=19", "react-dom": ">=19", - "@tanstack/router-core": "workspace:*", "@tanstack/react-router": "workspace:*", "@tanstack/react-query": ">=5.66.0" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", - "@tanstack/router-core": ">=1.114.7", - "@tanstack/react-router": ">=1.43.2", + "@tanstack/query-core": ">=5.49.0", + "@tanstack/react-router": ">=1.127.0", "@tanstack/react-query": ">=5.49.2" } } diff --git a/packages/react-router-ssr-query/src/index.tsx b/packages/react-router-ssr-query/src/index.tsx new file mode 100644 index 0000000000..c060cc98dc --- /dev/null +++ b/packages/react-router-ssr-query/src/index.tsx @@ -0,0 +1,29 @@ +import { Fragment } from 'react' +import { QueryClientProvider } from '@tanstack/react-query' +import { setupCoreRouterSsrQueryIntegration } from '@tanstack/router-ssr-query-core' +import type { RouterSsrQueryOptions } from '@tanstack/router-ssr-query-core' +import type { AnyRouter } from '@tanstack/react-router' + +export type Options = + RouterSsrQueryOptions & { + wrapQueryClient?: boolean + } + +export function setupRouterSsrQueryIntegration( + opts: Options, +) { + setupCoreRouterSsrQueryIntegration(opts) + + if (opts.wrapQueryClient === false) { + return + } + const OGWrap = opts.router.options.Wrap || Fragment + + opts.router.options.Wrap = ({ children }) => { + return ( + + {children} + + ) + } +} diff --git a/packages/react-router-with-query/tsconfig.json b/packages/react-router-ssr-query/tsconfig.json similarity index 100% rename from packages/react-router-with-query/tsconfig.json rename to packages/react-router-ssr-query/tsconfig.json diff --git a/packages/react-router-with-query/vite.config.ts b/packages/react-router-ssr-query/vite.config.ts similarity index 100% rename from packages/react-router-with-query/vite.config.ts rename to packages/react-router-ssr-query/vite.config.ts diff --git a/packages/react-router-with-query/tests/index.test-d.ts b/packages/react-router-with-query/tests/index.test-d.ts deleted file mode 100644 index 77a004ce07..0000000000 --- a/packages/react-router-with-query/tests/index.test-d.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { QueryClient } from '@tanstack/react-query' -import { - createRootRouteWithContext, - createRouter, -} from '@tanstack/react-router' - -import { expectTypeOf, test } from 'vitest' - -import { routerWithQueryClient } from '../src' - -test('basic { queryClient } context', () => { - const root = createRootRouteWithContext<{ - queryClient: QueryClient - }>()({}) - - const queryClient = new QueryClient() - const router = createRouter({ - context: { queryClient }, - routeTree: root, - }) - - const routerWithQuery = routerWithQueryClient(router, queryClient) - expectTypeOf(routerWithQuery).toEqualTypeOf(router) -}) - -test('no context fails', () => { - const root = createRootRouteWithContext()({}) - - const queryClient = new QueryClient() - const router = createRouter({ - routeTree: root, - }) - - routerWithQueryClient( - // @ts-expect-error - QueryClient must be in context type - router, - queryClient, - ) -}) - -test('allows additional props on context', () => { - const root = createRootRouteWithContext<{ - queryClient: QueryClient - extra: string - }>()({}) - - const queryClient = new QueryClient() - const router = createRouter({ - context: { queryClient, extra: 'extra' }, - routeTree: root, - }) - - const routerWithQuery = routerWithQueryClient(router, queryClient) - expectTypeOf(routerWithQuery).toEqualTypeOf(router) -}) diff --git a/packages/router-ssr-query-core/README.md b/packages/router-ssr-query-core/README.md new file mode 100644 index 0000000000..b3a422c890 --- /dev/null +++ b/packages/router-ssr-query-core/README.md @@ -0,0 +1,5 @@ + + +# TanStack Router Core + +See [https://tanstack.com/router](https://tanstack.com/router) for documentation. diff --git a/packages/router-ssr-query-core/eslint.config.js b/packages/router-ssr-query-core/eslint.config.js new file mode 100644 index 0000000000..657b7e420b --- /dev/null +++ b/packages/router-ssr-query-core/eslint.config.js @@ -0,0 +1,14 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], + rules: { + 'unused-imports/no-unused-vars': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +] diff --git a/packages/router-ssr-query-core/package.json b/packages/router-ssr-query-core/package.json new file mode 100644 index 0000000000..9ecec38dea --- /dev/null +++ b/packages/router-ssr-query-core/package.json @@ -0,0 +1,74 @@ +{ + "name": "@tanstack/router-ssr-query-core", + "version": "1.127.3", + "description": "Modern and scalable routing for React applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/router-ssr-query-core" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "react", + "location", + "router", + "routing", + "async", + "async router", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint ./src", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", + "test:types:ts58": "tsc", + "test:unit": "exit 0; vitest", + "test:unit:dev": "pnpm run test:unit --watch", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "devDependencies": { + "@tanstack/router-core": ">=1.127.0", + "@tanstack/query-core": ">=5.49.0" + }, + "peerDependencies": { + "@tanstack/router-core": ">=1.127.0", + "@tanstack/query-core": ">=5.49.0" + } +} diff --git a/packages/react-router-with-query/src/index.tsx b/packages/router-ssr-query-core/src/index.ts similarity index 58% rename from packages/react-router-with-query/src/index.tsx rename to packages/router-ssr-query-core/src/index.ts index af0d3eb951..a692c8c28f 100644 --- a/packages/react-router-with-query/src/index.tsx +++ b/packages/router-ssr-query-core/src/index.ts @@ -1,19 +1,18 @@ -import { Fragment } from 'react' import { - QueryClientProvider, dehydrate as queryDehydrate, hydrate as queryHydrate, -} from '@tanstack/react-query' +} from '@tanstack/query-core' import { isRedirect } from '@tanstack/router-core' -import '@tanstack/router-core/ssr/client' -import type { AnyRouter } from '@tanstack/react-router' +import type { AnyRouter } from '@tanstack/router-core' import type { QueryClient, DehydratedState as QueryDehydratedState, -} from '@tanstack/react-query' +} from '@tanstack/query-core' + +export type RouterSsrQueryOptions = { + router: TRouter + queryClient: QueryClient -type AdditionalOptions = { - WrapProvider?: (props: { children: any }) => React.JSX.Element /** * If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries. * @@ -24,62 +23,41 @@ type AdditionalOptions = { } type DehydratedRouterQueryState = { - dehydratedQueryClient: QueryDehydratedState + dehydratedQueryClient?: QueryDehydratedState queryStream: ReadableStream } -export type ValidateRouter = - NonNullable extends { - queryClient: QueryClient - } - ? TRouter - : never - -export function routerWithQueryClient( - router: ValidateRouter, - queryClient: QueryClient, - additionalOpts?: AdditionalOptions, -): TRouter { - const ogOptions = router.options - - router.options = { - ...router.options, - context: { - ...ogOptions.context, - // Pass the query client to the context, so we can access it in loaders - queryClient, - }, - // Wrap the app in a QueryClientProvider - Wrap: ({ children }) => { - const OuterWrapper = additionalOpts?.WrapProvider || Fragment - const OGWrap = ogOptions.Wrap || Fragment - return ( - - - {children} - - - ) - }, - } + +export function setupCoreRouterSsrQueryIntegration({ + router, + queryClient, + handleRedirects = true, +}: RouterSsrQueryOptions) { + const ogHydrate = router.options.hydrate + const ogDehydrate = router.options.dehydrate if (router.isServer) { + const sentQueries = new Set() const queryStream = createPushableStream() router.options.dehydrate = async (): Promise => { - const ogDehydrated = await ogOptions.dehydrate?.() - const dehydratedQueryClient = queryDehydrate(queryClient) - router.serverSsr!.onRenderFinished(() => queryStream.close()) + const ogDehydrated = await ogDehydrate?.() const dehydratedRouter = { ...ogDehydrated, - // When critical data is dehydrated, we also dehydrate the query client - dehydratedQueryClient, // prepare the stream for queries coming up during rendering queryStream: queryStream.stream, } + const dehydratedQueryClient = queryDehydrate(queryClient) + if (dehydratedQueryClient.queries.length > 0) { + dehydratedQueryClient.queries.forEach((query) => { + sentQueries.add(query.queryHash) + }) + dehydratedRouter.dehydratedQueryClient = dehydratedQueryClient + } + return dehydratedRouter } @@ -93,40 +71,49 @@ export function routerWithQueryClient( }) queryClient.getQueryCache().subscribe((event) => { - if (event.type === 'added') { - // before rendering starts, we do not stream individual queries - // instead we dehydrate the entire query client in router's dehydrate() - if (!router.serverSsr!.isDehydrated()) { - return - } - if (queryStream.isClosed()) { - console.warn( - `tried to stream query ${event.query.queryHash} after stream was already closed`, - ) - return - } - queryStream.enqueue( - queryDehydrate(queryClient, { - shouldDehydrateQuery: (query) => { - if (query.queryHash === event.query.queryHash) { - return ( - ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ?? - true - ) - } - return false - }, - }), + // before rendering starts, we do not stream individual queries + // instead we dehydrate the entire query client in router's dehydrate() + // if attachRouterServerSsrUtils() has not been called yet, `router.serverSsr` will be undefined and we also do not stream + if (!router.serverSsr?.isDehydrated()) { + return + } + if (sentQueries.has(event.query.queryHash)) { + return + } + if (queryStream.isClosed()) { + console.warn( + `tried to stream query ${event.query.queryHash} after stream was already closed`, ) + return + } + // promise not yet set on the query, so we cannot stream it yet + if (!event.query.promise) { + return } + sentQueries.add(event.query.queryHash) + queryStream.enqueue( + queryDehydrate(queryClient, { + shouldDehydrateQuery: (query) => { + if (query.queryHash === event.query.queryHash) { + return ( + ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ?? true + ) + } + return false + }, + }), + ) }) // on the client } else { router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => { - await ogOptions.hydrate?.(dehydrated) - // On the client, hydrate the query client with the dehydrated data - queryHydrate(queryClient, dehydrated.dehydratedQueryClient) + await ogHydrate?.(dehydrated) + // hydrate the query client with the dehydrated data (if it was dehydrated on the server) + if (dehydrated.dehydratedQueryClient) { + queryHydrate(queryClient, dehydrated.dehydratedQueryClient) + } + // read the query stream and hydrate the queries as they come in const reader = dehydrated.queryStream.getReader() reader .read() @@ -142,7 +129,7 @@ export function routerWithQueryClient( console.error('Error reading query stream:', err) }) } - if (additionalOpts?.handleRedirects ?? true) { + if (handleRedirects) { const ogMutationCacheConfig = queryClient.getMutationCache().config queryClient.getMutationCache().config = { ...ogMutationCacheConfig, @@ -175,8 +162,6 @@ export function routerWithQueryClient( } } } - - return router } type PushableStream = { diff --git a/packages/router-ssr-query-core/tsconfig.json b/packages/router-ssr-query-core/tsconfig.json new file mode 100644 index 0000000000..e99dc8130a --- /dev/null +++ b/packages/router-ssr-query-core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "vite.config.ts", "tests", "vite-minify-plugin.ts"] +} diff --git a/packages/router-ssr-query-core/vite.config.ts b/packages/router-ssr-query-core/vite.config.ts new file mode 100644 index 0000000000..f5bd8a0b05 --- /dev/null +++ b/packages/router-ssr-query-core/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import packageJson from './package.json' +import type { ViteUserConfig } from 'vitest/config' + +const config = defineConfig({ + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + typecheck: { enabled: true }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts'], + srcDir: './src', + }), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 686d27f0dd..96d4addd41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ overrides: vite: 6.3.5 '@playwright/test': ^1.52.0 '@tanstack/react-query': 5.66.0 + '@tanstack/query-core': 5.66.0 '@tanstack/history': workspace:* '@tanstack/router-core': workspace:* '@tanstack/react-router': workspace:* @@ -25,7 +26,8 @@ overrides: '@tanstack/virtual-file-routes': workspace:* '@tanstack/router-plugin': workspace:* '@tanstack/router-vite-plugin': workspace:* - '@tanstack/react-router-with-query': workspace:* + '@tanstack/router-ssr-query-core': workspace:* + '@tanstack/react-router-ssr-query': workspace:* '@tanstack/zod-adapter': workspace:* '@tanstack/valibot-adapter': workspace:* '@tanstack/arktype-adapter': workspace:* @@ -65,6 +67,9 @@ importers: '@tanstack/config': specifier: ^0.16.1 version: 0.16.1(@types/node@22.13.4)(esbuild@0.25.4)(eslint@9.22.0(jiti@2.4.2))(rollup@4.41.1)(typescript@5.8.2)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + '@tanstack/query-core': + specifier: 5.66.0 + version: 5.66.0 '@tanstack/react-query': specifier: 5.66.0 version: 5.66.0(react@19.0.0) @@ -1084,9 +1089,9 @@ importers: '@tanstack/react-router-devtools': specifier: workspace:^ version: link:../../../packages/react-router-devtools - '@tanstack/react-router-with-query': + '@tanstack/react-router-ssr-query': specifier: workspace:* - version: link:../../../packages/react-router-with-query + version: link:../../../packages/react-router-ssr-query '@tanstack/react-start': specifier: workspace:* version: link:../../../packages/react-start @@ -1296,6 +1301,76 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.2)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/react-start/query-integration: + dependencies: + '@tanstack/react-query': + specifier: 5.66.0 + version: 5.66.0(react@19.0.0) + '@tanstack/react-query-devtools': + specifier: ^5.66.0 + version: 5.67.2(@tanstack/react-query@5.66.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-router-devtools': + specifier: workspace:^ + version: link:../../../packages/react-router-devtools + '@tanstack/react-router-ssr-query': + specifier: workspace:* + version: link:../../../packages/react-router-ssr-query + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + react: + specifier: ^19.0.0 + version: 19.0.0 + react-dom: + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vite: + specifier: 6.3.5 + version: 6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0) + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.52.0 + version: 1.52.0 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + '@types/react': + specifier: ^19.0.8 + version: 19.0.8 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.0.3(@types/react@19.0.8) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.6.0(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.3) + postcss: + specifier: ^8.5.1 + version: 8.5.3 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.8.3 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/react-start/scroll-restoration: dependencies: '@tanstack/react-router': @@ -3979,10 +4054,10 @@ importers: version: 19.0.3(@types/react@19.0.8) html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(@rspack/core@1.2.2(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 5.6.3(@rspack/core@1.2.2(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.10.15(@swc/helpers@0.5.15))(webpack@5.97.1) + version: 0.2.6(@swc/core@1.10.15(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) typescript: specifier: ^5.7.2 version: 5.8.2 @@ -4817,9 +4892,9 @@ importers: '@tanstack/react-router-devtools': specifier: workspace:^ version: link:../../../packages/react-router-devtools - '@tanstack/react-router-with-query': + '@tanstack/react-router-ssr-query': specifier: workspace:* - version: link:../../../packages/react-router-with-query + version: link:../../../packages/react-router-ssr-query '@tanstack/react-start': specifier: workspace:* version: link:../../../packages/react-start @@ -5040,9 +5115,9 @@ importers: '@tanstack/react-router-devtools': specifier: workspace:^ version: link:../../../packages/react-router-devtools - '@tanstack/react-router-with-query': + '@tanstack/react-router-ssr-query': specifier: workspace:* - version: link:../../../packages/react-router-with-query + version: link:../../../packages/react-router-ssr-query '@tanstack/react-start': specifier: workspace:* version: link:../../../packages/react-start @@ -5367,9 +5442,9 @@ importers: '@tanstack/react-router-devtools': specifier: workspace:^ version: link:../../../packages/react-router-devtools - '@tanstack/react-router-with-query': + '@tanstack/react-router-ssr-query': specifier: workspace:* - version: link:../../../packages/react-router-with-query + version: link:../../../packages/react-router-ssr-query '@tanstack/react-start': specifier: workspace:* version: link:../../../packages/react-start @@ -6373,7 +6448,14 @@ importers: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) - packages/react-router-with-query: + packages/react-router-ssr-query: + dependencies: + '@tanstack/query-core': + specifier: 5.66.0 + version: 5.66.0 + '@tanstack/router-ssr-query-core': + specifier: workspace:* + version: link:../router-ssr-query-core devDependencies: '@tanstack/react-query': specifier: 5.66.0 @@ -6381,12 +6463,9 @@ importers: '@tanstack/react-router': specifier: workspace:* version: link:../react-router - '@tanstack/router-core': - specifier: workspace:* - version: link:../router-core '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + version: 4.6.0(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) react: specifier: ^19.0.0 version: 19.0.0 @@ -6719,6 +6798,15 @@ importers: specifier: ^7.20.7 version: 7.20.7 + packages/router-ssr-query-core: + devDependencies: + '@tanstack/query-core': + specifier: 5.66.0 + version: 5.66.0 + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + packages/router-utils: dependencies: '@babel/core': @@ -10324,9 +10412,6 @@ packages: '@tanstack/query-core@5.66.0': resolution: {integrity: sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==} - '@tanstack/query-core@5.72.2': - resolution: {integrity: sha512-fxl9/0yk3mD/FwTmVEf1/H6N5B975H0luT+icKyX566w6uJG0x6o+Yl+I38wJRCaogiMkstByt+seXfDbWDAcA==} - '@tanstack/query-devtools@5.67.2': resolution: {integrity: sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg==} @@ -19182,8 +19267,6 @@ snapshots: '@tanstack/query-core@5.66.0': {} - '@tanstack/query-core@5.72.2': {} - '@tanstack/query-devtools@5.67.2': {} '@tanstack/query-devtools@5.72.2': {} @@ -19220,7 +19303,7 @@ snapshots: '@tanstack/solid-query@5.72.2(solid-js@1.9.5)': dependencies: - '@tanstack/query-core': 5.72.2 + '@tanstack/query-core': 5.66.0 solid-js: 1.9.5 '@tanstack/solid-store@0.7.0(solid-js@1.9.5)': @@ -19943,17 +20026,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.1)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.0)(webpack@5.97.1)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack-dev-server@5.2.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1) @@ -21992,7 +22075,7 @@ snapshots: html-tags@3.3.1: {} - html-webpack-plugin@5.6.3(@rspack/core@1.2.2(@swc/helpers@0.5.15))(webpack@5.97.1): + html-webpack-plugin@5.6.3(@rspack/core@1.2.2(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -24302,7 +24385,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swc-loader@0.2.6(@swc/core@1.10.15(@swc/helpers@0.5.15))(webpack@5.97.1): + swc-loader@0.2.6(@swc/core@1.10.15(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)): dependencies: '@swc/core': 1.10.15(@swc/helpers@0.5.15) '@swc/counter': 0.1.3 @@ -24374,26 +24457,26 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser-webpack-plugin@5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)): + terser-webpack-plugin@5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 terser: 5.37.0 - webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4) + webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.10.15(@swc/helpers@0.5.15) esbuild: 0.25.4 - terser-webpack-plugin@5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack@5.97.1): + terser-webpack-plugin@5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 terser: 5.37.0 - webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4) + webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4) optionalDependencies: '@swc/core': 1.10.15(@swc/helpers@0.5.15) esbuild: 0.25.4 @@ -25052,9 +25135,9 @@ snapshots: webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.1) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.0)(webpack@5.97.1) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.2.0)(webpack@5.97.1))(webpack-dev-server@5.2.0(webpack-cli@5.1.4)(webpack@5.97.1))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -25068,7 +25151,7 @@ snapshots: optionalDependencies: webpack-dev-server: 5.2.0(webpack-cli@5.1.4)(webpack@5.97.1) - webpack-dev-middleware@7.4.2(webpack@5.97.1): + webpack-dev-middleware@7.4.2(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)): dependencies: colorette: 2.0.20 memfs: 4.17.0 @@ -25106,7 +25189,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.97.1) + webpack-dev-middleware: 7.4.2(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) ws: 8.18.0 optionalDependencies: webpack: 5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4) @@ -25181,7 +25264,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack@5.97.1) + terser-webpack-plugin: 5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))(esbuild@0.25.4)(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: diff --git a/scripts/publish.js b/scripts/publish.js index 0b38f0510c..a89e4fd5e6 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -25,8 +25,8 @@ await publish({ packageDir: 'packages/react-router', }, { - name: '@tanstack/react-router-with-query', - packageDir: 'packages/react-router-with-query', + name: '@tanstack/react-router-ssr-query', + packageDir: 'packages/react-router-ssr-query', }, { name: '@tanstack/zod-adapter',