This is a TanStack Start project with tRPC API running on Start's server functions so it's easily deployable to many hosting platforms.
All reads from the Postgres database are done via tRPC queries which populate TanStack DB query collections.
We sync normalized data from tables into TanStack DB collections in the client & then write client-side queries for displaying data in components.
To create a new project based on this starter, run the following commands:
npx gitpick tanstack/db/tree/main/examples/react/projects my-tanstack-db-project
cd my-tanstack-db-project
Copy the .env.example file to .env and fill in the values.
The database url will be set by default to development postgres docker container, and during development the better-auth secret is not required.
cp .env.example .env
Note: Docker is required to run this starter
To run this application:
npm install
npm run dev
# From a separate terminal
npm run migrateTo build this application for production:
npm run buildThis project uses Vitest for testing. You can run the tests with:
npm run testThe starter includes an AGENT.md. Depending on which AI coding tool you use, you may need to copy/move it to the right file name e.g. .cursor/rules.
This project uses Tailwind CSS for styling.
This project uses TanStack Router. The initial setup is a file based router. Which means that the routes are managed as files in src/routes.
To add a new route to your application just add another a new file in the ./src/routes directory.
TanStack will automatically generate the content of the route file for you.
Now that you have two routes you can use a Link component to navigate between them.
To use SPA (Single Page Application) navigation you will need to import the Link component from @tanstack/react-router.
import { Link } from '@tanstack/react-router'Then anywhere in your JSX you can use it like so:
<Link to="/about">About</Link>This will create a link that will navigate to the /about route.
More information on the Link component can be found in the Link documentation.
In the File Based Routing setup the layout is located in src/routes/__root.tsx. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the <Outlet /> component.
Here is an example layout that includes a header:
import { Outlet, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { Link } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
</header>
<Outlet />
<TanStackRouterDevtools />
</>
),
})The <TanStackRouterDevtools /> component is not required so you can remove it if you don't want it in your layout.
More information on layouts can be found in the Layouts documentation.
There are multiple ways to fetch data in your application. You can use TanStack DB to fetch data from a server. But you can also use the loader functionality built into TanStack Router to load the data for a route before it's rendered.
For example:
const peopleRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/people',
loader: async () => {
const response = await fetch('https://swapi.dev/api/people')
return response.json() as Promise<{
results: {
name: string
}[]
}>
},
component: () => {
const data = peopleRoute.useLoaderData()
return (
<ul>
{data.results.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
)
},
})Loaders simplify your data fetching logic dramatically. Check out more information in the Loader documentation.
TanStack DB gives you robust support for live queries and optimistic mutations. With no stale data, super fast re-rendering and sub-millisecond cross-collection queries — even for large complex apps.
Built on a TypeScript implementation of differential dataflow, TanStack DB provides:
- 🔥 Blazing fast query engine - sub-millisecond live queries, even for complex queries with joins and aggregates
- 🎯 Fine-grained reactivity - minimize component re-rendering
- 💪 Robust transaction primitives - easy optimistic mutations with sync and lifecycle support
- 🌟 Normalized data - keep your backend simple
Collections - Typed sets of objects that can mirror a backend table or be populated with filtered views like pendingTodos or decemberNewTodos. Collections are just JavaScript data that you can load on demand.
Live Queries - Run reactively against and across collections with support for joins, filters and aggregates. Powered by differential dataflow, query results update incrementally without re-running the whole query.
Transactional Optimistic Mutations - Batch and stage local changes across collections with immediate application of local optimistic updates. Sync transactions to the backend with automatic rollbacks and management of optimistic state.
This example uses Query Collections for server-state synchronization with tRPC:
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
import { QueryClient } from '@tanstack/query-core'
const queryClient = new QueryClient()
export const todoCollection = createCollection(
queryCollectionOptions<Todo>({
id: 'todos',
queryKey: ['todos'],
queryFn: async () => {
const todos = await trpc.todos.getAll.query()
return todos.map((todo) => ({
...todo,
created_at: new Date(todo.created_at),
updated_at: new Date(todo.updated_at),
}))
},
queryClient,
schema: todoSchema,
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
const { modified: newTodo } = transaction.mutations[0]
const result = await trpc.todos.create.mutate({
text: newTodo.text,
completed: newTodo.completed,
project_id: newTodo.project_id,
})
return { txid: result.txid }
},
// You can also implement onUpdate, onDelete as needed
})
)Apply mutations with local optimistic state that automatically syncs:
const AddTodo = () => {
return (
<Button
onClick={() =>
todoCollection.insert({
id: crypto.randomUUID(),
text: '🔥 Make app faster',
completed: false,
})
}
/>
)
}Use live queries to read data reactively across collections:
import { useLiveQuery } from '@tanstack/react-db'
const Todos = () => {
// Read data using live queries with cross-collection joins
const { data: todos } = useLiveQuery((query) =>
query
.from({ t: todoCollection })
.join({
type: 'inner',
from: { l: listCollection },
on: [`@l.id`, `=`, `@t.list_id`],
})
.where('@l.active', '=', true)
.select('@t.id', '@t.text', '@t.status', '@l.name')
)
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text} - {todo.name}
</li>
))}
</ul>
)
}This pattern provides blazing fast, cross-collection live queries and local optimistic mutations with automatically managed optimistic state, all synced with your backend via tRPC.
You can learn more about TanStack DB in the TanStack DB documentation.
You can learn more about all of the offerings from TanStack in the TanStack documentation.