diff --git a/.changeset/intent-skills-audit.md b/.changeset/intent-skills-audit.md new file mode 100644 index 00000000000..bbda87b47fe --- /dev/null +++ b/.changeset/intent-skills-audit.md @@ -0,0 +1,21 @@ +--- +'@tanstack/router-core': patch +'@tanstack/react-router': patch +'@tanstack/react-start': patch +'@tanstack/solid-router': patch +'@tanstack/solid-start': patch +'@tanstack/vue-router': patch +'@tanstack/vue-start': patch +'@tanstack/router-plugin': patch +'@tanstack/virtual-file-routes': patch +'@tanstack/start-client-core': patch +'@tanstack/start-server-core': patch +--- + +feat: add TanStack Intent skills and fix publishing + +- Fix `@tanstack/react-router` to publish skills (add `skills` to files array, `@tanstack/intent` devDep, bin entry) +- Add `tanstack-intent` keyword to all 11 skill-bearing packages for npm discoverability +- Create standalone `route-masking` skill (extracted from `not-found-and-errors`) +- Add TanStack Query composition skills for `solid-router` and `vue-router` +- Update `_artifacts/skill_tree.yaml` with all Start, framework, plugin, and new skills diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index b5ce76f5474..874e3a336d7 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -1,15 +1,16 @@ # skills/_artifacts/skill_tree.yaml library: name: '@tanstack/router' - version: '1.166.2' + version: '1.168.15' repository: 'https://github.com/TanStack/router' description: >- - Type-safe router for React and Solid with built-in SWR caching, - JSON-first search params, and end-to-end type inference. + Type-safe router for React, Solid, and Vue with built-in SWR caching, + JSON-first search params, and end-to-end type inference. Includes + TanStack Start full-stack framework skills. generated_from: domain_map: '_artifacts/domain_map.yaml' skill_spec: '_artifacts/skill_spec.md' -generated_at: '2026-03-07' +generated_at: '2026-04-11' skills: # ── Router Core Skills ─────────────────────────────────────────── @@ -168,12 +169,28 @@ skills: description: >- notFound() function, notFoundComponent, defaultNotFoundComponent, notFoundMode (fuzzy/root), errorComponent, onError/onCatch, - CatchBoundary, NotFoundRoute (deprecated), route masking (mask - option, createRouteMask, unmaskOnReload). + CatchBoundary, NotFoundRoute (deprecated). requires: - 'router-core' sources: - 'TanStack/router:docs/router/guide/not-found-errors.md' + + - name: 'Route Masking' + slug: 'router-core/route-masking' + type: 'sub-skill' + domain: 'rendering-and-layout' + path: 'skills/router-core/route-masking/SKILL.md' + package: 'packages/router-core' + description: >- + Route masking: showing a different URL in the browser bar than + the actual route being rendered. mask option on Link/navigate, + createRouteMask for declarative masking, unmaskOnReload, + type-safe mask options, location.state-based storage, automatic + unmasking on URL sharing. + requires: + - 'router-core' + - 'router-core/navigation' + sources: - 'TanStack/router:docs/router/guide/route-masking.md' - name: 'Type Safety' @@ -259,6 +276,44 @@ skills: - 'TanStack/router:docs/router/guide/external-data-loading.md' - 'TanStack/router:docs/router/integrations/query.md' + - name: 'External Data Loading — Solid (TanStack Query)' + slug: 'compositions/router-query-solid' + type: 'composition' + domain: 'loading-data' + path: 'skills/compositions/router-query/SKILL.md' + package: 'packages/solid-router' + description: >- + Integrating TanStack Solid Router with TanStack Query: queryClient + in router context, ensureQueryData/prefetchQuery in loaders, + createQuery in components, defaultPreloadStaleTime: 0, + per-request QueryClient isolation for SSR. + requires: + - 'router-core' + - 'router-core/data-loading' + - 'solid-router' + sources: + - 'TanStack/router:docs/router/guide/external-data-loading.md' + - 'TanStack/router:docs/router/integrations/query.md' + + - name: 'External Data Loading — Vue (TanStack Query)' + slug: 'compositions/router-query-vue' + type: 'composition' + domain: 'loading-data' + path: 'skills/compositions/router-query/SKILL.md' + package: 'packages/vue-router' + description: >- + Integrating TanStack Vue Router with TanStack Query: queryClient + in router context, ensureQueryData/prefetchQuery in loaders, + useQuery in components, defaultPreloadStaleTime: 0, + per-request QueryClient isolation for SSR. + requires: + - 'router-core' + - 'router-core/data-loading' + - 'vue-router' + sources: + - 'TanStack/router:docs/router/guide/external-data-loading.md' + - 'TanStack/router:docs/router/integrations/query.md' + # ── Lifecycle Skills ───────────────────────────────────────────── - name: 'Migrate from React Router' slug: 'lifecycle/migrate-from-react-router' @@ -277,3 +332,263 @@ skills: sources: - 'TanStack/router:docs/router/how-to/migrate-from-react-router.md' - 'TanStack/router:docs/router/installation/migrate-from-react-router.md' + + # ── Solid Router Skills ───────────────────────────────────────── + - name: 'Solid Router' + slug: 'solid-router' + type: 'framework' + domain: 'defining-routes' + path: 'skills/solid-router/SKILL.md' + package: 'packages/solid-router' + description: >- + Solid bindings for TanStack Router: RouterProvider, useRouter, + useRouterState, useMatch, useMatches, useLocation, useSearch, + useParams, useNavigate, useLoaderData, useLoaderDeps, + useRouteContext, useBlocker, useCanGoBack, Link, Navigate, + Outlet, CatchBoundary, ErrorComponent. Solid-specific patterns + with Accessor returns, createSignal/createMemo/createEffect, + Show/Switch/Match/Dynamic, and @solidjs/meta for head management. + requires: + - 'router-core' + sources: + - 'TanStack/router:packages/solid-router/src' + + # ── Vue Router Skills ─────────────────────────────────────────── + - name: 'Vue Router' + slug: 'vue-router' + type: 'framework' + domain: 'defining-routes' + path: 'skills/vue-router/SKILL.md' + package: 'packages/vue-router' + description: >- + Vue bindings for TanStack Router: RouterProvider, useRouter, + useRouterState, useMatch, useMatches, useLocation, useSearch, + useParams, useNavigate, useLoaderData, useLoaderDeps, + useRouteContext, useBlocker, useCanGoBack, Link, Navigate, + Outlet, CatchBoundary, ErrorComponent, Html, Body. + Vue-specific patterns with Ref returns, defineComponent, + h() render functions, provide/inject, and computed refs. + requires: + - 'router-core' + sources: + - 'TanStack/router:packages/vue-router/src' + + # ── Router Plugin & Virtual File Routes ───────────────────────── + - name: 'Router Plugin' + slug: 'router-plugin' + type: 'core' + domain: 'defining-routes' + path: 'skills/router-plugin/SKILL.md' + package: 'packages/router-plugin' + description: >- + TanStack Router bundler plugin for route generation and automatic + code splitting. Supports Vite, Webpack, Rspack, and esbuild. + Configures autoCodeSplitting, routesDirectory, target framework, + and code split groupings. + sources: + - 'TanStack/router:packages/router-plugin/src' + - 'TanStack/router:docs/router/routing/file-based-routing.md' + - 'TanStack/router:docs/router/guide/code-splitting.md' + + - name: 'Virtual File Routes' + slug: 'virtual-file-routes' + type: 'core' + domain: 'defining-routes' + path: 'skills/virtual-file-routes/SKILL.md' + package: 'packages/virtual-file-routes' + description: >- + Programmatic route tree building as an alternative to filesystem + conventions: rootRoute, index, route, layout, physical, + defineVirtualSubtreeConfig. Use with TanStack Router plugin's + virtualRouteConfig option. + sources: + - 'TanStack/router:packages/virtual-file-routes/src' + - 'TanStack/router:docs/router/routing/virtual-file-routes.md' + + # ── Start Core Skills ─────────────────────────────────────────── + - name: 'Start Core' + slug: 'start-core' + type: 'core' + domain: 'defining-routes' + path: 'skills/start-core/SKILL.md' + package: 'packages/start-client-core' + description: >- + Core overview for TanStack Start: tanstackStart() Vite plugin, + getRouter() factory, root route document shell (HeadContent, + Scripts, Outlet), client/server entry points, routeTree.gen.ts, + tsconfig configuration. Entry point for all Start skills. + sources: + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + - 'TanStack/router:docs/start/framework/react/quick-start.md' + - 'TanStack/router:docs/start/framework/react/guide/routing.md' + + - name: 'Server Functions' + slug: 'start-core/server-functions' + type: 'sub-skill' + domain: 'loading-data' + path: 'skills/start-core/server-functions/SKILL.md' + package: 'packages/start-client-core' + description: >- + createServerFn (GET/POST), inputValidator (Zod or function), + useServerFn hook, server context utilities (getRequest, + getRequestHeader, setResponseHeader, setResponseStatus), error + handling (throw errors, redirect, notFound), streaming, FormData + handling, file organization (.functions.ts, .server.ts). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + + - name: 'Start Middleware' + slug: 'start-core/middleware' + type: 'sub-skill' + domain: 'protecting-routes' + path: 'skills/start-core/middleware/SKILL.md' + package: 'packages/start-client-core' + description: >- + createMiddleware, request middleware (.server only), server function + middleware (.client + .server), context passing via next({ context }), + sendContext for client-server transfer, global middleware via + createStart in src/start.ts, middleware factories, method order + enforcement, fetch override precedence. + requires: + - 'start-core' + - 'start-core/server-functions' + sources: + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + + - name: 'Execution Model' + slug: 'start-core/execution-model' + type: 'sub-skill' + domain: 'server-side-rendering' + path: 'skills/start-core/execution-model/SKILL.md' + package: 'packages/start-client-core' + description: >- + Isomorphic-by-default principle, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), ClientOnly component, useHydrated hook, + import protection, dead code elimination, environment variable + safety (VITE_ prefix, process.env). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' + + - name: 'Server Routes' + slug: 'start-core/server-routes' + type: 'sub-skill' + domain: 'defining-routes' + path: 'skills/start-core/server-routes/SKILL.md' + package: 'packages/start-client-core' + description: >- + Server-side API endpoints using the server property on + createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), + createHandlers for per-handler middleware, handler context + (request, params, context), request body parsing, response + helpers, file naming for API routes. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-routes.md' + + - name: 'Deployment' + slug: 'start-core/deployment' + type: 'sub-skill' + domain: 'server-side-rendering' + path: 'skills/start-core/deployment/SKILL.md' + package: 'packages/start-client-core' + description: >- + Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, + Bun, Railway. Selective SSR (ssr option per route), SPA mode, + static prerendering, ISR with Cache-Control headers, SEO and + head management. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/hosting.md' + - 'TanStack/router:docs/start/framework/react/guide/selective-ssr.md' + + # ── Start Server Core Skills ──────────────────────────────────── + - name: 'Start Server Core' + slug: 'start-server-core' + type: 'core' + domain: 'server-side-rendering' + path: 'skills/start-server-core/SKILL.md' + package: 'packages/start-server-core' + description: >- + Server-side runtime for TanStack Start: createStartHandler, + request/response utilities (getRequest, setResponseHeader, + setCookie, getCookie, useSession), three-phase request handling, + AsyncLocalStorage context. + sources: + - 'TanStack/router:packages/start-server-core/src' + - 'TanStack/router:docs/start/framework/react/guide/server-entry-point.md' + + # ── Framework Start Skills ────────────────────────────────────── + - name: 'React Start' + slug: 'react-start' + type: 'framework' + domain: 'defining-routes' + path: 'skills/react-start/SKILL.md' + package: 'packages/react-start' + description: >- + React bindings for TanStack Start: createStart, StartClient, + StartServer, React-specific imports, re-exports from + @tanstack/react-router, full project setup with React, useServerFn + hook. + requires: + - 'start-core' + sources: + - 'TanStack/router:packages/react-start/src' + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + + - name: 'Migrate from Next.js' + slug: 'lifecycle/migrate-from-nextjs' + type: 'lifecycle' + domain: 'defining-routes' + path: 'skills/lifecycle/migrate-from-nextjs/SKILL.md' + package: 'packages/react-start' + description: >- + Step-by-step migration from Next.js App Router to TanStack Start: + route definition conversion, API mapping, server function + conversion from Server Actions, middleware conversion, data + fetching pattern changes. + requires: + - 'start-core' + - 'react-start' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' + + - name: 'Solid Start' + slug: 'solid-start' + type: 'framework' + domain: 'defining-routes' + path: 'skills/solid-start/SKILL.md' + package: 'packages/solid-start' + description: >- + Solid bindings for TanStack Start: useServerFn hook, tanstackStart + Vite plugin, StartClient, StartServer, Solid-specific setup, + re-exports from @tanstack/start-client-core. Full project setup + with Solid. + requires: + - 'start-core' + sources: + - 'TanStack/router:packages/solid-start/src' + + - name: 'Vue Start' + slug: 'vue-start' + type: 'framework' + domain: 'defining-routes' + path: 'skills/vue-start/SKILL.md' + package: 'packages/vue-start' + description: >- + Vue bindings for TanStack Start: useServerFn hook, tanstackStart + Vite plugin, StartClient, StartServer, Vue-specific setup, + re-exports from @tanstack/start-client-core. Full project setup + with Vue. + requires: + - 'start-core' + sources: + - 'TanStack/router:packages/vue-start/src' diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 26e9cd74ab3..09df1563203 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", @@ -90,7 +91,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" @@ -102,6 +106,7 @@ "isbot": "^5.1.22" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@vitejs/plugin-react": "^4.3.4", @@ -116,5 +121,8 @@ "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/react-start/package.json b/packages/react-start/package.json index 8a097b13acc..4b3b6f80e88 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/router-core/package.json b/packages/router-core/package.json index 486d7b93400..eeeee783d36 100644 --- a/packages/router-core/package.json +++ b/packages/router-core/package.json @@ -16,7 +16,8 @@ }, "keywords": [ "history", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/router-core/skills/router-core/SKILL.md b/packages/router-core/skills/router-core/SKILL.md index de11b17d288..9fb7bf3cdf6 100644 --- a/packages/router-core/skills/router-core/SKILL.md +++ b/packages/router-core/skills/router-core/SKILL.md @@ -29,6 +29,7 @@ TanStack Router is a type-safe router for React and Solid with built-in SWR cach | Auth guards, RBAC, beforeLoad redirects | [router-core/auth-and-guards/SKILL.md](./auth-and-guards/SKILL.md) | | Automatic and manual code splitting | [router-core/code-splitting/SKILL.md](./code-splitting/SKILL.md) | | 404 handling, error boundaries, notFound() | [router-core/not-found-and-errors/SKILL.md](./not-found-and-errors/SKILL.md) | +| Show different URL in browser bar, modal patterns | [router-core/route-masking/SKILL.md](./route-masking/SKILL.md) | | Inference, Register, from narrowing, TS perf | [router-core/type-safety/SKILL.md](./type-safety/SKILL.md) | | Streaming/non-streaming SSR, hydration, head mgmt | [router-core/ssr/SKILL.md](./ssr/SKILL.md) | @@ -59,6 +60,9 @@ Need to reduce bundle size per route? Need custom 404 or error handling? → router-core/not-found-and-errors +Need to show a different URL in the browser bar (modal patterns)? + → router-core/route-masking + Having TypeScript issues or performance problems? → router-core/type-safety diff --git a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md index b4fde048f86..deeb4348bbe 100644 --- a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md +++ b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md @@ -3,8 +3,7 @@ name: router-core/not-found-and-errors description: >- notFound() function, notFoundComponent, defaultNotFoundComponent, notFoundMode (fuzzy/root), errorComponent, CatchBoundary, - CatchNotFound, isNotFound, NotFoundRoute (deprecated), route - masking (mask option, createRouteMask, unmaskOnReload). + CatchNotFound, isNotFound, NotFoundRoute (deprecated). type: sub-skill library: tanstack-router library_version: '1.166.2' @@ -12,7 +11,6 @@ requires: - router-core sources: - TanStack/router:docs/router/guide/not-found-errors.md - - TanStack/router:docs/router/guide/route-masking.md --- # Not Found and Errors @@ -253,108 +251,6 @@ notFoundComponent: ({ data }) => { }, ``` -## Route Masking - -Route masking shows a different URL in the browser bar than the actual route being rendered. Masking data is stored in `location.state` and is lost when the URL is shared or opened in a new tab. - -### Imperative Masking on `` - -```tsx -import { Link } from '@tanstack/react-router' - -function PhotoGrid({ photoId }: { photoId: string }) { - return ( - - Open Photo - - ) -} -``` - -### Imperative Masking with `useNavigate` - -```tsx -import { useNavigate } from '@tanstack/react-router' - -function OpenPhotoButton({ photoId }: { photoId: string }) { - const navigate = useNavigate() - - return ( - - ) -} -``` - -### Declarative Masking with `createRouteMask` - -```tsx -import { createRouter, createRouteMask } from '@tanstack/react-router' -import { routeTree } from './routeTree.gen' - -const photoModalMask = createRouteMask({ - routeTree, - from: '/photos/$photoId/modal', - to: '/photos/$photoId', - params: (prev) => ({ photoId: prev.photoId }), -}) - -const router = createRouter({ - routeTree, - routeMasks: [photoModalMask], -}) -``` - -### Unmasking on Reload - -By default, masks survive local page reloads. To unmask on reload: - -```tsx -// Per-mask -const mask = createRouteMask({ - routeTree, - from: '/photos/$photoId/modal', - to: '/photos/$photoId', - params: (prev) => ({ photoId: prev.photoId }), - unmaskOnReload: true, -}) - -// Per-link - - Open Photo - - -// Router-wide default -const router = createRouter({ - routeTree, - unmaskOnReload: true, -}) -``` - ## Common Mistakes ### 1. HIGH: Using deprecated `NotFoundRoute` @@ -402,11 +298,7 @@ export const Route = createFileRoute('/posts/$postId')({ }) ``` -### 4. MEDIUM: Expecting masked URLs to survive sharing - -Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly. - -### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()` +### 4. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()` ```tsx // WRONG — reset() clears the error boundary but does NOT re-run the loader @@ -433,3 +325,4 @@ function ErrorFallback({ error }: { error: Error; reset: () => void }) { - **router-core/data-loading** — `notFound()` thrown in loaders interacts with error boundaries and loader data availability. `errorComponent` retry requires `router.invalidate()`. - **router-core/type-safety** — `notFoundComponent` data is typed as `unknown`; validate before use. +- **router-core/route-masking** — Route masking (showing a different URL in the browser bar) is now its own skill. See [route-masking/SKILL.md](../route-masking/SKILL.md). diff --git a/packages/router-core/skills/router-core/route-masking/SKILL.md b/packages/router-core/skills/router-core/route-masking/SKILL.md new file mode 100644 index 00000000000..e4c00d7effc --- /dev/null +++ b/packages/router-core/skills/router-core/route-masking/SKILL.md @@ -0,0 +1,168 @@ +--- +name: router-core/route-masking +description: >- + Route masking: showing a different URL in the browser bar than the actual + route being rendered. mask option on Link/navigate, createRouteMask for + declarative masking, unmaskOnReload, type-safe mask options, + location.state-based storage, automatic unmasking on URL sharing. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - router-core/navigation +sources: + - TanStack/router:docs/router/guide/route-masking.md +--- + +# Route Masking + +Route masking shows a different URL in the browser bar than the actual route being rendered. The masked URL is what gets persisted to the browser's history and URL bar. The real route location is stored in `location.state` and used internally by the router. + +> **KEY CONCEPT**: Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost and the browser navigates to the visible (masked) URL directly. This is by design. + +## When to Use Route Masking + +- Modal routes: navigate to `/photos/$photoId/modal` but show `/photos/$photoId` in the URL bar +- Hiding internal search params: navigate with `?showLogin=true` but mask the URL to exclude it +- Parallel route patterns: render one route while displaying a different URL + +## Imperative Masking on `` + +The `mask` option accepts the same navigation options as `Link` itself (`to`, `params`, `search`, `hash`, `replace`). It is fully type-safe. + +```tsx +import { Link } from '@tanstack/react-router' + +function PhotoGrid({ photoId }: { photoId: string }) { + return ( + + Open Photo + + ) +} +``` + +## Imperative Masking with `useNavigate` + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function OpenPhotoButton({ photoId }: { photoId: string }) { + const navigate = useNavigate() + + return ( + + ) +} +``` + +## Declarative Masking with `createRouteMask` + +Instead of adding `mask` to every ``, define masks at the router level. Any navigation matching the `from` route automatically gets masked. + +```tsx +import { createRouter, createRouteMask } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const photoModalMask = createRouteMask({ + routeTree, + from: '/photos/$photoId/modal', + to: '/photos/$photoId', + params: (prev) => ({ photoId: prev.photoId }), +}) + +const router = createRouter({ + routeTree, + routeMasks: [photoModalMask], +}) +``` + +`createRouteMask` requires: +- `routeTree` — the route tree for type inference +- `from` — the route ID to match for masking +- Standard navigation options (`to`, `params`, `search`, etc.) for the masked URL + +Both `from` and the navigation options are type-safe. + +## Unmasking Behavior + +### URL Sharing (automatic) + +Masked URLs are automatically unmasked when shared. Copying a URL from the address bar copies the masked (visible) URL. Opening it in a new tab navigates to that visible URL without masking data. + +### Page Reload (configurable) + +By default, masks **survive** local page reloads because `location.state` persists in the browser history stack. To unmask on reload, configure at three levels (each overrides the previous): + +```tsx +// 1. Router-wide default +const router = createRouter({ + routeTree, + unmaskOnReload: true, +}) + +// 2. Per-mask +const mask = createRouteMask({ + routeTree, + from: '/photos/$photoId/modal', + to: '/photos/$photoId', + params: (prev) => ({ photoId: prev.photoId }), + unmaskOnReload: true, +}) + +// 3. Per-link (highest priority) + + Open Photo + +``` + +## Common Mistakes + +### 1. MEDIUM: Expecting masked URLs to survive sharing + +Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly. Ensure the masked URL resolves to a valid, meaningful route. + +```tsx +// The user sees /photos/5 in the URL bar +// If they copy/paste it, they navigate to /photos/5 (not /photos/5/modal) +// Make sure /photos/5 is a valid route that renders something useful + + Open Photo + +``` + +## Cross-References + +- **router-core/navigation** — `mask` is an option on `Link` and `useNavigate`, using the same `to`/`params`/`search` navigation options. +- **router-core/not-found-and-errors** — if the masked URL doesn't match a route, standard not-found handling applies. +- **router-core/type-safety** — both `createRouteMask` and the `mask` option on `Link`/`navigate` are fully type-safe. diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json index 3a76dbd7155..d2c2c3c5a16 100644 --- a/packages/router-plugin/package.json +++ b/packages/router-plugin/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/solid-router/package.json b/packages/solid-router/package.json index 7e03eb20a7e..3cbb959ec62 100644 --- a/packages/solid-router/package.json +++ b/packages/solid-router/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/solid-router/skills/compositions/router-query/SKILL.md b/packages/solid-router/skills/compositions/router-query/SKILL.md new file mode 100644 index 00000000000..c1b03ac05cf --- /dev/null +++ b/packages/solid-router/skills/compositions/router-query/SKILL.md @@ -0,0 +1,306 @@ +--- +name: compositions/router-query +description: >- + Integrating TanStack Solid Router with TanStack Query: queryClient + in router context, ensureQueryData/prefetchQuery in loaders, + createQuery in components, defaultPreloadStaleTime: 0, + per-request QueryClient isolation for SSR. +type: composition +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - router-core/data-loading + - solid-router +sources: + - TanStack/router:docs/router/guide/external-data-loading.md + - TanStack/router:docs/router/integrations/query.md +--- + +# TanStack Solid Router + TanStack Query Integration + +This skill requires familiarity with both TanStack Router and TanStack Query. Read [router-core](../../../../router-core/skills/router-core/SKILL.md) and [solid-router](../../solid-router/SKILL.md) first. + +This skill covers coordinating TanStack Query as an external data cache with TanStack Router's loader system in Solid applications. The router acts as a **coordinator** — it triggers data fetching during navigation, while Query manages caching, background refetching, and data lifecycle. + +> **CRITICAL**: Set `defaultPreloadStaleTime: 0` when using TanStack Query. Without this, Router's built-in preload cache (30s default) prevents Query from controlling data freshness. + +> **CRITICAL**: For SSR, create `QueryClient` inside the `createRouter` factory function. A module-level singleton leaks data between server requests. + +## Setup: QueryClient in Router Context + +### Basic (Client-Only) + +```tsx +// src/main.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' +import { + RouterProvider, + createRouter, + createRootRouteWithContext, +} from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() + +const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, + Wrap: (props) => ( + + {props.children} + + ), +}) + +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +function App() { + return +} +``` + +### Root Route with Context + +```tsx +// src/routes/__root.tsx +import { createRootRouteWithContext, Outlet } from '@tanstack/solid-router' +import type { QueryClient } from '@tanstack/solid-query' + +export const Route = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: () => , +}) +``` + +### SSR-Safe Setup + +```tsx +// src/router.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/solid-query' +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createAppRouter() { + const queryClient = new QueryClient() + + return createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, + Wrap: (props) => ( + + {props.children} + + ), + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} +``` + +## Core Pattern: `ensureQueryData` in Loader + `createQuery` in Component + +The loader ensures data is in the cache before render (no loading flash). The component subscribes to the cache for updates using Solid's reactive primitives. + +```tsx +// src/routes/posts.tsx +import { queryOptions, createQuery } from '@tanstack/solid-query' +import { createFileRoute } from '@tanstack/solid-router' +import { For } from 'solid-js' + +interface Post { + id: string + title: string +} + +const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: (): Promise> => + fetch('/api/posts').then((r) => r.json()), +}) + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => { + return context.queryClient.ensureQueryData(postsQueryOptions) + }, + component: PostsPage, +}) + +function PostsPage() { + const postsQuery = createQuery(() => postsQueryOptions) + + return ( +
    + + {(post) =>
  • {post.title}
  • } +
    +
+ ) +} +``` + +### With Dynamic Params + +```tsx +// src/routes/posts/$postId.tsx +import { queryOptions, createQuery } from '@tanstack/solid-query' +import { createFileRoute } from '@tanstack/solid-router' + +interface Post { + id: string + title: string + content: string +} + +const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', postId], + queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()), + }) + +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ context, params }) => { + return context.queryClient.ensureQueryData(postQueryOptions(params.postId)) + }, + component: PostPage, +}) + +function PostPage() { + const params = Route.useParams() + const postQuery = createQuery(() => postQueryOptions(params().postId)) + + return
{postQuery.data?.title}
+} +``` + +## Streaming Pattern: `prefetchQuery` (Not Awaited) + +For non-critical data, start the fetch without blocking navigation: + +```tsx +import { createQuery } from '@tanstack/solid-query' +import { Show } from 'solid-js' + +export const Route = createFileRoute('/dashboard')({ + loader: ({ context }) => { + const user = context.queryClient.ensureQueryData(userQueryOptions) + context.queryClient.prefetchQuery(analyticsQueryOptions) + return user + }, + component: Dashboard, +}) + +function Dashboard() { + const userQuery = createQuery(() => userQueryOptions) + const analyticsQuery = createQuery(() => analyticsQueryOptions) + + return ( +
+

Welcome {userQuery.data?.name}

+ }> + + +
+ ) +} +``` + +## Common Mistakes + +### 1. HIGH: Not setting `defaultPreloadStaleTime` to 0 + +Router has a built-in preload cache (default `staleTime` for preloads is 30s). This prevents Query from controlling data freshness during preloading. + +```tsx +// WRONG — Router's preload cache serves stale data, Query never refetches +const router = createRouter({ routeTree }) + +// CORRECT — disable Router's preload cache, let Query manage freshness +const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, +}) +``` + +### 2. HIGH: Creating QueryClient outside `createRouter` for SSR + +A module-level singleton `QueryClient` is shared across all server requests, leaking user data between requests. + +```tsx +// WRONG — shared across SSR requests +const queryClient = new QueryClient() +export function createAppRouter() { + return createRouter({ + routeTree, + context: { queryClient }, + }) +} + +// CORRECT — new QueryClient per createAppRouter call +export function createAppRouter() { + const queryClient = new QueryClient() + return createRouter({ + routeTree, + context: { queryClient }, + }) +} +``` + +### 3. MEDIUM: Awaiting `prefetchQuery` in loader blocks rendering + +`prefetchQuery` is designed to fire-and-forget. Awaiting it blocks the navigation transition. + +```tsx +// WRONG — blocks navigation +loader: async ({ context }) => { + await context.queryClient.prefetchQuery(analyticsQueryOptions) +} + +// CORRECT — fire and forget for streaming +loader: ({ context }) => { + context.queryClient.prefetchQuery(analyticsQueryOptions) +} +``` + +### 4. HIGH: Missing double parentheses on `createRootRouteWithContext` + +```tsx +// WRONG +const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({ + component: RootComponent, +}) + +// CORRECT — double call: factory()({options}) +const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({ + component: RootComponent, +}) +``` + +### 5. MEDIUM: Using React Query patterns instead of Solid Query + +Solid Query uses `createQuery` (not `useQuery`) and returns reactive getters. Query options are passed as a function returning options, not directly. + +```tsx +// WRONG — React pattern +const { data } = useQuery(postsQueryOptions) + +// CORRECT — Solid pattern +const postsQuery = createQuery(() => postsQueryOptions) +// Access: postsQuery.data (reactive getter) +``` + +## Cross-References + +- [router-core/data-loading](../../../../router-core/skills/router-core/data-loading/SKILL.md) — built-in loader caching fundamentals +- [router-core/ssr](../../../../router-core/skills/router-core/ssr/SKILL.md) — SSR setup for dehydration/hydration diff --git a/packages/solid-router/skills/solid-router/SKILL.md b/packages/solid-router/skills/solid-router/SKILL.md index 75c82805df2..981fc7d049a 100644 --- a/packages/solid-router/skills/solid-router/SKILL.md +++ b/packages/solid-router/skills/solid-router/SKILL.md @@ -497,3 +497,4 @@ Must set `target: 'solid'` in the router plugin config. Default is `'react'`. ## Cross-References - [router-core/SKILL.md](../../../router-core/skills/router-core/SKILL.md) — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.) +- [compositions/router-query/SKILL.md](../compositions/router-query/SKILL.md) — integrating TanStack Query as an external data cache with Solid Router diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index adfc29cba8c..d1b75f34766 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json index c8fbf23bf84..4ba23a6f760 100644 --- a/packages/start-client-core/package.json +++ b/packages/start-client-core/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/start-server-core/package.json b/packages/start-server-core/package.json index 95bc18448a9..3491ffae39f 100644 --- a/packages/start-server-core/package.json +++ b/packages/start-server-core/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/virtual-file-routes/package.json b/packages/virtual-file-routes/package.json index 327bc2f8fd1..c177571b96c 100644 --- a/packages/virtual-file-routes/package.json +++ b/packages/virtual-file-routes/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index c69d65276d9..7c435f64a4c 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", diff --git a/packages/vue-router/skills/compositions/router-query/SKILL.md b/packages/vue-router/skills/compositions/router-query/SKILL.md new file mode 100644 index 00000000000..1f306c13447 --- /dev/null +++ b/packages/vue-router/skills/compositions/router-query/SKILL.md @@ -0,0 +1,279 @@ +--- +name: compositions/router-query +description: >- + Integrating TanStack Vue Router with TanStack Query: queryClient + in router context, ensureQueryData/prefetchQuery in loaders, + useQuery in components, defaultPreloadStaleTime: 0, + per-request QueryClient isolation for SSR. +type: composition +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - router-core/data-loading + - vue-router +sources: + - TanStack/router:docs/router/guide/external-data-loading.md + - TanStack/router:docs/router/integrations/query.md +--- + +# TanStack Vue Router + TanStack Query Integration + +This skill requires familiarity with both TanStack Router and TanStack Query. Read [router-core](../../../../router-core/skills/router-core/SKILL.md) and [vue-router](../../vue-router/SKILL.md) first. + +This skill covers coordinating TanStack Query as an external data cache with TanStack Router's loader system in Vue applications. The router acts as a **coordinator** — it triggers data fetching during navigation, while Query manages caching, background refetching, and data lifecycle. + +> **CRITICAL**: Set `defaultPreloadStaleTime: 0` when using TanStack Query. Without this, Router's built-in preload cache (30s default) prevents Query from controlling data freshness. + +> **CRITICAL**: For SSR, create `QueryClient` inside the `createRouter` factory function. A module-level singleton leaks data between server requests. + +## Setup: QueryClient in Router Context + +### Basic (Client-Only) + +```ts +// src/main.ts +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' +import { createApp } from 'vue' +import { RouterProvider, createRouter, createRootRouteWithContext } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() + +const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, +}) + +declare module '@tanstack/vue-router' { + interface Register { + router: typeof router + } +} + +const app = createApp(RouterProvider, { router }) +app.use(VueQueryPlugin, { queryClient }) +app.mount('#app') +``` + +### Root Route with Context + +```ts +// src/routes/__root.ts +import { createRootRouteWithContext } from '@tanstack/vue-router' +import type { QueryClient } from '@tanstack/vue-query' + +export const Route = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: () => import('./RootComponent.vue'), +}) +``` + +### SSR-Safe Setup + +```ts +// src/router.ts +import { QueryClient } from '@tanstack/vue-query' +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createAppRouter() { + const queryClient = new QueryClient() + + return createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} +``` + +## Core Pattern: `ensureQueryData` in Loader + `useQuery` in Component + +The loader ensures data is in the cache before render (no loading flash). The component subscribes to the cache for updates using Vue's reactive refs. + +```vue + + + + +``` + +```ts +// src/routes/posts.ts (route definition) +import { createFileRoute } from '@tanstack/vue-router' +import { queryOptions } from '@tanstack/vue-query' + +const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: (): Promise> => + fetch('/api/posts').then((r) => r.json()), +}) + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => { + return context.queryClient.ensureQueryData(postsQueryOptions) + }, + component: () => import('./posts.vue'), +}) +``` + +### With Dynamic Params + +```ts +// src/routes/posts/$postId.ts +import { createFileRoute } from '@tanstack/vue-router' +import { queryOptions } from '@tanstack/vue-query' + +const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', postId], + queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()), + }) + +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ context, params }) => { + return context.queryClient.ensureQueryData(postQueryOptions(params.postId)) + }, + component: () => import('./PostPage.vue'), +}) +``` + +```vue + + + + +``` + +## Streaming Pattern: `prefetchQuery` (Not Awaited) + +For non-critical data, start the fetch without blocking navigation: + +```ts +export const Route = createFileRoute('/dashboard')({ + loader: ({ context }) => { + const user = context.queryClient.ensureQueryData(userQueryOptions) + context.queryClient.prefetchQuery(analyticsQueryOptions) + return user + }, + component: () => import('./Dashboard.vue'), +}) +``` + +## Common Mistakes + +### 1. HIGH: Not setting `defaultPreloadStaleTime` to 0 + +Router has a built-in preload cache (default `staleTime` for preloads is 30s). This prevents Query from controlling data freshness during preloading. + +```ts +// WRONG — Router's preload cache serves stale data, Query never refetches +const router = createRouter({ routeTree }) + +// CORRECT — disable Router's preload cache, let Query manage freshness +const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, +}) +``` + +### 2. HIGH: Creating QueryClient outside `createRouter` for SSR + +A module-level singleton `QueryClient` is shared across all server requests, leaking user data between requests. + +```ts +// WRONG — shared across SSR requests +const queryClient = new QueryClient() +export function createAppRouter() { + return createRouter({ + routeTree, + context: { queryClient }, + }) +} + +// CORRECT — new QueryClient per createAppRouter call +export function createAppRouter() { + const queryClient = new QueryClient() + return createRouter({ + routeTree, + context: { queryClient }, + }) +} +``` + +### 3. MEDIUM: Awaiting `prefetchQuery` in loader blocks rendering + +`prefetchQuery` is designed to fire-and-forget. Awaiting it blocks the navigation transition. + +```ts +// WRONG — blocks navigation +loader: async ({ context }) => { + await context.queryClient.prefetchQuery(analyticsQueryOptions) +} + +// CORRECT — fire and forget for streaming +loader: ({ context }) => { + context.queryClient.prefetchQuery(analyticsQueryOptions) +} +``` + +### 4. HIGH: Missing double parentheses on `createRootRouteWithContext` + +```ts +// WRONG +const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({ + component: RootComponent, +}) + +// CORRECT — double call: factory()({options}) +const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({ + component: RootComponent, +}) +``` + +## Cross-References + +- [router-core/data-loading](../../../../router-core/skills/router-core/data-loading/SKILL.md) — built-in loader caching fundamentals +- [router-core/ssr](../../../../router-core/skills/router-core/ssr/SKILL.md) — SSR setup for dehydration/hydration diff --git a/packages/vue-router/skills/vue-router/SKILL.md b/packages/vue-router/skills/vue-router/SKILL.md index 17108957aa4..0d0d4f5d64f 100644 --- a/packages/vue-router/skills/vue-router/SKILL.md +++ b/packages/vue-router/skills/vue-router/SKILL.md @@ -390,3 +390,4 @@ Must set `target: 'vue'` in the router plugin config. Default is `'react'`. ## Cross-References - [router-core/SKILL.md](../../../router-core/skills/router-core/SKILL.md) — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.) +- [compositions/router-query/SKILL.md](../compositions/router-query/SKILL.md) — integrating TanStack Query as an external data cache with Vue Router diff --git a/packages/vue-start/package.json b/packages/vue-start/package.json index 9f0c58d0fea..de3e66a8988 100644 --- a/packages/vue-start/package.json +++ b/packages/vue-start/package.json @@ -21,7 +21,8 @@ "routing", "async", "async router", - "typescript" + "typescript", + "tanstack-intent" ], "scripts": { "clean": "rimraf ./dist && rimraf ./coverage",