Just-Use-Convex Docs
Frontend Integration

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

LibraryPurpose
React 19UI library
TanStack StartSSR framework with file-based routing
TanStack QueryServer state management
@convex-dev/react-queryConvex + TanStack Query bridge
Convex ReactCore Convex React bindings
@cloudflare/ai-chat/reactAgent WebSocket connection + chat
JotaiShared in-memory UI state (atoms)
xterm.jsTerminal emulation for sandbox PTY
motion/reactAnimation 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 presets

Setting 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 cancellation

Core 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 effort
  • favoriteModelsAtom -- persisted favorite model slugs
  • dashboardAtom -- dashboard layout preferences
  • sandboxAtom -- 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)
  • useSyncExternalStore for shared mutable state
  • Isolated useChat/useAgentChat hooks 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.