Frontend Overview
Integrating Convex with React, TanStack, and Cloudflare Agents
Frontend Overview
This template uses React 19, TanStack Start, TanStack Query, Convex, and Cloudflare Agents to build a fully type-safe, reactive agentic chat platform with sandbox capabilities.
Tech Stack
| Library | Purpose |
|---|---|
| React 19 | UI library |
| TanStack Start | SSR framework with file-based routing |
| TanStack Query | Server state management |
| @convex-dev/react-query | Convex + TanStack Query bridge |
| Convex React | Core Convex React bindings |
| @cloudflare/ai-chat/react | Agent WebSocket connection + chat |
| Jotai | Shared in-memory UI state (atoms) |
| xterm.js | Terminal emulation for sandbox PTY |
| motion/react | Animation primitives |
Project Structure
apps/web/src/
├── routes/ # File-based routing
│ ├── __root.tsx # Root layout with providers
│ ├── (public)/ # Unauthenticated routes
│ └── (protected)/ # Wrapped in <AuthBoundary>
├── components/
│ ├── chat/ # Chat interface, message items, input
│ ├── sandboxes/ # Terminal, file explorer, preview
│ ├── todos/ # Task management UI
│ ├── dashboard/ # Dashboard views + stats
│ ├── auth/ # Auth forms + boundaries
│ ├── ai-elements/ # Queue (todos), ask-user prompts
│ └── ui/ # shadcn/ui base components
├── hooks/ # Custom hooks
│ ├── use-chat.ts # Chat actions (submit, edit, regenerate)
│ ├── use-chats.ts # Chat CRUD + pagination
│ ├── use-sandbox.ts # Sandbox UI state + terminal
│ ├── use-sandboxes.ts # Sandbox CRUD + pagination
│ ├── use-attachments.ts # Upload with progress
│ ├── use-openrouter-models.ts # Model fetching + caching
│ └── use-model-filtering.ts # Fuzzy search + favorites
├── providers/
│ └── agent.tsx # Agent connection provider (isolated React root)
├── store/ # Jotai atoms
│ ├── chat-settings.ts # Model selection, system prompt
│ ├── models.ts # Favorite models
│ ├── dashboard.ts # Dashboard UI state
│ └── sandbox.ts # Sandbox panel state
└── lib/
├── convex.ts # Convex client setup
├── auth.ts # Auth client setup
└── motion.ts # Animation presetsSetting Up Providers
Root Layout
// routes/__root.tsx
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConvexQueryClient } from "@convex-dev/react-query";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
const convexQueryClient = new ConvexQueryClient(convex);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);
export function RootLayout({ children }) {
return (
<ConvexProvider client={convex}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ConvexProvider>
);
}Agent Connection
Agent connections are managed via an isolated React root to avoid re-rendering the main tree during streaming. The provider uses a get-or-create pattern to maintain WebSocket connections across route changes.
// providers/agent.tsx handles:
// - WebSocket connection to Cloudflare Workers agent
// - Token-based authentication (SSR/hydration aware)
// - Connection persistence across route navigation
// - AbortController for streaming cancellationCore Concepts
Chat Interface
The primary feature is the AI chat interface. Each chat connects to a Cloudflare Agent via WebSocket and supports:
- Streaming message responses
- Tool approval workflows (write_todos, ask_user)
- Message editing and regeneration
- File attachments (images, documents)
- Sandbox integration (terminal, file explorer, preview)
Sandbox UI
Each chat can have an associated Daytona sandbox with:
- PTY Terminal: xterm.js with FitAddon, multiple sessions, streaming I/O via agent callable methods
- File Explorer: Browse, download files/folders, delete entries
- Preview: Live URL preview for running web apps
- SSH/Editor: Open in VS Code or Cursor via SSH remote
State Management
Jotai atoms manage shared UI state:
chatSettingsAtom-- model selection, system prompt, reasoning effortfavoriteModelsAtom-- persisted favorite model slugsdashboardAtom-- dashboard layout preferencessandboxAtom-- sandbox panel open/closed state
Queries
import { useQuery } from "@tanstack/react-query";
import { convexQuery } from "@convex-dev/react-query";
import { api } from "@just-use-convex/backend/convex/_generated/api";
function ChatView({ chatId }: { chatId: Id<"chats"> }) {
const { data: chat } = useQuery(
convexQuery(api.chats.index.get, { _id: chatId })
);
if (!chat) return <LoadingSkeleton />;
return <ChatInterface chat={chat} />;
}Mutations
import { useMutation } from "@tanstack/react-query";
import { useConvexMutation } from "@convex-dev/react-query";
function CreateChat() {
const createChat = useMutation({
mutationFn: useConvexMutation(api.chats.index.create),
onSuccess: () => toast.success("Chat created"),
});
return <button onClick={() => createChat.mutateAsync({ title: "New Chat" })}>New</button>;
}Type Safety
Types flow from your Convex schema to the frontend via generated API types:
import type { FunctionReturnType } from "convex/server";
import { api } from "@just-use-convex/backend/convex/_generated/api";
// Inferred from the API return type
type Chat = FunctionReturnType<typeof api.chats.index.list>["page"][number];
type ChatWithDetails = FunctionReturnType<typeof api.chats.index.get>;Conditional Queries
function ChatDetails({ chatId }: { chatId: Id<"chats"> | undefined }) {
const { data: chat } = useQuery({
...convexQuery(api.chats.index.get, chatId ? { _id: chatId } : "skip"),
enabled: !!chatId,
});
if (!chatId) return <p>Select a chat</p>;
if (!chat) return <LoadingSkeleton />;
return <ChatView chat={chat} />;
}Real-Time Updates
Convex queries are reactive by default. Chat lists, sandbox states, and stats all update in real-time:
function ChatStats() {
const { data: stats } = useQuery(
convexQuery(api.chats.index.getMemberStats, {})
);
return <span>Total chats: {stats?.total ?? 0}</span>;
}Performance Notes
Heavy focus on preventing re-renders during AI streaming:
- Custom memo comparisons (
areMessageItemPropsEqual) useSyncExternalStorefor shared mutable state- Isolated
useChat/useAgentChathooks in separate React roots - Content-based comparison vs reference equality
- Derive state during render, not in effects
- Functional setState for stable callbacks
Next, learn about Custom Hooks.