+
+
+ {
+ setLoaderDelay(150)
+ }}
+ >
+ Fast
+
+ {
+ setLoaderDelay(500)
+ }}
+ >
+ Fast 3G
+
+ {
+ setLoaderDelay(2000)
+ }}
+ >
+ Slow 3G
+
+
+
+
Loader Delay: {loaderDelay()}ms
+
setLoaderDelay(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
+
+ {
+ setPendingMs(1000)
+ setPendingMinMs(500)
+ }}
+ >
+ Reset to Default
+
+
+
+
defaultPendingMs: {pendingMs()}ms
+
setPendingMs(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
defaultPendingMinMs: {pendingMinMs()}ms
+
setPendingMinMs(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
+ ()
+
+/* ROUTE_MANIFEST_START
+{
+ "routes": {
+ "__root__": {
+ "filePath": "__root.tsx",
+ "children": [
+ "/",
+ "/dashboard",
+ "/_auth",
+ "/_pathlessLayout",
+ "/login",
+ "/foo/bar",
+ "/expensive/"
+ ]
+ },
+ "/": {
+ "filePath": "index.tsx"
+ },
+ "/dashboard": {
+ "filePath": "dashboard.route.tsx",
+ "children": [
+ "/dashboard/invoices",
+ "/dashboard/users",
+ "/dashboard/"
+ ]
+ },
+ "/_auth": {
+ "filePath": "_auth.tsx",
+ "children": [
+ "/_auth/profile"
+ ]
+ },
+ "/_pathlessLayout": {
+ "filePath": "_pathlessLayout.tsx",
+ "children": [
+ "/_pathlessLayout/route-a",
+ "/_pathlessLayout/route-b"
+ ]
+ },
+ "/login": {
+ "filePath": "login.tsx"
+ },
+ "/dashboard/invoices": {
+ "filePath": "dashboard.invoices.route.tsx",
+ "parent": "/dashboard",
+ "children": [
+ "/dashboard/invoices/$invoiceId",
+ "/dashboard/invoices/"
+ ]
+ },
+ "/dashboard/users": {
+ "filePath": "dashboard.users.route.tsx",
+ "parent": "/dashboard",
+ "children": [
+ "/dashboard/users/user",
+ "/dashboard/users/"
+ ]
+ },
+ "/_auth/profile": {
+ "filePath": "_auth.profile.tsx",
+ "parent": "/_auth"
+ },
+ "/_pathlessLayout/route-a": {
+ "filePath": "_pathlessLayout.route-a.tsx",
+ "parent": "/_pathlessLayout"
+ },
+ "/_pathlessLayout/route-b": {
+ "filePath": "_pathlessLayout.route-b.tsx",
+ "parent": "/_pathlessLayout"
+ },
+ "/foo/bar": {
+ "filePath": "foo/bar.tsx"
+ },
+ "/dashboard/": {
+ "filePath": "dashboard.index.tsx",
+ "parent": "/dashboard"
+ },
+ "/expensive/": {
+ "filePath": "expensive/index.tsx"
+ },
+ "/dashboard/invoices/$invoiceId": {
+ "filePath": "dashboard.invoices.$invoiceId.tsx",
+ "parent": "/dashboard/invoices"
+ },
+ "/dashboard/users/user": {
+ "filePath": "dashboard.users.user.tsx",
+ "parent": "/dashboard/users"
+ },
+ "/dashboard/invoices/": {
+ "filePath": "dashboard.invoices.index.tsx",
+ "parent": "/dashboard/invoices"
+ },
+ "/dashboard/users/": {
+ "filePath": "dashboard.users.index.tsx",
+ "parent": "/dashboard/users"
+ }
+ }
+}
+ROUTE_MANIFEST_END */
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/__root.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/__root.tsx
new file mode 100644
index 0000000000..4752063de3
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/__root.tsx
@@ -0,0 +1,80 @@
+import {
+ Link,
+ Outlet,
+ createRootRouteWithContext,
+ useRouterState,
+} from '@tanstack/solid-router'
+import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
+import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
+import { Spinner } from '../components/Spinner'
+import type { QueryClient } from '@tanstack/solid-query'
+import type { Auth } from '../utils/auth'
+
+function RouterSpinner() {
+ const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
+ return
+}
+
+export const Route = createRootRouteWithContext<{
+ auth: Auth
+ queryClient: QueryClient
+}>()({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+
Kitchen Sink
+ {/* Show a global spinner when the router is transitioning */}
+
+
+
+
+
+
+ {(
+ [
+ ['/', 'Home'],
+ ['/dashboard', 'Dashboard'],
+ ['/expensive', 'Expensive'],
+ ['/route-a', 'Pathless Layout A'],
+ ['/route-b', 'Pathless Layout B'],
+ ['/profile', 'Profile'],
+ ['/login', 'Login'],
+ ] as const
+ ).map(([to, label]) => {
+ return (
+
+
+ {label}
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_auth.profile.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_auth.profile.tsx
new file mode 100644
index 0000000000..c72820435f
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_auth.profile.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_auth/profile')({
+ component: ProfileComponent,
+})
+
+function ProfileComponent() {
+ const routeContext = Route.useRouteContext()
+
+ return (
+
+
+ Username:{routeContext().username}
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_auth.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_auth.tsx
new file mode 100644
index 0000000000..a5b98f8631
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_auth.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+import { auth } from '../utils/auth'
+
+export const Route = createFileRoute('/_auth')({
+ // Before loading, authenticate the user via our auth context
+ // This will also happen during prefetching (e.g. hovering over links, etc)
+ beforeLoad: ({ context, location }) => {
+ // If the user is logged out, redirect them to the login page
+ if (context.auth.status === 'loggedOut') {
+ throw redirect({
+ to: '/login',
+ search: {
+ // Use the current location to power a redirect after login
+ // (Do not use `router.state.resolvedLocation` as it can
+ // potentially lag behind the actual current location)
+ redirect: location.href,
+ },
+ })
+ }
+
+ // Otherwise, return the user in context
+ return {
+ username: auth.username,
+ }
+ },
+})
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.route-a.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.route-a.tsx
new file mode 100644
index 0000000000..c1c392cbad
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.route-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_pathlessLayout/route-a')({
+ component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+ return I'm A!
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.route-b.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.route-b.tsx
new file mode 100644
index 0000000000..30dd035c93
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.route-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_pathlessLayout/route-b')({
+ component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+ return I'm B!
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.tsx
new file mode 100644
index 0000000000..7d1a8084c3
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/_pathlessLayout.tsx
@@ -0,0 +1,15 @@
+import { Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/_pathlessLayout')({
+ component: LayoutComponent,
+})
+
+function LayoutComponent() {
+ return (
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.index.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.index.tsx
new file mode 100644
index 0000000000..a0985ab160
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.index.tsx
@@ -0,0 +1,23 @@
+import { useQuery } from '@tanstack/solid-query'
+import { createFileRoute } from '@tanstack/solid-router'
+import { invoicesQueryOptions } from '../utils/queryOptions'
+
+export const Route = createFileRoute('/dashboard/')({
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(invoicesQueryOptions()),
+ component: DashboardIndexComponent,
+})
+
+function DashboardIndexComponent() {
+ const invoicesQuery = useQuery(() => invoicesQueryOptions())
+ const invoices = invoicesQuery.data
+
+ return (
+
+
+ Welcome to the dashboard! You have{' '}
+ {invoices?.length} total invoices .
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
new file mode 100644
index 0000000000..2fe75cdfab
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.$invoiceId.tsx
@@ -0,0 +1,125 @@
+import * as Solid from 'solid-js'
+import { Link, createFileRoute, useNavigate } from '@tanstack/solid-router'
+import { useQuery } from '@tanstack/solid-query'
+import { z } from 'zod'
+import { InvoiceFields } from '../components/InvoiceFields'
+import {
+ invoiceQueryOptions,
+ useUpdateInvoiceMutation,
+} from '../utils/queryOptions'
+
+export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
+ params: {
+ parse: (params) => ({
+ invoiceId: z.number().int().parse(Number(params.invoiceId)),
+ }),
+ stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
+ },
+ validateSearch: (search) =>
+ z
+ .object({
+ showNotes: z.boolean().optional(),
+ notes: z.string().optional(),
+ })
+ .parse(search),
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(
+ invoiceQueryOptions(opts.params.invoiceId),
+ ),
+ component: InvoiceComponent,
+})
+
+function InvoiceComponent() {
+ const search = Route.useSearch()
+ const params = Route.useParams()
+ const navigate = useNavigate({ from: Route.fullPath })
+ const invoiceQuery = useQuery(() => invoiceQueryOptions(params().invoiceId))
+ const invoice = invoiceQuery.data
+ const updateInvoiceMutation = useUpdateInvoiceMutation(params().invoiceId)
+ const [notes, setNotes] = Solid.createSignal(search().notes ?? '')
+
+ Solid.createEffect(() => {
+ navigate({
+ search: (old) => ({
+ ...old,
+ notes: notes() ? notes() : undefined,
+ }),
+ replace: true,
+ params: true,
+ })
+ }, [notes])
+
+ return (
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.index.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.index.tsx
new file mode 100644
index 0000000000..e3d3e66247
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.index.tsx
@@ -0,0 +1,58 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { InvoiceFields } from '../components/InvoiceFields'
+import { Spinner } from '../components/Spinner'
+import { useCreateInvoiceMutation } from '../utils/queryOptions'
+import type { Invoice } from '../utils/mockTodos'
+
+export const Route = createFileRoute('/dashboard/invoices/')({
+ component: InvoicesIndexComponent,
+})
+
+function InvoicesIndexComponent() {
+ const createInvoiceMutation = useCreateInvoiceMutation()
+
+ return (
+ <>
+
+
{
+ event.preventDefault()
+ event.stopPropagation()
+ const formData = new FormData(event.target as HTMLFormElement)
+ createInvoiceMutation.mutate({
+ title: formData.get('title') as string,
+ body: formData.get('body') as string,
+ })
+ }}
+ class="space-y-2"
+ >
+ Create a new Invoice:
+
+
+
+ {createInvoiceMutation.status === 'pending' ? (
+ <>
+ Creating
+ >
+ ) : (
+ 'Create'
+ )}
+
+
+ {createInvoiceMutation.status === 'success' ? (
+
+ Created!
+
+ ) : createInvoiceMutation.status === 'error' ? (
+
+ Failed to create.
+
+ ) : null}
+
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.route.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.route.tsx
new file mode 100644
index 0000000000..705f94adac
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.invoices.route.tsx
@@ -0,0 +1,58 @@
+import {
+ Link,
+ MatchRoute,
+ Outlet,
+ createFileRoute,
+} from '@tanstack/solid-router'
+import { useQuery } from '@tanstack/solid-query'
+import { Spinner } from '../components/Spinner'
+import { invoicesQueryOptions } from '../utils/queryOptions'
+
+export const Route = createFileRoute('/dashboard/invoices')({
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(invoicesQueryOptions()),
+ component: InvoicesComponent,
+})
+
+function InvoicesComponent() {
+ const invoicesQuery = useQuery(() => invoicesQueryOptions())
+ const invoices = invoicesQuery.data
+
+ return (
+
+
+ {invoices?.map((invoice) => {
+ return (
+
+
+
+ #{invoice.id} - {invoice.title.slice(0, 10)}{' '}
+
+ {(match) => }
+
+
+
+
+ )
+ })}
+
+
+
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.route.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.route.tsx
new file mode 100644
index 0000000000..056483eb1a
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.route.tsx
@@ -0,0 +1,37 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/dashboard')({
+ component: DashboardComponent,
+})
+
+function DashboardComponent() {
+ return (
+ <>
+
+
Dashboard
+
+
+ {(
+ [
+ ['/dashboard', 'Summary', true],
+ ['/dashboard/invoices', 'Invoices'],
+ ['/dashboard/users', 'Users'],
+ ] as const
+ ).map(([to, label, exact]) => {
+ return (
+
+ {label}
+
+ )
+ })}
+
+
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.index.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.index.tsx
new file mode 100644
index 0000000000..75ca9dbf93
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.index.tsx
@@ -0,0 +1,29 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/dashboard/users/')({
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return (
+
+
+ Normally, setting default search parameters would either need to be done
+ manually in every link to a page, or as a side-effect (not a great
+ experience).
+
+
+ Instead, we can use search filters to provide defaults
+ or even persist search params for links to routes (and child routes).
+
+
+ A good example of this is the sorting and filtering of the users list.
+ In a traditional router, both would be lost while navigating around
+ individual users or even changing each sort/filter option unless each
+ state was manually passed from the current route into each new link we
+ created (that's a lot of tedious and error-prone work). With TanStack
+ router and search filters, they are persisted with little effort.
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.route.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.route.tsx
new file mode 100644
index 0000000000..704dea02b2
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.route.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable @typescript-eslint/no-unnecessary-condition */
+import * as Solid from 'solid-js'
+import {
+ Link,
+ MatchRoute,
+ Outlet,
+ createFileRoute,
+ retainSearchParams,
+ useNavigate,
+} from '@tanstack/solid-router'
+import { useQuery } from '@tanstack/solid-query'
+import { z } from 'zod'
+import { Spinner } from '../components/Spinner'
+import { usersQueryOptions } from '../utils/queryOptions'
+
+type UsersViewSortBy = 'name' | 'id' | 'email'
+
+export const Route = createFileRoute('/dashboard/users')({
+ validateSearch: z.object({
+ usersView: z
+ .object({
+ sortBy: z.enum(['name', 'id', 'email']).optional(),
+ filterBy: z.string().optional(),
+ })
+ .optional(),
+ }).parse,
+ search: {
+ // Retain the usersView search param while navigating
+ // within or to this route (or it's children!)
+ middlewares: [retainSearchParams(['usersView'])],
+ },
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(usersQueryOptions(opts.deps)),
+ component: UsersComponent,
+})
+
+function UsersComponent() {
+ const navigate = useNavigate({ from: Route.fullPath })
+ const search = Route.useSearch()
+ const usersQuery = useQuery(() => usersQueryOptions(Route.useLoaderDeps()))
+ const users = usersQuery.data
+ const sortBy = search().usersView?.sortBy ?? 'name'
+ const filterBy = search().usersView?.filterBy
+
+ const [filterDraft, setFilterDraft] = Solid.createSignal(filterBy ?? '')
+
+ Solid.createEffect(() => {
+ setFilterDraft(filterBy ?? '')
+ }, [filterBy])
+
+ const sortedUsers = Solid.createMemo(() => {
+ if (!users) return []
+
+ return !sortBy
+ ? users
+ : [...users].sort((a, b) => {
+ return a[sortBy] > b[sortBy] ? 1 : -1
+ })
+ }, [users, sortBy])
+
+ const filteredUsers = Solid.createMemo(() => {
+ if (!filterBy) return sortedUsers()
+
+ return sortedUsers().filter((user) =>
+ user.name.toLowerCase().includes(filterBy.toLowerCase()),
+ )
+ }, [sortedUsers, filterBy])
+
+ const setSortBy = (sortBy: UsersViewSortBy) =>
+ navigate({
+ search: (old) => {
+ return {
+ ...old,
+ usersView: {
+ ...(old?.usersView ?? {}),
+ sortBy,
+ },
+ }
+ },
+ replace: true,
+ })
+
+ Solid.createEffect(() => {
+ navigate({
+ search: (old) => {
+ return {
+ ...old,
+ usersView: {
+ ...old?.usersView,
+ filterBy: filterDraft() || undefined,
+ },
+ }
+ },
+ replace: true,
+ })
+ }, [filterDraft])
+
+ return (
+
+
+
+
Sort By:
+
setSortBy(e.target.value as UsersViewSortBy)}
+ class="flex-1 border p-1 px-2 rounded"
+ >
+ {['name', 'id', 'email'].map((d) => {
+ return
+ })}
+
+
+
+
Filter By:
+
setFilterDraft(e.target.value)}
+ placeholder="Search Names..."
+ class="min-w-0 flex-1 border p-1 px-2 rounded"
+ />
+
+ {filteredUsers()?.map((user) => {
+ return (
+
+
+
+ {user.name}{' '}
+
+ {(match) => }
+
+
+
+
+ )
+ })}
+
+
+
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.user.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.user.tsx
new file mode 100644
index 0000000000..26e51474ee
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/dashboard.users.user.tsx
@@ -0,0 +1,31 @@
+import { useQuery } from '@tanstack/solid-query'
+import { z } from 'zod'
+import { createFileRoute } from '@tanstack/solid-router'
+import { userQueryOptions } from '../utils/queryOptions'
+
+export const Route = createFileRoute('/dashboard/users/user')({
+ validateSearch: z.object({
+ userId: z.number(),
+ }),
+ loaderDeps: ({ search: { userId } }) => ({ userId }),
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(
+ userQueryOptions(opts.deps.userId),
+ ),
+ component: UserComponent,
+})
+
+function UserComponent() {
+ const search = Route.useSearch()
+ const userQuery = useQuery(() => userQueryOptions(search().userId))
+ const user = userQuery.data
+
+ return (
+ <>
+ {user?.name}
+
+ {JSON.stringify(user, null, 2)}
+
+ >
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/expensive/-components/Expensive.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/expensive/-components/Expensive.tsx
new file mode 100644
index 0000000000..fd29d131b6
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/expensive/-components/Expensive.tsx
@@ -0,0 +1,8 @@
+export default function Expensive() {
+ return (
+
+ I am an "expensive" component... which really just means that I was
+ code-split 😉
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/expensive/index.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/expensive/index.tsx
new file mode 100644
index 0000000000..f1ae88b32b
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/expensive/index.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import Expensive from './-components/Expensive'
+
+export const Route = createFileRoute('/expensive/')({
+ component: Expensive,
+})
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/foo/bar.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/foo/bar.tsx
new file mode 100644
index 0000000000..05d7f0f2d2
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/foo/bar.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/foo/bar')({
+ component: () => {JSON.stringify(Route.useSearch())}
,
+ validateSearch: z.object({ asdf: z.string() }),
+})
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/index.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/index.tsx
new file mode 100644
index 0000000000..eed9991668
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/index.tsx
@@ -0,0 +1,36 @@
+import { Link, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/')({
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+
+ 1 New Invoice
+
+
+
+ As you navigate around take note of the UX. It should feel
+ suspense-like, where routes are only rendered once all of their data and
+ elements are ready.
+
+ To exaggerate async effects, play with the artificial request delay
+ slider in the bottom-left corner.
+
+ The last 2 sliders determine if link-hover preloading is enabled (and
+ how long those preloads stick around) and also whether to cache rendered
+ route data (and for how long). Both of these default to 0 (or off).
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routes/login.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/login.tsx
new file mode 100644
index 0000000000..b1e8dddd82
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routes/login.tsx
@@ -0,0 +1,69 @@
+import * as Solid from 'solid-js'
+import { createFileRoute, useRouter } from '@tanstack/solid-router'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/login')({
+ validateSearch: z.object({
+ redirect: z.string().optional(),
+ }),
+ component: LoginComponent,
+})
+
+function LoginComponent() {
+ const router = useRouter()
+ const routeContext: Solid.Accessor<{ auth: any; status: any }> =
+ Route.useRouteContext({
+ select: ({ auth }: { auth: any }) => ({ auth, status: auth.status }),
+ })
+ const search = Route.useSearch()
+ const [username, setUsername] = Solid.createSignal('')
+
+ const onSubmit = (e: SubmitEvent) => {
+ e.preventDefault()
+ routeContext().auth.login(username())
+ router.invalidate()
+ }
+
+ // Ah, the subtle nuances of client side auth. 🙄
+ Solid.createRenderEffect(() => {
+ if (routeContext().status === 'loggedIn' && search().redirect) {
+ router.history.push(search().redirect || '')
+ }
+ }, [routeContext().status, search().redirect])
+
+ return routeContext().status === 'loggedIn' ? (
+
+ Logged in as
{routeContext().auth.username}
+
+
{
+ routeContext().auth.logout()
+ router.invalidate()
+ }}
+ class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded"
+ >
+ Log out
+
+
+
+ ) : (
+
+
You must log in!
+
+
+ setUsername(e.target.value)}
+ placeholder="Username"
+ class="border p-1 px-2 rounded"
+ />
+
+ Login
+
+
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/styles.css b/examples/solid/kitchen-sink-solid-query-file-based/src/styles.css
new file mode 100644
index 0000000000..0b8e317099
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/utils/auth.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/auth.tsx
new file mode 100644
index 0000000000..6c3d4f1638
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/auth.tsx
@@ -0,0 +1,19 @@
+export const auth: Auth = {
+ status: 'loggedOut',
+ username: undefined,
+ login: (username: string) => {
+ auth.status = 'loggedIn'
+ auth.username = username
+ },
+ logout: () => {
+ auth.status = 'loggedOut'
+ auth.username = undefined
+ },
+}
+
+export type Auth = {
+ login: (username: string) => void
+ logout: () => void
+ status: 'loggedOut' | 'loggedIn'
+ username?: string
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/utils/mockTodos.ts b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/mockTodos.ts
new file mode 100644
index 0000000000..475d0fa88f
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/mockTodos.ts
@@ -0,0 +1,174 @@
+import axios from 'redaxios'
+import { produce } from 'immer'
+import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
+
+type PickAsRequired = Omit &
+ Required>
+
+export type Invoice = {
+ id: number
+ title: string
+ body: string
+}
+
+export interface User {
+ id: number
+ name: string
+ username: string
+ email: string
+ address: Address
+ phone: string
+ website: string
+ company: Company
+}
+
+export interface Address {
+ street: string
+ suite: string
+ city: string
+ zipcode: string
+ geo: Geo
+}
+
+export interface Geo {
+ lat: string
+ lng: string
+}
+
+export interface Company {
+ name: string
+ catchPhrase: string
+ bs: string
+}
+
+let invoices: Array = null!
+let users: Array = null!
+
+let invoicesPromise: Promise | undefined = undefined
+let usersPromise: Promise | undefined = undefined
+
+const ensureInvoices = async () => {
+ if (!invoicesPromise) {
+ invoicesPromise = Promise.resolve().then(async () => {
+ const { data } = await axios.get(
+ 'https://jsonplaceholder.typicode.com/posts',
+ )
+ invoices = data.slice(0, 10)
+ })
+ }
+
+ await invoicesPromise
+}
+
+const ensureUsers = async () => {
+ if (!usersPromise) {
+ usersPromise = Promise.resolve().then(async () => {
+ const { data } = await axios.get(
+ 'https://jsonplaceholder.typicode.com/users',
+ )
+ users = data.slice(0, 10)
+ })
+ }
+
+ await usersPromise
+}
+
+export async function fetchInvoices() {
+ return loaderDelayFn(() => ensureInvoices().then(() => invoices))
+}
+
+export async function fetchInvoiceById(id: number) {
+ return loaderDelayFn(() =>
+ ensureInvoices().then(() => {
+ const invoice = invoices.find((d) => d.id === id)
+ if (!invoice) {
+ throw new Error('Invoice not found')
+ }
+ return invoice
+ }),
+ )
+}
+
+export async function postInvoice(partialInvoice: Partial) {
+ return actionDelayFn(() => {
+ if (partialInvoice.title?.includes('error')) {
+ console.error('error')
+ throw new Error('Ouch!')
+ }
+ const invoice = {
+ id: invoices.length + 1,
+ title:
+ partialInvoice.title ?? `New Invoice ${String(Date.now()).slice(0, 5)}`,
+ body:
+ partialInvoice.body ??
+ shuffle(
+ `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam nec ante.
+ Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. Sed lectus. Integer euismod lacus luctus magna. Integer id quam. Morbi mi. Quisque nisl felis, venenatis tristique, dignissim in, ultrices sit amet, augue. Proin sodales libero eget ante.
+ `.split(' '),
+ ).join(' '),
+ }
+ invoices = [...invoices, invoice]
+ return invoice
+ })
+}
+
+export async function patchInvoice({
+ id,
+ ...updatedInvoice
+}: PickAsRequired, 'id'>) {
+ return actionDelayFn(() => {
+ invoices = produce(invoices, (draft) => {
+ const invoice = draft.find((d) => d.id === id)
+ if (!invoice) {
+ throw new Error('Invoice not found.')
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (updatedInvoice.title?.toLocaleLowerCase()?.includes('error')) {
+ throw new Error('Ouch!')
+ }
+ Object.assign(invoice, updatedInvoice)
+ })
+
+ return invoices.find((d) => d.id === id)
+ })
+}
+
+export type UsersSortBy = 'name' | 'id' | 'email'
+
+export async function fetchUsers({
+ filterBy,
+ sortBy,
+}: { filterBy?: string; sortBy?: UsersSortBy } = {}) {
+ return loaderDelayFn(() =>
+ ensureUsers().then(() => {
+ let usersDraft = users
+
+ if (filterBy) {
+ usersDraft = usersDraft.filter((d) =>
+ d.name.toLowerCase().includes(filterBy.toLowerCase()),
+ )
+ }
+
+ if (sortBy) {
+ usersDraft = [...usersDraft].sort((a, b) => {
+ return a[sortBy] > b[sortBy] ? 1 : -1
+ })
+ }
+
+ return usersDraft
+ }),
+ )
+}
+
+export async function fetchUserById(id: number) {
+ return loaderDelayFn(() =>
+ ensureUsers().then(() => users.find((d) => d.id === id)),
+ )
+}
+
+export async function fetchRandomNumber() {
+ return loaderDelayFn(() => {
+ return Math.random()
+ })
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/utils/queryOptions.ts b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/queryOptions.ts
new file mode 100644
index 0000000000..78150ea272
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/queryOptions.ts
@@ -0,0 +1,52 @@
+import { queryOptions, useMutation } from '@tanstack/solid-query'
+import { queryClient } from '../main'
+import {
+ fetchInvoiceById,
+ fetchInvoices,
+ fetchUserById,
+ fetchUsers,
+ patchInvoice,
+ postInvoice,
+} from './mockTodos'
+
+export const invoicesQueryOptions = () =>
+ queryOptions({
+ queryKey: ['invoices'],
+ queryFn: () => fetchInvoices(),
+ })
+
+export const invoiceQueryOptions = (invoiceId: number) =>
+ queryOptions({
+ queryKey: ['invoices', invoiceId],
+ queryFn: () => fetchInvoiceById(invoiceId),
+ })
+
+export const usersQueryOptions = (opts: {
+ filterBy?: string
+ sortBy?: 'name' | 'id' | 'email'
+}) =>
+ queryOptions({
+ queryKey: ['users', opts],
+ queryFn: () => fetchUsers(opts),
+ })
+
+export const userQueryOptions = (userId: number) =>
+ queryOptions({
+ queryKey: ['users', userId],
+ queryFn: () => fetchUserById(userId),
+ })
+
+export const useCreateInvoiceMutation = () => {
+ return useMutation(() => ({
+ mutationFn: postInvoice,
+ onSuccess: () => queryClient.invalidateQueries(),
+ }))
+}
+
+export const useUpdateInvoiceMutation = (invoiceId: number) => {
+ return useMutation(() => ({
+ mutationFn: patchInvoice,
+ onSuccess: () => queryClient.invalidateQueries(),
+ gcTime: 1000 * 10,
+ }))
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/utils/utils.tsx b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/utils.tsx
new file mode 100644
index 0000000000..6435657b12
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/utils/utils.tsx
@@ -0,0 +1,33 @@
+export async function loaderDelayFn(
+ fn: (...args: Array) => Promise | T,
+) {
+ const delay = Number(sessionStorage.getItem('loaderDelay') ?? 0)
+ const delayPromise = new Promise((r) => setTimeout(r, delay))
+
+ await delayPromise
+ const res = await fn()
+
+ return res
+}
+
+export async function actionDelayFn(
+ fn: (...args: Array) => Promise | T,
+) {
+ const delay = Number(sessionStorage.getItem('actionDelay') ?? 0)
+ await new Promise((r) => setTimeout(r, delay))
+ return fn()
+}
+
+export function shuffle(arr: Array): Array {
+ let i = arr.length
+ if (i == 0) return arr
+ const copy = [...arr]
+ while (--i) {
+ const j = Math.floor(Math.random() * (i + 1))
+ const a = copy[i]
+ const b = copy[j]
+ copy[i] = b!
+ copy[j] = a!
+ }
+ return copy
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/tailwind.config.mjs b/examples/solid/kitchen-sink-solid-query-file-based/tailwind.config.mjs
new file mode 100644
index 0000000000..4986094b9d
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/tsconfig.dev.json b/examples/solid/kitchen-sink-solid-query-file-based/tsconfig.dev.json
new file mode 100644
index 0000000000..285a09b0dc
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/tsconfig.json b/examples/solid/kitchen-sink-solid-query-file-based/tsconfig.json
new file mode 100644
index 0000000000..6e3ba6f110
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/vite.config.js b/examples/solid/kitchen-sink-solid-query-file-based/vite.config.js
new file mode 100644
index 0000000000..4af89e7520
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query-file-based/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ TanStackRouterVite({ target: 'solid', autoCodeSplitting: true }),
+ solid(),
+ ],
+})
diff --git a/examples/solid/kitchen-sink-solid-query/.gitignore b/examples/solid/kitchen-sink-solid-query/.gitignore
new file mode 100644
index 0000000000..d451ff16c1
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
diff --git a/examples/solid/kitchen-sink-solid-query/.vscode/settings.json b/examples/solid/kitchen-sink-solid-query/.vscode/settings.json
new file mode 100644
index 0000000000..00b5278e58
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "files.watcherExclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "search.exclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "files.readonlyInclude": {
+ "**/routeTree.gen.ts": true
+ }
+}
diff --git a/examples/solid/kitchen-sink-solid-query/README.md b/examples/solid/kitchen-sink-solid-query/README.md
new file mode 100644
index 0000000000..115199d292
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm start` or `yarn start`
diff --git a/examples/solid/kitchen-sink-solid-query/index.html b/examples/solid/kitchen-sink-solid-query/index.html
new file mode 100644
index 0000000000..9b6335c0ac
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/examples/solid/kitchen-sink-solid-query/package.json b/examples/solid/kitchen-sink-solid-query/package.json
new file mode 100644
index 0000000000..4171ea2dfa
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "tanstack-router-solid-example-kitchen-sink-solid-query",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "build": "vite build && tsc --noEmit",
+ "serve": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tanstack/solid-query": "^5.72.0",
+ "@tanstack/solid-query-devtools": "^5.72.0",
+ "@tanstack/solid-router": "^1.114.29",
+ "@tanstack/solid-router-devtools": "^1.114.29",
+ "immer": "^10.1.1",
+ "solid-js": "^1.9.5",
+ "redaxios": "^0.5.1",
+ "postcss": "^8.5.1",
+ "autoprefixer": "^10.4.20",
+ "tailwindcss": "^3.4.17",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "vite-plugin-solid": "^2.11.6",
+ "typescript": "^5.7.2",
+ "vite": "^6.1.0"
+ }
+}
diff --git a/examples/solid/kitchen-sink-solid-query/postcss.config.mjs b/examples/solid/kitchen-sink-solid-query/postcss.config.mjs
new file mode 100644
index 0000000000..2e7af2b7f1
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/solid/kitchen-sink-solid-query/src/Expensive.tsx b/examples/solid/kitchen-sink-solid-query/src/Expensive.tsx
new file mode 100644
index 0000000000..fd29d131b6
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/src/Expensive.tsx
@@ -0,0 +1,8 @@
+export default function Expensive() {
+ return (
+
+ I am an "expensive" component... which really just means that I was
+ code-split 😉
+
+ )
+}
diff --git a/examples/solid/kitchen-sink-solid-query/src/main.tsx b/examples/solid/kitchen-sink-solid-query/src/main.tsx
new file mode 100644
index 0000000000..a235041c8e
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/src/main.tsx
@@ -0,0 +1,1116 @@
+/* eslint-disable @typescript-eslint/no-unnecessary-condition */
+import * as Solid from 'solid-js'
+import { render } from 'solid-js/web'
+import {
+ ErrorComponent,
+ Link,
+ MatchRoute,
+ Outlet,
+ RouterProvider,
+ createRootRouteWithContext,
+ createRoute,
+ createRouter,
+ lazyRouteComponent,
+ redirect,
+ retainSearchParams,
+ useNavigate,
+ useRouter,
+ useRouterState,
+ useSearch,
+} from '@tanstack/solid-router'
+import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
+import {
+ QueryClient,
+ QueryClientProvider,
+ queryOptions,
+ useMutation,
+ useQuery,
+} from '@tanstack/solid-query'
+import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
+import { z } from 'zod'
+import {
+ fetchInvoiceById,
+ fetchInvoices,
+ fetchUserById,
+ fetchUsers,
+ patchInvoice,
+ postInvoice,
+} from './mockTodos'
+import type { Invoice } from './mockTodos'
+import './styles.css'
+
+//
+
+type UsersViewSortBy = 'name' | 'id' | 'email'
+
+const invoicesQueryOptions = () =>
+ queryOptions({
+ queryKey: ['invoices'],
+ queryFn: () => fetchInvoices(),
+ })
+
+const invoiceQueryOptions = (invoiceId: number) =>
+ queryOptions({
+ queryKey: ['invoices', invoiceId],
+ queryFn: () => fetchInvoiceById(invoiceId),
+ })
+
+const usersQueryOptions = ({
+ filterBy,
+ sortBy,
+}: {
+ filterBy?: string
+ sortBy?: UsersViewSortBy
+}) =>
+ queryOptions({
+ queryKey: ['users', { filterBy, sortBy }],
+ queryFn: () =>
+ fetchUsers({
+ filterBy,
+ sortBy,
+ }),
+ })
+
+const userQueryOptions = (userId: number) =>
+ queryOptions({
+ queryKey: ['users', userId],
+ queryFn: async () => {
+ const user = await fetchUserById(userId)
+ if (!user) {
+ throw new Error('User not found.')
+ }
+ return user
+ },
+ })
+
+const useCreateInvoiceMutation = () => {
+ return useMutation(() => ({
+ mutationKey: ['invoices', 'create'],
+ mutationFn: postInvoice,
+ onSuccess: () => queryClient.invalidateQueries(),
+ }))
+}
+
+const useUpdateInvoiceMutation = (invoiceId: number) => {
+ return useMutation(() => ({
+ mutationKey: ['invoices', 'update', invoiceId],
+ mutationFn: patchInvoice,
+ onSuccess: () => queryClient.invalidateQueries(),
+ gcTime: 1000 * 10,
+ }))
+}
+
+function RouterSpinner() {
+ const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
+ return
+}
+
+// Routes
+
+// Build our routes. We could do this in our component, too.
+const rootRoute = createRootRouteWithContext<{
+ auth: Auth
+ queryClient: QueryClient
+}>()({
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+ <>
+
+
+
Kitchen Sink
+ {/* Show a global spinner when the router is transitioning */}
+
+
+
+
+
+
+ {(
+ [
+ ['/', 'Home'],
+ ['/dashboard', 'Dashboard'],
+ ['/expensive', 'Expensive'],
+ ['/route-a', 'Pathless Layout A'],
+ ['/route-b', 'Pathless Layout B'],
+ ['/profile', 'Profile'],
+ ['/login', 'Login'],
+ ] as const
+ ).map(([to, label]) => {
+ return (
+
+
+ {label}
+
+
+ )
+ })}
+
+
+ {/* Render our first route match */}
+
+
+
+
+
+
+ >
+ )
+}
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+})
+
+function IndexComponent() {
+ return (
+
+
Welcome Home!
+
+
+ 1 New Invoice
+
+
+
+ As you navigate around take note of the UX. It should feel
+ suspense-like, where routes are only rendered once all of their data and
+ elements are ready.
+
+ To exaggerate async effects, play with the artificial request delay
+ slider in the bottom-left corner.
+
+ The last 2 sliders determine if link-hover preloading is enabled (and
+ how long those preloads stick around) and also whether to cache rendered
+ route data (and for how long). Both of these default to 0 (or off).
+
+
+ )
+}
+
+const dashboardLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'dashboard',
+ component: DashboardLayoutComponent,
+})
+
+function DashboardLayoutComponent() {
+ return (
+ <>
+
+
Dashboard
+
+
+ {(
+ [
+ ['/dashboard', 'Summary', true],
+ ['/dashboard/invoices', 'Invoices'],
+ ['/dashboard/users', 'Users'],
+ ] as const
+ ).map(([to, label, exact]) => {
+ return (
+
+ {label}
+
+ )
+ })}
+
+
+
+ >
+ )
+}
+
+const dashboardIndexRoute = createRoute({
+ getParentRoute: () => dashboardLayoutRoute,
+ path: '/',
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(invoicesQueryOptions()),
+ component: DashboardIndexComponent,
+})
+
+function DashboardIndexComponent() {
+ const invoicesQuery = useQuery(() => invoicesQueryOptions())
+ const invoices = invoicesQuery.data
+
+ return (
+
+
+ Welcome to the dashboard! You have{' '}
+ {invoices?.length} total invoices .
+
+
+ )
+}
+
+const invoicesLayoutRoute = createRoute({
+ getParentRoute: () => dashboardLayoutRoute,
+ path: 'invoices',
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(invoicesQueryOptions()),
+ component: InvoicesLayoutComponent,
+})
+
+function InvoicesLayoutComponent() {
+ const invoicesQuery = useQuery(() => invoicesQueryOptions())
+ const invoices = invoicesQuery.data
+ // const updateInvoiceMutation = useUpdateInvoiceMutation()
+ // const createInvoiceMutation = useCreateInvoiceMutation()
+
+ return (
+
+ {/* {routerTransitionIsPending ? 'pending' : 'null'} */}
+
+ {invoices?.map((invoice) => {
+ // const updateSubmission = updateInvoiceMutation.submissions.find(
+ // (d) => d.variables?.id === invoice.id,
+ // )
+
+ // if (updateSubmission) {
+ // invoice = {
+ // ...invoice,
+ // ...updateSubmission.variables,
+ // }
+ // }
+
+ return (
+
+
+
+ #{invoice.id} - {invoice.title.slice(0, 10)}{' '}
+ {/* {updateSubmission ? (
+
+ ) : ( */}
+
+ {(match) => }
+
+ {/* )} */}
+
+
+
+ )
+ })}
+ {/* {createSubmissions.map((action) => (
+
+ ))} */}
+
+
+
+
+
+ )
+}
+
+const invoicesIndexRoute = createRoute({
+ getParentRoute: () => invoicesLayoutRoute,
+ path: '/',
+ component: InvoicesIndexComponent,
+})
+
+function InvoicesIndexComponent() {
+ const createInvoiceMutation = useCreateInvoiceMutation()
+
+ return (
+ <>
+
+
{
+ event.preventDefault()
+ event.stopPropagation()
+ const formData = new FormData(event.target as HTMLFormElement)
+ createInvoiceMutation.mutate({
+ title: formData.get('title') as string,
+ body: formData.get('body') as string,
+ })
+ }}
+ class="space-y-2"
+ >
+ Create a new Invoice:
+
+
+
+ {createInvoiceMutation.status === 'pending' ? (
+ <>
+ Creating
+ >
+ ) : (
+ 'Create'
+ )}
+
+
+ {createInvoiceMutation.status === 'success' ? (
+
+ Created!
+
+ ) : createInvoiceMutation.status === 'error' ? (
+
+ Failed to create.
+
+ ) : null}
+
+
+ >
+ )
+}
+
+const invoiceRoute = createRoute({
+ getParentRoute: () => invoicesLayoutRoute,
+ path: '$invoiceId',
+ params: {
+ parse: (params) => ({
+ invoiceId: z.number().int().parse(Number(params.invoiceId)),
+ }),
+ stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
+ },
+ validateSearch: (search) =>
+ z
+ .object({
+ showNotes: z.boolean().optional(),
+ notes: z.string().optional(),
+ })
+ .parse(search),
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(
+ invoiceQueryOptions(opts.params.invoiceId),
+ ),
+ component: InvoiceComponent,
+})
+
+function InvoiceComponent() {
+ const params = invoiceRoute.useParams()
+ const search = invoiceRoute.useSearch()
+ const navigate = useNavigate({ from: invoiceRoute.fullPath })
+ const invoiceQuery = useQuery(() => invoiceQueryOptions(params().invoiceId))
+ const invoice = invoiceQuery.data
+ const updateInvoiceMutation = useUpdateInvoiceMutation(params().invoiceId)
+ const [notes, setNotes] = Solid.createSignal(search().notes ?? '')
+
+ Solid.createEffect(() => {
+ navigate({
+ search: (old: any) => ({
+ ...old,
+ notes: notes() ? notes() : undefined,
+ }),
+ replace: true,
+ params: true,
+ })
+ })
+
+ return (
+ {
+ event.preventDefault()
+ event.stopPropagation()
+ const formData = new FormData(event.target as HTMLFormElement)
+ updateInvoiceMutation.mutate({
+ id: invoice!.id,
+ title: formData.get('title') as string,
+ body: formData.get('body') as string,
+ })
+ }}
+ class="p-2 space-y-2"
+ >
+
+
+
({
+ ...old,
+ showNotes: old.showNotes ? undefined : true,
+ })}
+ class="text-blue-700"
+ from={invoiceRoute.fullPath}
+ params={true}
+ >
+ {search().showNotes ? 'Close Notes' : 'Show Notes'}{' '}
+
+ {search().showNotes ? (
+ <>
+
+
+
{
+ setNotes(e.target.value)
+ }}
+ rows={5}
+ class="shadow w-full p-2 rounded"
+ placeholder="Write some notes here..."
+ />
+
+ Notes are stored in the URL. Try copying the URL into a new tab!
+
+
+ >
+ ) : null}
+
+
+
+ Save
+
+
+ {updateInvoiceMutation.variables?.id === invoice?.id ? (
+
+ {updateInvoiceMutation.status === 'success' ? (
+
+ Saved!
+
+ ) : updateInvoiceMutation.status === 'error' ? (
+
+ Failed to save.
+
+ ) : null}
+
+ ) : null}
+
+ )
+}
+
+const usersLayoutRoute = createRoute({
+ getParentRoute: () => dashboardLayoutRoute,
+ path: 'users',
+ validateSearch: z.object({
+ usersView: z
+ .object({
+ sortBy: z.enum(['name', 'id', 'email']).optional(),
+ filterBy: z.string().optional(),
+ })
+ .optional(),
+ }).parse,
+ search: {
+ // Retain the usersView search param while navigating
+ // within or to this route (or it's children!)
+ middlewares: [retainSearchParams(['usersView'])],
+ },
+ loaderDeps: ({ search }) => ({
+ filterBy: search.usersView?.filterBy,
+ sortBy: search.usersView?.sortBy,
+ }),
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(usersQueryOptions(opts.deps)),
+ component: UsersComponent,
+})
+
+function UsersComponent() {
+ const navigate = useNavigate({ from: usersLayoutRoute.fullPath })
+ const search = usersLayoutRoute.useSearch()
+ const usersQuery = useQuery(() =>
+ usersQueryOptions(usersLayoutRoute.useLoaderDeps()),
+ )
+ const users = usersQuery.data
+ const sortBy = Solid.createMemo(() => search().usersView?.sortBy ?? 'name')
+ const filterBy = Solid.createMemo(() => search().usersView?.filterBy)
+
+ const [filterDraft, setFilterDraft] = Solid.createSignal(filterBy() ?? '')
+
+ Solid.createEffect(() => {
+ setFilterDraft(filterBy() ?? '')
+ })
+
+ const sortedUsers = Solid.createMemo(() => {
+ if (!users) return []
+
+ return !sortBy
+ ? users
+ : [...users].sort((a: any, b: any) => {
+ return a[sortBy()] > b[sortBy()] ? 1 : -1
+ })
+ })
+
+ const filteredUsers = Solid.createMemo(() => {
+ if (!filterBy()) return sortedUsers()
+
+ return sortedUsers().filter((user) =>
+ user.name.toLowerCase().includes(filterBy()?.toLowerCase() ?? ''),
+ )
+ })
+
+ const setSortBy = (sortBy: UsersViewSortBy) =>
+ navigate({
+ search: (old: any) => {
+ return {
+ ...old,
+ usersView: {
+ ...(old.usersView ?? {}),
+ sortBy,
+ },
+ }
+ },
+ replace: true,
+ })
+
+ Solid.createEffect(() => {
+ navigate({
+ search: (old: any) => {
+ return {
+ ...old,
+ usersView: {
+ ...old.usersView,
+ filterBy: filterDraft() || undefined,
+ },
+ }
+ },
+ replace: true,
+ })
+ }, [filterDraft])
+
+ return (
+
+
+
+
Sort By:
+
setSortBy(e.target.value as UsersViewSortBy)}
+ class="flex-1 border p-1 px-2 rounded"
+ >
+ {['name', 'id', 'email'].map((d) => {
+ return
+ })}
+
+
+
+
Filter By:
+
setFilterDraft(e.target.value)}
+ placeholder="Search Names..."
+ class="min-w-0 flex-1 border p-1 px-2 rounded"
+ />
+
+ {filteredUsers().map((user) => {
+ return (
+
+
+
+ {user.name}{' '}
+
+ {(match) => }
+
+
+
+
+ )
+ })}
+
+
+
+
+
+ )
+}
+
+const usersIndexRoute = createRoute({
+ getParentRoute: () => usersLayoutRoute,
+ path: '/',
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return (
+
+
+ Normally, setting default search parameters would either need to be done
+ manually in every link to a page, or as a side-effect (not a great
+ experience).
+
+
+ Instead, we can use search filters to provide defaults
+ or even persist search params for links to routes (and child routes).
+
+
+ A good example of this is the sorting and filtering of the users list.
+ In a traditional router, both would be lost while navigating around
+ individual users or even changing each sort/filter option unless each
+ state was manually passed from the current route into each new link we
+ created (that's a lot of tedious and error-prone work). With TanStack
+ router and search filters, they are persisted with little effort.
+
+
+ )
+}
+
+const userRoute = createRoute({
+ getParentRoute: () => usersLayoutRoute,
+ path: 'user',
+ validateSearch: z.object({
+ userId: z.number(),
+ }),
+ loaderDeps: ({ search }) => ({
+ userId: search.userId,
+ }),
+ loader: (opts) =>
+ opts.context.queryClient.ensureQueryData(
+ userQueryOptions(opts.deps.userId),
+ ),
+ component: UserComponent,
+})
+
+function UserComponent() {
+ const search = userRoute.useSearch()
+ const userQuery = useQuery(() => userQueryOptions(search().userId))
+ const user = userQuery.data
+
+ return (
+ <>
+ {user?.name}
+
+ {JSON.stringify(user, null, 2)}
+
+ >
+ )
+}
+
+const expensiveRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ // Your elements can be asynchronous, which means you can code-split!
+ path: 'expensive',
+ component: lazyRouteComponent(() => import('./Expensive')),
+})
+
+const authPathlessLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: 'auth',
+ // Before loading, authenticate the user via our auth context
+ // This will also happen during prefetching (e.g. hovering over links, etc)
+ beforeLoad: ({ context, location }) => {
+ // If the user is logged out, redirect them to the login page
+ if (context.auth.status === 'loggedOut') {
+ throw redirect({
+ to: loginRoute.to,
+ search: {
+ // Use the current location to power a redirect after login
+ // (Do not use `router.state.resolvedLocation` as it can
+ // potentially lag behind the actual current location)
+ redirect: location.href,
+ },
+ })
+ }
+
+ // Otherwise, return the user in context
+ return {
+ username: auth.username,
+ }
+ },
+})
+
+const profileRoute = createRoute({
+ getParentRoute: () => authPathlessLayoutRoute,
+ path: 'profile',
+ component: ProfileComponent,
+})
+
+function ProfileComponent() {
+ const routeContext = profileRoute.useRouteContext()
+
+ return (
+
+
+ Username:{routeContext().username}
+
+
+ )
+}
+
+const loginRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'login',
+ validateSearch: z.object({
+ redirect: z.string().optional(),
+ }),
+}).update({
+ component: LoginComponent,
+})
+
+function LoginComponent() {
+ const router = useRouter()
+ const routeContext = loginRoute.useRouteContext({
+ select: ({ auth }) => ({ auth, status: auth.status }),
+ })
+ const search = useSearch({ from: loginRoute.fullPath })
+ const [username, setUsername] = Solid.createSignal('')
+
+ const onSubmit = (e: any) => {
+ e.preventDefault()
+ routeContext()?.auth.login(username())
+ router.invalidate()
+ }
+
+ // Ah, the subtle nuances of client side auth. 🙄
+ Solid.createRenderEffect(() => {
+ if (routeContext().status === 'loggedIn' && search().redirect) {
+ router.history.push(search().redirect!)
+ }
+ }, [routeContext().status, search().redirect])
+
+ return status === 'loggedIn' ? (
+
+ Logged in as
{routeContext().auth.username}
+
+
routeContext().auth.logout()}
+ class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded"
+ >
+ Log out
+
+
+
+ ) : (
+
+
You must log in!
+
+
+ setUsername(e.target.value)}
+ placeholder="Username"
+ class="border p-1 px-2 rounded"
+ />
+ routeContext().auth.logout()}
+ class="text-sm bg-blue-500 text-white border inline-block py-1 px-2 rounded"
+ >
+ Login
+
+
+
+ )
+}
+
+const pathlessLayoutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: 'pathlessLayout',
+ component: PathlessLayoutComponent,
+})
+
+function PathlessLayoutComponent() {
+ return (
+
+ )
+}
+
+const pathlessLayoutARoute = createRoute({
+ getParentRoute: () => pathlessLayoutRoute,
+ path: 'route-a',
+ component: PathlessLayoutAComponent,
+})
+
+function PathlessLayoutAComponent() {
+ return (
+
+ )
+}
+
+const pathlessLayoutBRoute = createRoute({
+ getParentRoute: () => pathlessLayoutRoute,
+ path: 'route-b',
+ component: PathlessLayoutBComponent,
+})
+
+function PathlessLayoutBComponent() {
+ return (
+
+ )
+}
+
+const routeTree = rootRoute.addChildren([
+ indexRoute,
+ dashboardLayoutRoute.addChildren([
+ dashboardIndexRoute,
+ invoicesLayoutRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+ usersLayoutRoute.addChildren([usersIndexRoute, userRoute]),
+ ]),
+ expensiveRoute,
+ authPathlessLayoutRoute.addChildren([profileRoute]),
+ loginRoute,
+ pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]),
+])
+
+const queryClient = new QueryClient()
+
+const router = createRouter({
+ routeTree,
+ defaultPendingComponent: () => (
+
+
+
+ ),
+ defaultErrorComponent: ({ error }) => ,
+ context: {
+ auth: undefined!, // We'll inject this when we render
+ queryClient,
+ },
+ defaultPreload: 'intent',
+ // Since we're using React Query, we don't want loader calls to ever be stale
+ // This will ensure that the loader is always called when the route is preloaded or visited
+ defaultPreloadStaleTime: 0,
+ scrollRestoration: true,
+})
+
+declare module '@tanstack/solid-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+const auth: Auth = {
+ status: 'loggedOut',
+ username: undefined,
+ login: (username: string) => {
+ auth.status = 'loggedIn'
+ auth.username = username
+ },
+ logout: () => {
+ auth.status = 'loggedOut'
+ auth.username = undefined
+ },
+}
+
+function App() {
+ // This stuff is just to tweak our sandbox setup in real-time
+ const [loaderDelay, setLoaderDelay] = useSessionStorage('loaderDelay', 500)
+ const [pendingMs, setPendingMs] = useSessionStorage('pendingMs', 1000)
+ const [pendingMinMs, setPendingMinMs] = useSessionStorage('pendingMinMs', 500)
+
+ return (
+ <>
+
+
+
+ {
+ setLoaderDelay(150)
+ }}
+ >
+ Fast
+
+ {
+ setLoaderDelay(500)
+ }}
+ >
+ Fast 3G
+
+ {
+ setLoaderDelay(2000)
+ }}
+ >
+ Slow 3G
+
+
+
+
Loader Delay: {loaderDelay()}ms
+
setLoaderDelay(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
+
+ {
+ setPendingMs(1000)
+ setPendingMinMs(500)
+ }}
+ >
+ Reset to Default
+
+
+
+
defaultPendingMs: {pendingMs()}ms
+
setPendingMs(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
defaultPendingMinMs: {pendingMinMs()}ms
+
setPendingMinMs(e.target.valueAsNumber)}
+ class="w-full"
+ />
+
+
+
+
+
+
+ >
+ )
+}
+
+function InvoiceFields({
+ invoice,
+ disabled,
+}: {
+ invoice: Invoice
+ disabled?: boolean
+}) {
+ return (
+
+ )
+}
+
+type Auth = {
+ login: (username: string) => void
+ logout: () => void
+ status: 'loggedOut' | 'loggedIn'
+ username?: string
+}
+
+function Spinner({ show, wait }: { show?: boolean; wait?: `delay-${number}` }) {
+ return (
+
+ ⍥
+
+ )
+}
+
+function useSessionStorage(key: string, initialValue: T) {
+ const stored = sessionStorage.getItem(key)
+ const [state, setState] = Solid.createSignal(
+ stored ? JSON.parse(stored) : initialValue,
+ )
+
+ Solid.createEffect(() => {
+ sessionStorage.setItem(key, JSON.stringify(state()))
+ })
+
+ return [state, setState]
+}
+
+const rootElement = document.getElementById('app')!
+if (!rootElement.innerHTML) {
+ render(() => , rootElement)
+}
diff --git a/examples/solid/kitchen-sink-solid-query/src/mockTodos.ts b/examples/solid/kitchen-sink-solid-query/src/mockTodos.ts
new file mode 100644
index 0000000000..475d0fa88f
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/src/mockTodos.ts
@@ -0,0 +1,174 @@
+import axios from 'redaxios'
+import { produce } from 'immer'
+import { actionDelayFn, loaderDelayFn, shuffle } from './utils'
+
+type PickAsRequired = Omit &
+ Required>
+
+export type Invoice = {
+ id: number
+ title: string
+ body: string
+}
+
+export interface User {
+ id: number
+ name: string
+ username: string
+ email: string
+ address: Address
+ phone: string
+ website: string
+ company: Company
+}
+
+export interface Address {
+ street: string
+ suite: string
+ city: string
+ zipcode: string
+ geo: Geo
+}
+
+export interface Geo {
+ lat: string
+ lng: string
+}
+
+export interface Company {
+ name: string
+ catchPhrase: string
+ bs: string
+}
+
+let invoices: Array = null!
+let users: Array = null!
+
+let invoicesPromise: Promise | undefined = undefined
+let usersPromise: Promise | undefined = undefined
+
+const ensureInvoices = async () => {
+ if (!invoicesPromise) {
+ invoicesPromise = Promise.resolve().then(async () => {
+ const { data } = await axios.get(
+ 'https://jsonplaceholder.typicode.com/posts',
+ )
+ invoices = data.slice(0, 10)
+ })
+ }
+
+ await invoicesPromise
+}
+
+const ensureUsers = async () => {
+ if (!usersPromise) {
+ usersPromise = Promise.resolve().then(async () => {
+ const { data } = await axios.get(
+ 'https://jsonplaceholder.typicode.com/users',
+ )
+ users = data.slice(0, 10)
+ })
+ }
+
+ await usersPromise
+}
+
+export async function fetchInvoices() {
+ return loaderDelayFn(() => ensureInvoices().then(() => invoices))
+}
+
+export async function fetchInvoiceById(id: number) {
+ return loaderDelayFn(() =>
+ ensureInvoices().then(() => {
+ const invoice = invoices.find((d) => d.id === id)
+ if (!invoice) {
+ throw new Error('Invoice not found')
+ }
+ return invoice
+ }),
+ )
+}
+
+export async function postInvoice(partialInvoice: Partial) {
+ return actionDelayFn(() => {
+ if (partialInvoice.title?.includes('error')) {
+ console.error('error')
+ throw new Error('Ouch!')
+ }
+ const invoice = {
+ id: invoices.length + 1,
+ title:
+ partialInvoice.title ?? `New Invoice ${String(Date.now()).slice(0, 5)}`,
+ body:
+ partialInvoice.body ??
+ shuffle(
+ `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam nec ante.
+ Vestibulum sapien. Proin quam. Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. Sed lectus. Integer euismod lacus luctus magna. Integer id quam. Morbi mi. Quisque nisl felis, venenatis tristique, dignissim in, ultrices sit amet, augue. Proin sodales libero eget ante.
+ `.split(' '),
+ ).join(' '),
+ }
+ invoices = [...invoices, invoice]
+ return invoice
+ })
+}
+
+export async function patchInvoice({
+ id,
+ ...updatedInvoice
+}: PickAsRequired, 'id'>) {
+ return actionDelayFn(() => {
+ invoices = produce(invoices, (draft) => {
+ const invoice = draft.find((d) => d.id === id)
+ if (!invoice) {
+ throw new Error('Invoice not found.')
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (updatedInvoice.title?.toLocaleLowerCase()?.includes('error')) {
+ throw new Error('Ouch!')
+ }
+ Object.assign(invoice, updatedInvoice)
+ })
+
+ return invoices.find((d) => d.id === id)
+ })
+}
+
+export type UsersSortBy = 'name' | 'id' | 'email'
+
+export async function fetchUsers({
+ filterBy,
+ sortBy,
+}: { filterBy?: string; sortBy?: UsersSortBy } = {}) {
+ return loaderDelayFn(() =>
+ ensureUsers().then(() => {
+ let usersDraft = users
+
+ if (filterBy) {
+ usersDraft = usersDraft.filter((d) =>
+ d.name.toLowerCase().includes(filterBy.toLowerCase()),
+ )
+ }
+
+ if (sortBy) {
+ usersDraft = [...usersDraft].sort((a, b) => {
+ return a[sortBy] > b[sortBy] ? 1 : -1
+ })
+ }
+
+ return usersDraft
+ }),
+ )
+}
+
+export async function fetchUserById(id: number) {
+ return loaderDelayFn(() =>
+ ensureUsers().then(() => users.find((d) => d.id === id)),
+ )
+}
+
+export async function fetchRandomNumber() {
+ return loaderDelayFn(() => {
+ return Math.random()
+ })
+}
diff --git a/examples/solid/kitchen-sink-solid-query/src/styles.css b/examples/solid/kitchen-sink-solid-query/src/styles.css
new file mode 100644
index 0000000000..0b8e317099
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/examples/solid/kitchen-sink-solid-query/src/utils.tsx b/examples/solid/kitchen-sink-solid-query/src/utils.tsx
new file mode 100644
index 0000000000..6435657b12
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/src/utils.tsx
@@ -0,0 +1,33 @@
+export async function loaderDelayFn(
+ fn: (...args: Array) => Promise | T,
+) {
+ const delay = Number(sessionStorage.getItem('loaderDelay') ?? 0)
+ const delayPromise = new Promise((r) => setTimeout(r, delay))
+
+ await delayPromise
+ const res = await fn()
+
+ return res
+}
+
+export async function actionDelayFn(
+ fn: (...args: Array) => Promise | T,
+) {
+ const delay = Number(sessionStorage.getItem('actionDelay') ?? 0)
+ await new Promise((r) => setTimeout(r, delay))
+ return fn()
+}
+
+export function shuffle(arr: Array): Array {
+ let i = arr.length
+ if (i == 0) return arr
+ const copy = [...arr]
+ while (--i) {
+ const j = Math.floor(Math.random() * (i + 1))
+ const a = copy[i]
+ const b = copy[j]
+ copy[i] = b!
+ copy[j] = a!
+ }
+ return copy
+}
diff --git a/examples/solid/kitchen-sink-solid-query/tailwind.config.mjs b/examples/solid/kitchen-sink-solid-query/tailwind.config.mjs
new file mode 100644
index 0000000000..4986094b9d
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/examples/solid/kitchen-sink-solid-query/tsconfig.dev.json b/examples/solid/kitchen-sink-solid-query/tsconfig.dev.json
new file mode 100644
index 0000000000..285a09b0dc
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+ "composite": true,
+ "extends": "../../../tsconfig.base.json",
+
+ "files": ["src/main.tsx"],
+ "include": [
+ "src"
+ // "__tests__/**/*.test.*"
+ ]
+}
diff --git a/examples/solid/kitchen-sink-solid-query/tsconfig.json b/examples/solid/kitchen-sink-solid-query/tsconfig.json
new file mode 100644
index 0000000000..a972899814
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/solid/kitchen-sink-solid-query/vite.config.js b/examples/solid/kitchen-sink-solid-query/vite.config.js
new file mode 100644
index 0000000000..05041cc6d8
--- /dev/null
+++ b/examples/solid/kitchen-sink-solid-query/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import solid from 'vite-plugin-solid'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [solid()],
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b958b69ec2..50f8023701 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5474,6 +5474,101 @@ importers:
specifier: ^2.11.2
version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
+ examples/solid/kitchen-sink-solid-query:
+ dependencies:
+ '@tanstack/solid-query':
+ specifier: ^5.72.0
+ version: 5.72.0(solid-js@1.9.5)
+ '@tanstack/solid-query-devtools':
+ specifier: ^5.72.0
+ version: 5.72.0(@tanstack/solid-query@5.72.0(solid-js@1.9.5))(solid-js@1.9.5)
+ '@tanstack/solid-router':
+ specifier: ^1.114.29
+ version: link:../../../packages/solid-router
+ '@tanstack/solid-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router-devtools
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.5.3)
+ immer:
+ specifier: ^10.1.1
+ version: 10.1.1
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.3
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ solid-js:
+ specifier: ^1.9.5
+ version: 1.9.5
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.17
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.2
+ devDependencies:
+ typescript:
+ specifier: ^5.7.2
+ version: 5.8.2
+ vite:
+ specifier: 6.1.4
+ version: 6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)
+ vite-plugin-solid:
+ specifier: ^2.11.6
+ version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
+
+ examples/solid/kitchen-sink-solid-query-file-based:
+ dependencies:
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ '@tanstack/solid-query':
+ specifier: ^5.72.0
+ version: 5.72.0(solid-js@1.9.5)
+ '@tanstack/solid-query-devtools':
+ specifier: ^5.72.0
+ version: 5.72.0(@tanstack/solid-query@5.72.0(solid-js@1.9.5))(solid-js@1.9.5)
+ '@tanstack/solid-router':
+ specifier: ^1.114.29
+ version: link:../../../packages/solid-router
+ '@tanstack/solid-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router-devtools
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.5.3)
+ immer:
+ specifier: ^10.1.1
+ version: 10.1.1
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.3
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ solid-js:
+ specifier: ^1.9.5
+ version: 1.9.5
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.17
+ zod:
+ specifier: ^3.24.2
+ version: 3.24.2
+ devDependencies:
+ typescript:
+ specifier: ^5.7.2
+ version: 5.8.2
+ vite:
+ specifier: 6.1.4
+ version: 6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)
+ vite-plugin-solid:
+ specifier: ^2.11.6
+ version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.1.4(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
+
examples/solid/quickstart-file-based:
dependencies:
'@tanstack/solid-router':
@@ -9926,12 +10021,18 @@ packages:
'@tanstack/query-core@5.71.5':
resolution: {integrity: sha512-XOQ5SyjCdwhxyLksGKWSL5poqyEXYPDnsrZAzJm2LgrMm4Yh6VOrfC+IFosXreDw9HNqC11YAMY3HlfHjNzuaA==}
+ '@tanstack/query-core@5.72.0':
+ resolution: {integrity: sha512-aa3p6Mou++JLLxxxVX9AB9uGeRIGc0JWkw96GASXuMG8K3D+JpYbSFcqXbkGFJ1eX2jKHPurmCBoO43RjjXJCA==}
+
'@tanstack/query-devtools@5.67.2':
resolution: {integrity: sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg==}
'@tanstack/query-devtools@5.71.5':
resolution: {integrity: sha512-Fq1JeAp+I52Md/KnyeFxzG7G0RpdHgeOfDNhSPkZQs/JqqXuAfpUf+wFHDz+vP0GZbSnla2JmcLSQebOkIb1yA==}
+ '@tanstack/query-devtools@5.72.0':
+ resolution: {integrity: sha512-/SJr77+epgxmwMB5Wjlg1TiN6bJObAJKi/mIHWQcon0VJxH3NNOtGCpQRbWih8xOq9oQ5kuIYLftYfD0j2ujew==}
+
'@tanstack/react-query-devtools@5.67.2':
resolution: {integrity: sha512-cmj2DxBc+/9btQ66n5xI8wTtAma2BLVa403K7zIYiguzJ/kV201jnGensYqJeu1Rd8uRMLLRM74jLVMLDWNRJA==}
peerDependencies:
@@ -9961,11 +10062,22 @@ packages:
'@tanstack/solid-query': ^5.71.9
solid-js: ^1.6.0
+ '@tanstack/solid-query-devtools@5.72.0':
+ resolution: {integrity: sha512-K4Gdyk/yx8OSAxO/hwol1Ou2fNOGmZ9m4E8fRrUP5wzcDXaP3Lh1a6CRg6bunRiE6/12rNdy7xpvyZIsyKCNBQ==}
+ peerDependencies:
+ '@tanstack/solid-query': ^5.72.0
+ solid-js: ^1.6.0
+
'@tanstack/solid-query@5.71.9':
resolution: {integrity: sha512-ue9xAX2okLGdXxSWcnYfOedL/QgOx9eNKEJdoM9D3RF5xxxn3p+dXp7+Buzqu2OmfwRnAjHkWRASgS/9NlDMLg==}
peerDependencies:
solid-js: ^1.6.0
+ '@tanstack/solid-query@5.72.0':
+ resolution: {integrity: sha512-zQ27dZEakN05hZB9C+qTKgtBU3N7Li3rx++nUOd4K5LtrTMjiIEp2FEbYoGe9B0Pn9tF5CIthc9fknl/qBVI5g==}
+ peerDependencies:
+ solid-js: ^1.6.0
+
'@tanstack/solid-store@0.7.0':
resolution: {integrity: sha512-uDQYkUuH3MppitiduZLTEcItkTr8vEJ33jzp2rH2VvlNRMGbuU54GQcqf3dLIlTbZ1/Z2TtIBtBjjl+N/OhwRg==}
peerDependencies:
@@ -18114,10 +18226,14 @@ snapshots:
'@tanstack/query-core@5.71.5': {}
+ '@tanstack/query-core@5.72.0': {}
+
'@tanstack/query-devtools@5.67.2': {}
'@tanstack/query-devtools@5.71.5': {}
+ '@tanstack/query-devtools@5.72.0': {}
+
'@tanstack/react-query-devtools@5.67.2(@tanstack/react-query@5.66.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@tanstack/query-devtools': 5.67.2
@@ -18148,11 +18264,22 @@ snapshots:
'@tanstack/solid-query': 5.71.9(solid-js@1.9.5)
solid-js: 1.9.5
+ '@tanstack/solid-query-devtools@5.72.0(@tanstack/solid-query@5.72.0(solid-js@1.9.5))(solid-js@1.9.5)':
+ dependencies:
+ '@tanstack/query-devtools': 5.72.0
+ '@tanstack/solid-query': 5.72.0(solid-js@1.9.5)
+ solid-js: 1.9.5
+
'@tanstack/solid-query@5.71.9(solid-js@1.9.5)':
dependencies:
'@tanstack/query-core': 5.71.5
solid-js: 1.9.5
+ '@tanstack/solid-query@5.72.0(solid-js@1.9.5)':
+ dependencies:
+ '@tanstack/query-core': 5.72.0
+ solid-js: 1.9.5
+
'@tanstack/solid-store@0.7.0(solid-js@1.9.5)':
dependencies:
'@tanstack/store': 0.7.0