Loading...
Loading...
Comprehensive guide for TanStack ecosystem in React - Query/DB for data fetching, Form for form handling, and Router for client-side routing. Use when working with collections, live queries, optimistic updates, forms, validation, routing, URL parameters, or navigation.
npx skill4agent add pedronauck/skills tanstackreferences/references/query-patterns.mdreferences/form-patterns.mdreferences/router-patterns.mduseMutationinvalidateQueriesonInsertonUpdateonDeleteonUpdateimport { createCollection } from '@tanstack/react-db';
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { z } from 'zod';
const itemSchema = z.object({
id: z.string(),
name: z.string().min(1),
status: z.enum(['active', 'archived']),
});
const itemCollection = createCollection(
queryCollectionOptions({
queryKey: ['items'],
queryFn: async () => (await fetch('/api/items')).json(),
queryClient,
getKey: (item) => item.id,
schema: itemSchema,
})
);// CORRECT - share the instance
export function useItems() {
const collection = useMemo(() => createItemsCollection(), []);
const { data } = useLiveQuery(collection);
return { data, collection }; // Expose collection
}
export function useUpdateItem(collection: ItemsCollection) {
return (id, data) => collection.update(id, data);
}createFormHookimport { createFormHookContexts, createFormHook } from '@tanstack/react-form'
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
SelectField,
},
formComponents: {
SubmitButton,
},
})const form = useAppForm({
defaultValues: {
username: '',
email: '',
age: 0,
},
validators: {
onChange: schema,
},
onSubmit: async ({ value }) => {
// Handle submission
},
})<form.Field
name="username"
asyncDebounceMs={500}
validators={{
onChangeAsync: async ({ value }) => {
const isAvailable = await checkUsernameAvailability(value)
return isAvailable ? undefined : 'Username already taken'
},
}}
/>src/routes/
├── __root.tsx # Root layout with providers
├── _authenticated.tsx # Auth layout wrapper
├── index.tsx # Home page (/)
├── posts/
│ ├── index.tsx # /posts
│ └── $postId.tsx # /posts/:postId (typed params)
└── settings/
├── _layout.tsx # Settings layout
└── profile.tsx # /settings/profileimport { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().min(1).catch(1),
search: z.string().optional(),
})
export const Route = createFileRoute('/posts/')({
validateSearch: searchSchema,
component: PostsList,
})
function PostsList() {
const { page, search } = Route.useSearch()
// Use search params...
}// routes/_authenticated.tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
const isAuthenticated = checkAuth()
if (!isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
component: () => <Outlet />,
})import { Link, useNavigate } from '@tanstack/react-router'
function Navigation() {
const navigate = useNavigate()
return (
<>
<Link
to="/posts/$postId"
params={{ postId: '123' }}
search={{ tab: 'comments' }}
>
View Post
</Link>
<button onClick={() => navigate({ to: '/posts', search: { page: 1 } })}>
Go to Posts
</button>
</>
)
}onInsertonUpdateonDeleteuseMutationinvalidateQueriesonUpdatecreateFormHookuseAppFormuseFormrole="alert"createFileRoute.catch()loaderDepsbeforeLoadLinkuseNavigatepnpm run typecheckpnpm run test