53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/router.tsx:
--------------------------------------------------------------------------------
1 | import { createRouter } from '@tanstack/react-router'
2 | import { routeTree } from './routeTree.gen'
3 | import { ConvexQueryClient } from '@convex-dev/react-query'
4 | import { QueryClient } from '@tanstack/react-query'
5 | import { routerWithQueryClient } from '@tanstack/react-router-with-query'
6 | import { ConvexProvider, ConvexReactClient } from 'convex/react'
7 | import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
8 | import { NotFound } from './components/NotFound'
9 |
10 | export function getRouter() {
11 | let CONVEX_URL = (import.meta as any).env.VITE_CONVEX_URL!
12 | // hardcoded, Tom's dev instance
13 | // this is temporary
14 | const BACKUP_CONVEX_URL = 'https://coordinated-lion-120.convex.cloud'
15 | CONVEX_URL = CONVEX_URL || BACKUP_CONVEX_URL
16 | if (!CONVEX_URL) {
17 | throw new Error('missing VITE_CONVEX_URL envar')
18 | }
19 | const convex = new ConvexReactClient(CONVEX_URL, {
20 | unsavedChangesWarning: false,
21 | })
22 | const convexQueryClient = new ConvexQueryClient(convex)
23 |
24 | const queryClient: QueryClient = new QueryClient({
25 | defaultOptions: {
26 | queries: {
27 | queryKeyHashFn: convexQueryClient.hashFn(),
28 | queryFn: convexQueryClient.queryFn(),
29 | },
30 | },
31 | })
32 | convexQueryClient.connect(queryClient)
33 |
34 | const router = routerWithQueryClient(
35 | createRouter({
36 | routeTree,
37 | defaultPreload: 'intent',
38 | defaultErrorComponent: DefaultCatchBoundary,
39 | defaultNotFoundComponent: () => ,
40 | scrollRestoration: true,
41 | context: { queryClient },
42 | Wrap: ({ children }) => {
43 | return (
44 |
45 | {children}
46 |
47 | )
48 | },
49 | }),
50 | queryClient,
51 | )
52 |
53 | return router
54 | }
55 |
--------------------------------------------------------------------------------
/convex/_generated/dataModel.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated data model types.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import type {
12 | DataModelFromSchemaDefinition,
13 | DocumentByName,
14 | TableNamesInDataModel,
15 | SystemTableNames,
16 | } from "convex/server";
17 | import type { GenericId } from "convex/values";
18 | import schema from "../schema.js";
19 |
20 | /**
21 | * The names of all of your Convex tables.
22 | */
23 | export type TableNames = TableNamesInDataModel;
24 |
25 | /**
26 | * The type of a document stored in Convex.
27 | *
28 | * @typeParam TableName - A string literal type of the table name (like "users").
29 | */
30 | export type Doc = DocumentByName<
31 | DataModel,
32 | TableName
33 | >;
34 |
35 | /**
36 | * An identifier for a document in Convex.
37 | *
38 | * Convex documents are uniquely identified by their `Id`, which is accessible
39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
40 | *
41 | * Documents can be loaded using `db.get(id)` in query and mutation functions.
42 | *
43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other
44 | * strings when type checking.
45 | *
46 | * @typeParam TableName - A string literal type of the table name (like "users").
47 | */
48 | export type Id =
49 | GenericId;
50 |
51 | /**
52 | * A type describing your Convex data model.
53 | *
54 | * This type includes information about what tables you have, the type of
55 | * documents stored in those tables, and the indexes defined on them.
56 | *
57 | * This type is used to parameterize methods like `queryGeneric` and
58 | * `mutationGeneric` to make them type-safe.
59 | */
60 | export type DataModel = DataModelFromSchemaDefinition;
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tanstack-start",
3 | "private": true,
4 | "version": "1.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "npx convex run messages:seed --push && concurrently -r npm:dev:web npm:dev:db",
8 | "dev:web": "vite dev",
9 | "dev:db": "npx convex dev",
10 | "build": "vite build && tsc --noEmit",
11 | "start": "node .output/server/index.mjs",
12 | "lint": "prettier --check '**/*' --ignore-unknown && eslint --ext .ts,.tsx ./app",
13 | "format": "prettier --write '**/*' --ignore-unknown"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "dependencies": {
19 | "@convex-dev/react-query": "^0.0.0-alpha.11",
20 | "@radix-ui/react-icons": "~1.3.0",
21 | "@radix-ui/react-slot": "^1.1.0",
22 | "@tailwindcss/vite": "^4.1.13",
23 | "@tanstack/react-query": "^5.89.0",
24 | "@tanstack/react-query-devtools": "^5.89.0",
25 | "@tanstack/react-router": "^1.132.25",
26 | "@tanstack/react-router-with-query": "^1.130.17",
27 | "@tanstack/react-start": "^1.132.25",
28 | "@tanstack/router-devtools": "^1.132.25",
29 | "@vitejs/plugin-react": "^5.0.3",
30 | "class-variance-authority": "^0.7.0",
31 | "clsx": "1.2.1",
32 | "convex": "^1.27.3",
33 | "nitro": "npm:nitro-nightly",
34 | "prism-react-renderer": "^2.4.0",
35 | "react": "^18.3.1",
36 | "react-dom": "^18.3.1",
37 | "shadcn": "~2.1.2",
38 | "tailwind-merge": "^3.3.1"
39 | },
40 | "devDependencies": {
41 | "@tailwindcss/typography": "^0.5.19",
42 | "@types/react": "^18.2.65",
43 | "@types/react-dom": "^18.2.21",
44 | "@typescript-eslint/parser": "^6.7.4",
45 | "concurrently": "~8.2.2",
46 | "eslint": "^8.29.0",
47 | "eslint-config-react-app": "~7.0.1",
48 | "prettier": "3.2.5",
49 | "tailwindcss": "^4.1.13",
50 | "typescript": "~5.6.2",
51 | "vite": "^7.1.5",
52 | "vite-tsconfig-paths": "^5.1.4"
53 | },
54 | "engines": {
55 | "node": "22.x"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '~/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-slate-300 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-slate-700 text-white shadow-sm hover:bg-slate-800',
13 | destructive: 'bg-red-600 text-white shadow-xs hover:bg-red-500',
14 | outline:
15 | 'border border-slate-200 bg-white shadow-xs hover:bg-slate-100 hover:text-slate-900',
16 | secondary: 'bg-slate-100 text-slate-900 shadow-xs hover:bg-slate-200',
17 | ghost: 'hover:bg-slate-100 hover:text-slate-900',
18 | link: 'hover:underline',
19 | },
20 | size: {
21 | default: 'h-9 px-4 py-2',
22 | sm: 'h-8 rounded-md px-3 text-xs',
23 | lg: 'h-10 rounded-md px-8',
24 | icon: 'h-9 w-9',
25 | },
26 | },
27 | defaultVariants: {
28 | variant: 'default',
29 | size: 'default',
30 | },
31 | },
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {
37 | asChild?: boolean
38 | }
39 |
40 | const Button = React.forwardRef(
41 | ({ className, variant, size, asChild = false, ...props }, ref) => {
42 | const Comp = asChild ? Slot : 'button'
43 | return (
44 |
49 | )
50 | },
51 | )
52 | Button.displayName = 'Button'
53 |
54 | export { Button, buttonVariants }
55 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cn } from '~/lib/utils'
3 |
4 | const Card = React.forwardRef<
5 | HTMLDivElement,
6 | React.HTMLAttributes
7 | >(({ className, ...props }, ref) => (
8 |
13 | ))
14 | Card.displayName = 'Card'
15 |
16 | const CardHeader = React.forwardRef<
17 | HTMLDivElement,
18 | React.HTMLAttributes
19 | >(({ className, ...props }, ref) => (
20 |
25 | ))
26 | CardHeader.displayName = 'CardHeader'
27 |
28 | const CardTitle = React.forwardRef<
29 | HTMLParagraphElement,
30 | React.HTMLAttributes
31 | >(({ className, children, ...props }, ref) => {
32 | if (!children) return null
33 | return (
34 |
39 | {children}
40 |
41 | )
42 | })
43 | CardTitle.displayName = 'CardTitle'
44 |
45 | const CardDescription = React.forwardRef<
46 | HTMLParagraphElement,
47 | React.HTMLAttributes
48 | >(({ className, ...props }, ref) => (
49 |
54 | ))
55 | CardDescription.displayName = 'CardDescription'
56 |
57 | const CardContent = React.forwardRef<
58 | HTMLDivElement,
59 | React.HTMLAttributes
60 | >(({ className, ...props }, ref) => (
61 |
62 | ))
63 | CardContent.displayName = 'CardContent'
64 |
65 | const CardFooter = React.forwardRef<
66 | HTMLDivElement,
67 | React.HTMLAttributes
68 | >(({ className, ...props }, ref) => (
69 |
74 | ))
75 | CardFooter.displayName = 'CardFooter'
76 |
77 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
78 |
--------------------------------------------------------------------------------
/convex/_generated/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import {
12 | actionGeneric,
13 | httpActionGeneric,
14 | queryGeneric,
15 | mutationGeneric,
16 | internalActionGeneric,
17 | internalMutationGeneric,
18 | internalQueryGeneric,
19 | } from "convex/server";
20 |
21 | /**
22 | * Define a query in this Convex app's public API.
23 | *
24 | * This function will be allowed to read your Convex database and will be accessible from the client.
25 | *
26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
28 | */
29 | export const query = queryGeneric;
30 |
31 | /**
32 | * Define a query that is only accessible from other Convex functions (but not from the client).
33 | *
34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
35 | *
36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
38 | */
39 | export const internalQuery = internalQueryGeneric;
40 |
41 | /**
42 | * Define a mutation in this Convex app's public API.
43 | *
44 | * This function will be allowed to modify your Convex database and will be accessible from the client.
45 | *
46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
48 | */
49 | export const mutation = mutationGeneric;
50 |
51 | /**
52 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
53 | *
54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
55 | *
56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
58 | */
59 | export const internalMutation = internalMutationGeneric;
60 |
61 | /**
62 | * Define an action in this Convex app's public API.
63 | *
64 | * An action is a function which can execute any JavaScript code, including non-deterministic
65 | * code and code with side-effects, like calling third-party services.
66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
68 | *
69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
71 | */
72 | export const action = actionGeneric;
73 |
74 | /**
75 | * Define an action that is only accessible from other Convex functions (but not from the client).
76 | *
77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
79 | */
80 | export const internalAction = internalActionGeneric;
81 |
82 | /**
83 | * Define a Convex HTTP action.
84 | *
85 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
86 | * as its second.
87 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
88 | */
89 | export const httpAction = httpActionGeneric;
90 |
--------------------------------------------------------------------------------
/src/routes/ssr.tsx:
--------------------------------------------------------------------------------
1 | import { Link, createFileRoute } from '@tanstack/react-router'
2 | import CodeSample from '~/components/CodeSample'
3 | import Chat from '~/components/Chat'
4 | import { Button } from '~/components/ui/button'
5 | import { convexQuery, useConvexMutation } from '@convex-dev/react-query'
6 | import { api } from 'convex/_generated/api'
7 | import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
8 | import { ReloadIcon } from '@radix-ui/react-icons'
9 |
10 | export const Route = createFileRoute('/ssr')({
11 | component: LiveQueriesSSR,
12 | })
13 |
14 | export default function LiveQueriesSSR() {
15 | const sendTraffic = useConvexMutation(api.messages.simulateTraffic)
16 | const { data: simulationRunning } = useSuspenseQuery({
17 | ...convexQuery(api.messages.isSimulatingTraffic, {}),
18 | gcTime: 2000,
19 | })
20 |
21 | return (
22 | <>
23 |
Server-Side Rendering and Live Queries
24 |
25 | TanStack Start routes render on the server for the first pageload of a
26 | browsing session. Neither the React Query nor standard Convex{' '}
27 | useQuery() hooks kick off requests for this data during
28 | this initial SSR pass, but the React Query hook{' '}
29 | useSuspenseQuery() does. The React Query client is then
30 | serialized with whatever data was loaded to make it available in the
31 | browser at hydration time. This reduces rendering on the server and
32 | updating on the client from{' '}
33 |
34 | two steps
35 | {' '}
36 | to one step: isomorphic data fetching with a single hook.
37 |
28 | TanStack Start routes can have isomorphic loader functions that run
29 | on the server for the initial pageload and on the client for
30 | subsequent client-side navigations.
31 |
32 |
33 | These three links navigate to subpages that show chat messages from
34 | different channels. Notice how client navigations between these
35 | pages work differently.
36 |
37 |
38 |
39 | Awaiting ensureQueryData will block rendering of the
40 | page until that data is available and calling{' '}
41 | prefetchQuery will start the request but not block.
42 | Loaders also run during prefetching, triggered by the cursor mousing
43 | onto a link in TanStack Start by default.
44 |
45 |
46 |
86 |
87 |
When to use a loader
88 |
89 | Loading a query in a loader ejects from the convenience of
90 | component-local reasoning through useSuspenseQuery{' '}
91 | where a component fetching its own data to the cold reality of fast
92 | page loads when you need it.
93 |
132 | {newMessage !== '' && (
133 |
134 | As this is a public demo, your message will be replaced with
135 | system-generated text.
136 |
137 | )}
138 |
32 | TanStack Start apps can use React Query (TanStack Query for React)
33 | hooks to take advantage of React Query's excellent Start
34 | integration. Convex queries are exposed through{' '}
35 |
36 | query options factories
37 | {' '}
38 | like{' '}
39 |
40 | convexQuery()
41 |
42 | .
43 |
44 |
45 | Instead of React Query's standard interval and activity-based
46 | polling and manual invalidation, updates are streamed down WebSocket
47 | to update the Query Cache directly for live, always-up-to-date
48 | results.
49 |
50 |
51 | Open this page in another tab or on your phone and send a message or{' '}
52 | {' '}
62 | to see these updates pushed live.{' '}
63 |
64 |
65 |
66 |
67 |
90 |
91 |
92 |
93 |
Already used Convex React hooks?
94 |
95 | If you've used Convex React hooks React Query hooks are going to
96 | familiar. Instead of returning the data directly, this{' '}
97 | useQuery() returns an object with a .data{' '}
98 | property along with other metadata. The hook accepts a single object,
99 | which you'll mostly populate with the return value of the{' '}
100 | convexQuery() hook.
101 |
102 |
103 | The same React Query hooks can be used for fetch endpoints and Convex
104 | actions for standard interval or activity-based polling while Convex
105 | queries benefit from the live update behavior.
106 |
107 |
108 | What does this give you? It's not just the often-asked-for{' '}
109 | isLoading and error properties or simple
110 | interop with other server endpoints or introspection from the TanStack
111 | Query devtools. It's React Query's Router integration with the TanStack
112 | Start providing things like{' '}
113 | server-side rendering and live updating queries{' '}
114 | in a single hook.
115 |
26 | When the last component subscribed to a given Convex query unmounts
27 | that subscription can be dropped. But it's often useful to keep that
28 | subscription around for while.
29 |
30 |
31 | For non-Convex query function the React Query option{' '}
32 |
33 | gcTime
34 | {' '}
35 | is the length of time to hold on to a stale result before dropping
36 | it out of cache. Convex queries use the same parameter to mean how
37 | long to stay subscribed to the query. This way Convex query results
38 | are always consistent, are never stale.
39 |
40 |
41 | {' '}
59 | to see these query subscriptions stick around as you click between
60 | chat channels. These queries use a gcTime of 10
61 | seconds. If you want subscriptions to be dropped immediately use a{' '}
62 | gcTime of 0.
63 |
64 |
65 |
66 |
67 |
68 |
78 |
88 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | The default gcTime in React Query is five minutes and
109 | this is not currently overridden in the Convex query options
110 | factories like convexQuery(), but this may change in
111 | future versions of the integration. You can always override this
112 | value by spreading the query options into a new options object.
113 |
114 |
115 | Since client-side navigations in TanStack Start preserve the Query
116 | Client, these query subscriptions remain active from previous pages
117 | as well. When debugging why data is loaded or not it's good to keep
118 | this in mind. To get more prescriptive about data being available
119 | ahead of time you might add the query to a{' '}
120 |
121 | loader
122 |
123 | .
124 |
125 |
126 |
127 |
141 |
142 |
143 | >
144 | )
145 | }
146 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { Link, createFileRoute } from '@tanstack/react-router'
2 | import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
3 | import { ExternalLinkIcon } from '@radix-ui/react-icons'
4 |
5 | export const Route = createFileRoute('/')({
6 | component: LandingPage,
7 | })
8 |
9 | export default function LandingPage() {
10 | const tools = [
11 | {
12 | name: 'TanStack Start',
13 | description:
14 | 'A new React framework focused on routing, caching, and type safety.',
15 | href: 'https://tanstack.com/start',
16 | },
17 | {
18 | name: 'Convex',
19 | description:
20 | 'A typesafe database that live-updates and automatically invalidates queries.',
21 | href: 'https://www.convex.dev/',
22 | },
23 | {
24 | name: 'TanStack Query',
25 | description:
26 | 'Asynchronous state management for server-side state like queries and mutations. AKA React Query.',
27 | href: 'https://tanstack.com/query',
28 | },
29 | ]
30 |
31 | const features = [
32 | {
33 | title: 'Using React Query hooks',
34 | description: 'Supercharging React Query with live updates from Convex.',
35 | link: '/react-query',
36 | },
37 | {
38 | title: 'Render on the server, update in the browser',
39 | description:
40 | 'Hydrating the Query Client in the browser makes SSR take no extra steps.',
41 | link: '/ssr',
42 | },
43 | {
44 | title: 'Staying subscribed to queries',
45 | description: "Data not currently rendered doesn't need to be stale.",
46 | link: '/gcTime',
47 | },
48 | {
49 | title: 'Loaders and prefetching',
50 | description:
51 | 'Adding queries to loaders enables prefetching and can prevent waterfalls.',
52 | link: '/loaders',
53 | },
54 | ]
55 |
56 | return (
57 |
66 | TanStack Start is coming. The best way to use Convex with Start is
67 | via React Query's excellent Start integration. This site is{' '}
68 |
69 | written with Start
70 | {' '}
71 | using this setup.
72 |
73 |
74 |
75 | You can jump straight to the quickstart or read more to learn about
76 | using Convex with Start. There's a lot more to TanStack Start so
77 | also check out the{' '}
78 |
79 | official Start guide
80 |
81 | .{' '}
82 |