# Advanced Component Patterns Additional patterns for Convex components that go beyond the basics covered in the main skill file. ## Function Handles for callbacks When the app needs to pass a callback function to the component, use function handles. This is common for components that run app-defined logic on a schedule or in a workflow. ```ts // App side: create a handle and pass it to the component import { createFunctionHandle } from "convex/server"; export const startJob = mutation({ handler: async (ctx) => { const handle = await createFunctionHandle(internal.myModule.processItem); await ctx.runMutation(components.workpool.enqueue, { callback: handle, }); }, }); ``` ```ts // Component side: accept and invoke the handle import { v } from "convex/values"; import type { FunctionHandle } from "convex/server"; import { mutation } from "./_generated/server.js"; export const enqueue = mutation({ args: { callback: v.string() }, handler: async (ctx, args) => { const handle = args.callback as FunctionHandle<"mutation">; await ctx.scheduler.runAfter(0, handle, {}); }, }); ``` ## Deriving validators from schema Instead of manually repeating field types in return validators, extend the schema validator: ```ts import { v } from "convex/values"; import schema from "./schema.js"; const notificationDoc = schema.tables.notifications.validator.extend({ _id: v.id("notifications"), _creationTime: v.number(), }); export const getLatest = query({ args: {}, returns: v.nullable(notificationDoc), handler: async (ctx) => { return await ctx.db.query("notifications").order("desc").first(); }, }); ``` ## Static configuration with a globals table A common pattern for component configuration is a single-document "globals" table: ```ts // schema.ts export default defineSchema({ globals: defineTable({ maxRetries: v.number(), webhookUrl: v.optional(v.string()), }), // ... other tables }); ``` ```ts // lib.ts export const configure = mutation({ args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) }, returns: v.null(), handler: async (ctx, args) => { const existing = await ctx.db.query("globals").first(); if (existing) { await ctx.db.patch(existing._id, args); } else { await ctx.db.insert("globals", args); } return null; }, }); ``` ## Class-based client wrappers For components with many functions or configuration options, a class-based client provides a cleaner API. This pattern is common in published components. ```ts // src/client/index.ts import type { GenericMutationCtx, GenericDataModel } from "convex/server"; import type { ComponentApi } from "../component/_generated/component.js"; type MutationCtx = Pick, "runMutation">; export class Notifications { constructor( private component: ComponentApi, private options?: { defaultChannel?: string }, ) {} async send(ctx: MutationCtx, args: { userId: string; message: string }) { return await ctx.runMutation(this.component.lib.send, { ...args, channel: this.options?.defaultChannel ?? "default", }); } } ``` ```ts // App usage import { Notifications } from "@convex-dev/notifications"; import { components } from "./_generated/api"; const notifications = new Notifications(components.notifications, { defaultChannel: "alerts", }); export const send = mutation({ args: { message: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); await notifications.send(ctx, { userId, message: args.message }); }, }); ```