Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design and build reusable Convex components with isolated tables, backend functions, and app-facing wrappers.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/advanced-patterns.md
1# Advanced Component Patterns23Additional patterns for Convex components that go beyond the basics covered in4the main skill file.56## Function Handles for callbacks78When the app needs to pass a callback function to the component, use function9handles. This is common for components that run app-defined logic on a schedule10or in a workflow.1112```ts13// App side: create a handle and pass it to the component14import { createFunctionHandle } from "convex/server";1516export const startJob = mutation({17handler: async (ctx) => {18const handle = await createFunctionHandle(internal.myModule.processItem);19await ctx.runMutation(components.workpool.enqueue, {20callback: handle,21});22},23});24```2526```ts27// Component side: accept and invoke the handle28import { v } from "convex/values";29import type { FunctionHandle } from "convex/server";30import { mutation } from "./_generated/server.js";3132export const enqueue = mutation({33args: { callback: v.string() },34handler: async (ctx, args) => {35const handle = args.callback as FunctionHandle<"mutation">;36await ctx.scheduler.runAfter(0, handle, {});37},38});39```4041## Deriving validators from schema4243Instead of manually repeating field types in return validators, extend the44schema validator:4546```ts47import { v } from "convex/values";48import schema from "./schema.js";4950const notificationDoc = schema.tables.notifications.validator.extend({51_id: v.id("notifications"),52_creationTime: v.number(),53});5455export const getLatest = query({56args: {},57returns: v.nullable(notificationDoc),58handler: async (ctx) => {59return await ctx.db.query("notifications").order("desc").first();60},61});62```6364## Static configuration with a globals table6566A common pattern for component configuration is a single-document "globals"67table:6869```ts70// schema.ts71export default defineSchema({72globals: defineTable({73maxRetries: v.number(),74webhookUrl: v.optional(v.string()),75}),76// ... other tables77});78```7980```ts81// lib.ts82export const configure = mutation({83args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) },84returns: v.null(),85handler: async (ctx, args) => {86const existing = await ctx.db.query("globals").first();87if (existing) {88await ctx.db.patch(existing._id, args);89} else {90await ctx.db.insert("globals", args);91}92return null;93},94});95```9697## Class-based client wrappers9899For components with many functions or configuration options, a class-based100client provides a cleaner API. This pattern is common in published components.101102```ts103// src/client/index.ts104import type { GenericMutationCtx, GenericDataModel } from "convex/server";105import type { ComponentApi } from "../component/_generated/component.js";106107type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;108109export class Notifications {110constructor(111private component: ComponentApi,112private options?: { defaultChannel?: string },113) {}114115async send(ctx: MutationCtx, args: { userId: string; message: string }) {116return await ctx.runMutation(this.component.lib.send, {117...args,118channel: this.options?.defaultChannel ?? "default",119});120}121}122```123124```ts125// App usage126import { Notifications } from "@convex-dev/notifications";127import { components } from "./_generated/api";128129const notifications = new Notifications(components.notifications, {130defaultChannel: "alerts",131});132133export const send = mutation({134args: { message: v.string() },135handler: async (ctx, args) => {136const userId = await getAuthUserId(ctx);137await notifications.send(ctx, { userId, message: args.message });138},139});140```141