Just-Use-Convex Docs
Frontend Integration

Custom Hooks

Reusable hooks for chat, sandbox, attachments, and model selection

Custom Hooks

Custom hooks encapsulate data fetching, mutations, and complex UI logic. The app provides hooks for chat interactions, sandbox management, file uploads, and AI model selection.

Chat Hooks

useChat (Agent Actions)

Wraps an AgentChatInstance from @cloudflare/ai-chat/react. Provides submit, edit, regenerate, tool approval, and message extraction utilities.

// hooks/use-chat.ts
import type { useAgentChat } from "@cloudflare/ai-chat/react";

type AgentChatInstance = ReturnType<typeof useAgentChat>;

export function useChat(chat: AgentChatInstance | null, agent: AgentConnection) {
  // Returns:
  return {
    status,         // "ready" | "streaming" | ...
    error,          // Error | undefined
    stop,           // () => void — stop streaming
    messages,       // UIMessage[]
    isStreaming,    // boolean
    handleSubmit,   // ({ text, files }) => Promise<void>
    handleToolApprovalResponse,  // approve/reject tool calls
    handleRegenerate,            // (messageId) => regenerate from that point
    handleEditMessage,           // (messageId, newText, files) => edit + re-send
  };
}

Helper exports for extracting structured data from messages:

// Extract text content from a UIMessage
extractMessageText(message: UIMessage): string

// Extract file parts from a UIMessage
extractMessageFiles(message: UIMessage): FileUIPart[]

// Extract write_todos tool state from the last assistant message
extractTodosFromMessage(message, isLastAssistantMessage): TodosState | null

// Extract ask_user tool state from the last assistant message
extractAskUserFromMessage(message, isLastAssistantMessage): AskUserState | null

useMessageEditing

Full editing flow for user messages, including file add/remove and keyboard shortcuts.

export function useMessageEditing(message, onEditMessage?) {
  return {
    isEditing,
    editedText, setEditedText,
    editedFiles,
    textareaRef, fileInputRef,
    hasChanges,
    handleStartEdit,
    handleCancelEdit,
    handleRemoveFile,
    handleAddFiles,
    handleConfirmEdit,    // Ctrl+Enter to confirm
    handleKeyDown,        // Escape to cancel
  };
}

useCopyToClipboard

export function useCopyToClipboard(text: string) {
  return { copied, handleCopy };  // copied resets after 2s
}

useChats / useChatsList

Chat CRUD mutations and paginated listing via useConvexMutation and useConvexPaginatedQuery.

// hooks/use-chats.ts
export function useChats() {
  return {
    createChat,   // mutateAsync — create a new chat
    updateChat,   // mutateAsync — update chat fields
    deleteChat,   // mutateAsync — delete a chat
    updateTitle,  // (id, title) => update just the title
    togglePin,    // (id, isPinned) => pin/unpin
    isCreating, isUpdating, isDeleting,
  };
}

export function useChatsList(filters?: ChatFilters) {
  // Returns useConvexPaginatedQuery result with cursor-based pagination
  // initialNumItems: 20
}

// Single chat query
export function useChat(chatId: Id<"chats"> | undefined) {
  // Conditional convexQuery, skips when chatId is undefined
}

// Chat stats for the current member
export function useChatStats() {
  // Returns getMemberStats query
}

Exported types derived from the API:

export type Chat = FunctionReturnType<typeof api.chats.index.list>["page"][number];
export type ChatWithDetails = FunctionReturnType<typeof api.chats.index.get>;
export type ChatStats = FunctionReturnType<typeof api.chats.index.getMemberStats>;
export type ChatFilters = FunctionArgs<typeof api.chats.index.list>["filters"];

Sandbox Hooks

useChatSandbox

Full sandbox UI state for a chat: SSH sessions, PTY terminal (xterm.js + FitAddon), file explorer, preview URLs. All terminal I/O streams through the agent's callable methods.

// hooks/use-sandbox.ts
export function useChatSandbox(chatId: Id<"chats">, agent: AgentConnection) {
  return {
    // Panel state
    isOpen, open, close, toggle,

    // SSH
    sshSession,            // { ssh: { token, host, expiresAt } } | null
    reconnectSsh,
    openInEditor,          // (editor: "vscode" | "cursor") => open remote SSH

    // Terminal (PTY via agent proxy)
    terminalContainerRef,  // ref for xterm.js mount
    terminalBackground,    // CSS color
    terminalSessions,      // TerminalSession[] — active sessions
    activeTerminalId,
    switchTerminalSession,
    createTerminalSession,
    closeTerminalSession,
    reconnectTerminal,
    refreshTerminalSessions,
    focusTerminal,

    // File explorer
    explorer,              // { path, entries: ExplorerEntry[] } | null
    refreshExplorer,
    navigateExplorer,
    downloadFile,
    downloadFolder,
    deleteEntry,

    // Preview
    previewPort, previewUrl,
    setPreviewPort,
    createPreviewAccess,

    // Loading states
    isConnectingSsh,
    isConnectingPreview,
  };
}

The terminal setup dynamically imports xterm.js and @xterm/addon-fit, opens a PTY session via agent.call("openPtyTerminal"), then streams output via agent.call("streamPtyTerminal") with onChunk. Input is batched at 25ms intervals and written via agent.call("writePtyTerminal"). A ResizeObserver keeps the terminal fitted and syncs dimensions to the remote PTY.

Exported types:

export type ExplorerEntry = { name, path, isDir, size, modifiedAt };
export type ExplorerState = { path, entries: ExplorerEntry[] };
export type TerminalSession = { id, active?, processId?, cwd?, cols?, rows?, createdAt? };

useSandboxes / useSandboxesList

Sandbox CRUD mutations and paginated listing, same pattern as useChats.

// hooks/use-sandboxes.ts
export function useSandboxes() {
  return {
    createSandbox,  // mutateAsync
    updateSandbox,  // mutateAsync
    deleteSandbox,  // mutateAsync
    updateName,     // (id, name) shortcut
    isCreating, isUpdating, isDeleting,
  };
}

export function useSandboxesList(filters?: SandboxFilters) {
  // Paginated query, initialNumItems: 20
}

export function useSandbox(sandboxId: Id<"sandboxes"> | undefined) {
  // Conditional single-sandbox query
}

export function useSandboxStats() {
  // Returns getUserStats query
}

Attachment Hooks

useAttachments

Upload flow with XHR progress tracking, SHA-256 hashing for dedup, and Convex storage integration.

// hooks/use-attachments.ts
export function useAttachments() {
  return {
    uploadAttachment,   // (args: { fileBytes, fileName, contentType, onProgress?, signal? }) => Promise
    deleteAttachment,   // mutateAsync
    isUploading,        // true during any phase (URL gen, upload, hash creation)
    isDeleting,
  };
}

export function useAttachmentsList(filters?: AttachmentFilters) {
  // Paginated query
}

The upload pipeline:

  1. generateUploadUrl -- get a signed Convex upload URL
  2. uploadBlobWithProgress -- XHR upload with onProgress callback (0-100%)
  3. toHexHash -- SHA-256 hash via crypto.subtle.digest
  4. createFromHash -- dedup check + create attachment record

Supports AbortSignal for cancellation.

Model Selection Hooks

useOpenRouterModels

Fetches models from /api/models, caches in localStorage for 1 hour, groups by author. Filters out free-tier and models older than 1 year.

// hooks/use-openrouter-models.ts
export function useOpenRouterModels() {
  return {
    models,          // OpenRouterModel[] — all models
    groupedModels,   // [author, OpenRouterModel[]][] — grouped by author
    reasoningModels, // OpenRouterModel[] — models with reasoning support
    isLoading,
    error,
  };
}

export type OpenRouterModel = {
  slug: string;          // e.g. "openai/gpt-5.2-chat"
  name: string;
  short_name: string;
  author: string;
  description: string;
  context_length: number;
  supports_reasoning: boolean;
  created_at: string;
  input_modalities?: string[];
  output_modalities?: string[];
  reasoning_config?: { ... };
};

useModelFiltering

Fuzzy search across models, provider grouping with logo mapping, favorites via Jotai favoriteModelsAtom.

// hooks/use-model-filtering.ts
export function useModelFiltering({ groupedModels, models }) {
  return {
    selectedAuthor,     // "all" | "favorites" | provider display name
    setSelectedAuthor,
    authors,            // Available filter options
    filteredModels,     // [author, OpenRouterModel[]][] — filtered result
    toggleFavorite,     // (slug) => add/remove from favorites
    isFavorite,         // (slug) => boolean
    favorites,          // string[] — favorite model slugs
  };
}

// Utility exports
getProviderLabel(author: string): string | null     // Match author to known provider
getProviderDisplayName(label: string): string        // Display name for filter tab
getProviderLogoSlug(displayLabel: string): string    // Logo slug for provider icon

Supported providers: Anthropic, OpenAI, Google, xAI, MiniMax, DeepSeek, Moonshot AI, Z-AI, Black Forest Labs.

Auth Hooks

Current User

export function useCurrentUser() {
  return {
    user,               // User object or null
    isLoading,
    isAuthenticated,
    organizationId,     // Active org from session
    teamId,
    role,               // "owner" | "admin" | "member"
  };
}

Permissions

export function usePermissions() {
  return {
    isOwner, isAdmin, isMember,
    canManageMembers,
    canDeleteOrg,
    canEditSettings,
    canEditResource,    // (resourceUserId) => boolean
  };
}

Hook Patterns

Mutation Pattern

All mutation hooks follow the same structure using useConvexMutation + useMutation:

const mutation = useMutation({
  mutationFn: useConvexMutation(api.resource.index.action),
  onSuccess: () => toast.success("Done"),
  onError: (error: Error) => toast.error(error.message || "Failed"),
});

Paginated Query Pattern

All list hooks use useConvexPaginatedQuery with cursor-based pagination:

export function useResourceList(filters = {}) {
  return useConvexPaginatedQuery(
    api.resource.index.list,
    { filters },
    { initialNumItems: 20 }
  );
}

Conditional Query Pattern

Single-resource queries skip when the ID is undefined:

export function useResource(id: Id<"table"> | undefined) {
  return useQuery({
    ...convexQuery(api.resource.index.get, id ? { _id: id } : "skip"),
    enabled: !!id,
  });
}

Next, learn about Optimistic Updates.