TanStack Start
TanStack Start is a full-stack React framework powered by TanStack Router. It provides full-document SSR, streaming, server functions, bundling, and more.
When used with Convex, TanStack Start provides
- Live-updating queries with React Query (the React client for TanStack Query)
- Subscription session resumption, from SSR to live on the client
- Loader-based preloading and prefetching
- Consistent logical query timestamp during SSR
- Opt-in component-local SSR
and more!
This page describes the recommended way to use Convex with TanStack Start, via React Query. The standard Convex React hooks work also with TanStack Start without React Query, as do the React Query hooks without TanStack Start! But using all three is a sweet spot.
TanStack Start is a new React framework currently in the Release Candidate stage. You can use it today but there may be bugs or breaking changes before a stable release.
Getting started
Follow the TanStack Start Quickstart to add Convex to a new TanStack Start project.
Using Convex with React Query
You can read more about React Query hooks, but a few highlights relevant to TanStack Start.
Staying subscribed to queries
Convex queries in React Query continue to receive updates after the last
component subscribed to the query unmounts. The default for this behavior is 5
minutes and this value is configured with
gcTime.
This is useful to know when debugging why a query result is already loaded: for client side navigations, whether a subscription is already active can depend on what pages were previously visited in a session.
Open the React Query Devtools to observe subscriptions staying active as you navigate.
Using Convex React hooks
Convex React hooks like
usePaginatedQuery can be used
alongside TanStack hooks. These hooks reference the same Convex Client so
there's still just one set of consistent query results in your app when these
are combined.
Server-side Rendering
Using TanStack Start and Query with Convex makes it particularly easy to
live-update Convex queries on the client while also
server-rendering
them.
useSuspenseQuery()
kicks off data fetching during the initial SSR pass on the server, while
useQuery() does not. This makes useSuspenseQuery the simplest way to
server-render Convex data:
const { data } = useSuspenseQuery(convexQuery(api.messages.list, {}));
After server-rendering query results, the Convex client in the browser resumes live subscriptions from where SSR left off, so there's no flash of loading state or redundant data fetching.
Consistent client views
In the browser all Convex query subscriptions present a consistent, at-the-same-logical-timestamp view of the database: if one query result reflects a given mutation transaction, every other query result will too.
Server-side rendering is usually a special case: instead of a stateful WebSocket session, on the server it's simpler to fetch query results ad-hoc. This can lead to inconsistencies analogous to one REST endpoint returning results before a mutation ran and another endpoint returning results after that change.
In TanStack Start, this issue is avoided by sending in a timestamp along with each query: Convex uses the same timestamp for all queries.
Loaders
TanStack Start routes can have isomorphic loader functions that run on the server for the initial page load and on the client for subsequent client-side navigations. By default, loaders will also run when mousing over a link to a page, enabling prefetching.
Adding queries to loaders moves away from the convenience of co-locating data
fetching in components with useSuspenseQuery in favor of faster page loads.
Consider whether a given route benefits from this trade-off.
Use ensureQueryData to block rendering until data is available. This is a good
fit when the component requires the data to render:
export const Route = createFileRoute('/posts')({
loader: async (opts) => {
await opts.context.queryClient.ensureQueryData(
convexQuery(api.messages.list, {}),
);
},
component: () => {
const { data } = useSuspenseQuery(convexQuery(api.messages.list, {}));
return (
<div>
{data.map((message) => (
<Message key={message.id} post={message} />
))}
</div>
);
},
})
Use prefetchQuery to start the request without blocking rendering. This is a
good fit when data would be nice to have early but the component can handle a
loading state:
export const Route = createFileRoute('/posts')({
loader: async (opts) => {
opts.context.queryClient.prefetchQuery(
convexQuery(api.messages.list, {}),
);
// no await — rendering is not blocked
},
component: () => {
const { data } = useQuery(convexQuery(api.messages.list, {}));
if (!data) return <p>Loading...</p>;
return (
<div>
{data.map((message) => (
<Message key={message.id} post={message} />
))}
</div>
);
},
})
You can use loaderDeps to pass search parameters into the loader, which causes
the loader to re-run and cache-bust when those parameters change:
export const Route = createFileRoute("/posts")({
validateSearch: (search) => ({ channel: String(search.channel ?? "") }),
loaderDeps: ({ search: { channel } }) => ({ channel }),
loader: async (opts) => {
await opts.context.queryClient.ensureQueryData(
convexQuery(api.messages.list, { channel: opts.deps.channel }),
);
},
});
Authentication
Client-side authentication in Start works the way client-side authentication with Convex generally works in React because TanStack Start works well as a client-side framework.
To make authenticated Convex calls on the server as well see our setup guides: