convex-component-authoring

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Convex Component Authoring

Convex 组件开发

Create self-contained, reusable Convex components with proper isolation, exports, and dependency management for sharing across projects.
创建具备适当隔离性、导出规则和依赖管理的独立可复用Convex组件,以便在多个项目中共享。

Documentation Sources

文档来源

Before implementing, do not assume; fetch the latest documentation:
在实现之前,不要凭经验假设;请获取最新文档:

Instructions

操作指南

What Are Convex Components?

什么是Convex组件?

Convex components are self-contained packages that include:
  • Database tables (isolated from the main app)
  • Functions (queries, mutations, actions)
  • TypeScript types and validators
  • Optional frontend hooks
Convex组件是独立的包,包含以下内容:
  • 数据库表(与主应用隔离)
  • 函数(查询、变更、操作)
  • TypeScript类型和验证器
  • 可选的前端钩子

Component Structure

组件结构

my-convex-component/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│   ├── index.ts           # Main exports
│   ├── component.ts       # Component definition
│   ├── schema.ts          # Component schema
│   └── functions/
│       ├── queries.ts
│       ├── mutations.ts
│       └── actions.ts
└── convex.config.ts       # Component configuration
my-convex-component/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│   ├── index.ts           # 主导出文件
│   ├── component.ts       # 组件定义
│   ├── schema.ts          # 组件 schema
│   └── functions/
│       ├── queries.ts
│       ├── mutations.ts
│       └── actions.ts
└── convex.config.ts       # 组件配置文件

Creating a Component

创建组件

1. Component Configuration

1. 组件配置

typescript
// convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("myComponent");
typescript
// convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("myComponent");

2. Component Schema

2. 组件 Schema

typescript
// src/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  // Tables are isolated to this component
  items: defineTable({
    name: v.string(),
    data: v.any(),
    createdAt: v.number(),
  }).index("by_name", ["name"]),
  
  config: defineTable({
    key: v.string(),
    value: v.any(),
  }).index("by_key", ["key"]),
});
typescript
// src/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  // 表仅属于当前组件
  items: defineTable({
    name: v.string(),
    data: v.any(),
    createdAt: v.number(),
  }).index("by_name", ["name"]),
  
  config: defineTable({
    key: v.string(),
    value: v.any(),
  }).index("by_key", ["key"]),
});

3. Component Definition

3. 组件定义

typescript
// src/component.ts
import { defineComponent, ComponentDefinition } from "convex/server";
import schema from "./schema";
import * as queries from "./functions/queries";
import * as mutations from "./functions/mutations";

const component = defineComponent("myComponent", {
  schema,
  functions: {
    ...queries,
    ...mutations,
  },
});

export default component;
typescript
// src/component.ts
import { defineComponent, ComponentDefinition } from "convex/server";
import schema from "./schema";
import * as queries from "./functions/queries";
import * as mutations from "./functions/mutations";

const component = defineComponent("myComponent", {
  schema,
  functions: {
    ...queries,
    ...mutations,
  },
});

export default component;

4. Component Functions

4. 组件函数

typescript
// src/functions/queries.ts
import { query } from "../_generated/server";
import { v } from "convex/values";

export const list = query({
  args: {
    limit: v.optional(v.number()),
  },
  returns: v.array(v.object({
    _id: v.id("items"),
    name: v.string(),
    data: v.any(),
    createdAt: v.number(),
  })),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .order("desc")
      .take(args.limit ?? 10);
  },
});

export const get = query({
  args: { name: v.string() },
  returns: v.union(v.object({
    _id: v.id("items"),
    name: v.string(),
    data: v.any(),
  }), v.null()),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .withIndex("by_name", (q) => q.eq("name", args.name))
      .unique();
  },
});
typescript
// src/functions/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";

export const create = mutation({
  args: {
    name: v.string(),
    data: v.any(),
  },
  returns: v.id("items"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("items", {
      name: args.name,
      data: args.data,
      createdAt: Date.now(),
    });
  },
});

export const update = mutation({
  args: {
    id: v.id("items"),
    data: v.any(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { data: args.data });
    return null;
  },
});

export const remove = mutation({
  args: { id: v.id("items") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.id);
    return null;
  },
});
typescript
// src/functions/queries.ts
import { query } from "../_generated/server";
import { v } from "convex/values";

export const list = query({
  args: {
    limit: v.optional(v.number()),
  },
  returns: v.array(v.object({
    _id: v.id("items"),
    name: v.string(),
    data: v.any(),
    createdAt: v.number(),
  })),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .order("desc")
      .take(args.limit ?? 10);
  },
});

export const get = query({
  args: { name: v.string() },
  returns: v.union(v.object({
    _id: v.id("items"),
    name: v.string(),
    data: v.any(),
  }), v.null()),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .withIndex("by_name", (q) => q.eq("name", args.name))
      .unique();
  },
});
typescript
// src/functions/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";

export const create = mutation({
  args: {
    name: v.string(),
    data: v.any(),
  },
  returns: v.id("items"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("items", {
      name: args.name,
      data: args.data,
      createdAt: Date.now(),
    });
  },
});

export const update = mutation({
  args: {
    id: v.id("items"),
    data: v.any(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { data: args.data });
    return null;
  },
});

export const remove = mutation({
  args: { id: v.id("items") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.id);
    return null;
  },
});

5. Main Exports

5. 主导出

typescript
// src/index.ts
export { default as component } from "./component";
export * from "./functions/queries";
export * from "./functions/mutations";

// Export types for consumers
export type { Id } from "./_generated/dataModel";
typescript
// src/index.ts
export { default as component } from "./component";
export * from "./functions/queries";
export * from "./functions/mutations";

// 为使用者导出类型
export type { Id } from "./_generated/dataModel";

Using a Component

使用组件

typescript
// In the consuming app's convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "my-convex-component";

const app = defineApp();

app.use(myComponent, { name: "myComponent" });

export default app;
typescript
// In the consuming app's code
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function MyApp() {
  // Access component functions through the app's API
  const items = useQuery(api.myComponent.list, { limit: 10 });
  const createItem = useMutation(api.myComponent.create);
  
  return (
    <div>
      {items?.map((item) => (
        <div key={item._id}>{item.name}</div>
      ))}
      <button onClick={() => createItem({ name: "New", data: {} })}>
        Add Item
      </button>
    </div>
  );
}
typescript
// 在消费应用的 convex/convex.config.ts 中
import { defineApp } from "convex/server";
import myComponent from "my-convex-component";

const app = defineApp();

app.use(myComponent, { name: "myComponent" });

export default app;
typescript
// 在消费应用的代码中
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function MyApp() {
  // 通过应用的 API 访问组件函数
  const items = useQuery(api.myComponent.list, { limit: 10 });
  const createItem = useMutation(api.myComponent.create);
  
  return (
    <div>
      {items?.map((item) => (
        <div key={item._id}>{item.name}</div>
      ))}
      <button onClick={() => createItem({ name: "New", data: {} })}>
        添加条目
      </button>
    </div>
  );
}

Component Configuration Options

组件配置选项

typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "my-convex-component";

const app = defineApp();

// Basic usage
app.use(myComponent);

// With custom name
app.use(myComponent, { name: "customName" });

// Multiple instances
app.use(myComponent, { name: "instance1" });
app.use(myComponent, { name: "instance2" });

export default app;
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "my-convex-component";

const app = defineApp();

// 基础用法
app.use(myComponent);

// 自定义名称
app.use(myComponent, { name: "customName" });

// 多实例
app.use(myComponent, { name: "instance1" });
app.use(myComponent, { name: "instance2" });

export default app;

Providing Component Hooks

提供组件钩子

typescript
// src/hooks.ts
import { useQuery, useMutation } from "convex/react";
import { FunctionReference } from "convex/server";

// Type-safe hooks for component consumers
export function useMyComponent(api: {
  list: FunctionReference<"query">;
  create: FunctionReference<"mutation">;
}) {
  const items = useQuery(api.list, {});
  const createItem = useMutation(api.create);
  
  return {
    items,
    createItem,
    isLoading: items === undefined,
  };
}
typescript
// src/hooks.ts
import { useQuery, useMutation } from "convex/react";
import { FunctionReference } from "convex/server";

// 为组件使用者提供类型安全的钩子
export function useMyComponent(api: {
  list: FunctionReference<"query">;
  create: FunctionReference<"mutation">;
}) {
  const items = useQuery(api.list, {});
  const createItem = useMutation(api.create);
  
  return {
    items,
    createItem,
    isLoading: items === undefined,
  };
}

Publishing a Component

发布组件

package.json

package.json

json
{
  "name": "my-convex-component",
  "version": "1.0.0",
  "description": "A reusable Convex component",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "convex.config.ts"
  ],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "convex": "^1.0.0"
  },
  "devDependencies": {
    "convex": "^1.17.0",
    "typescript": "^5.0.0"
  },
  "keywords": [
    "convex",
    "component"
  ]
}
json
{
  "name": "my-convex-component",
  "version": "1.0.0",
  "description": "一个可复用的 Convex 组件",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "convex.config.ts"
  ],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "convex": "^1.0.0"
  },
  "devDependencies": {
    "convex": "^1.17.0",
    "typescript": "^5.0.0"
  },
  "keywords": [
    "convex",
    "component"
  ]
}

tsconfig.json

tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Examples

示例

Rate Limiter Component

限流组件

typescript
// rate-limiter/src/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  requests: defineTable({
    key: v.string(),
    timestamp: v.number(),
  })
    .index("by_key", ["key"])
    .index("by_key_and_time", ["key", "timestamp"]),
});
typescript
// rate-limiter/src/functions/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";

export const checkLimit = mutation({
  args: {
    key: v.string(),
    limit: v.number(),
    windowMs: v.number(),
  },
  returns: v.object({
    allowed: v.boolean(),
    remaining: v.number(),
    resetAt: v.number(),
  }),
  handler: async (ctx, args) => {
    const now = Date.now();
    const windowStart = now - args.windowMs;
    
    // Clean old entries
    const oldEntries = await ctx.db
      .query("requests")
      .withIndex("by_key_and_time", (q) => 
        q.eq("key", args.key).lt("timestamp", windowStart)
      )
      .collect();
    
    for (const entry of oldEntries) {
      await ctx.db.delete(entry._id);
    }
    
    // Count current window
    const currentRequests = await ctx.db
      .query("requests")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .collect();
    
    const remaining = Math.max(0, args.limit - currentRequests.length);
    const allowed = remaining > 0;
    
    if (allowed) {
      await ctx.db.insert("requests", {
        key: args.key,
        timestamp: now,
      });
    }
    
    const oldestRequest = currentRequests[0];
    const resetAt = oldestRequest 
      ? oldestRequest.timestamp + args.windowMs 
      : now + args.windowMs;
    
    return { allowed, remaining: remaining - (allowed ? 1 : 0), resetAt };
  },
});
typescript
// Usage in consuming app
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function useRateLimitedAction() {
  const checkLimit = useMutation(api.rateLimiter.checkLimit);
  
  return async (action: () => Promise<void>) => {
    const result = await checkLimit({
      key: "user-action",
      limit: 10,
      windowMs: 60000,
    });
    
    if (!result.allowed) {
      throw new Error(`Rate limited. Try again at ${new Date(result.resetAt)}`);
    }
    
    await action();
  };
}
typescript
// rate-limiter/src/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  requests: defineTable({
    key: v.string(),
    timestamp: v.number(),
  })
    .index("by_key", ["key"])
    .index("by_key_and_time", ["key", "timestamp"]),
});
typescript
// rate-limiter/src/functions/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";

export const checkLimit = mutation({
  args: {
    key: v.string(),
    limit: v.number(),
    windowMs: v.number(),
  },
  returns: v.object({
    allowed: v.boolean(),
    remaining: v.number(),
    resetAt: v.number(),
  }),
  handler: async (ctx, args) => {
    const now = Date.now();
    const windowStart = now - args.windowMs;
    
    // 清理旧条目
    const oldEntries = await ctx.db
      .query("requests")
      .withIndex("by_key_and_time", (q) => 
        q.eq("key", args.key).lt("timestamp", windowStart)
      )
      .collect();
    
    for (const entry of oldEntries) {
      await ctx.db.delete(entry._id);
    }
    
    // 统计当前窗口内的请求
    const currentRequests = await ctx.db
      .query("requests")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .collect();
    
    const remaining = Math.max(0, args.limit - currentRequests.length);
    const allowed = remaining > 0;
    
    if (allowed) {
      await ctx.db.insert("requests", {
        key: args.key,
        timestamp: now,
      });
    }
    
    const oldestRequest = currentRequests[0];
    const resetAt = oldestRequest 
      ? oldestRequest.timestamp + args.windowMs 
      : now + args.windowMs;
    
    return { allowed, remaining: remaining - (allowed ? 1 : 0), resetAt };
  },
});
typescript
// 在消费应用中使用
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function useRateLimitedAction() {
  const checkLimit = useMutation(api.rateLimiter.checkLimit);
  
  return async (action: () => Promise<void>) => {
    const result = await checkLimit({
      key: "user-action",
      limit: 10,
      windowMs: 60000,
    });
    
    if (!result.allowed) {
      throw new Error(`已达限流阈值,请于 ${new Date(result.resetAt)} 后重试`);
    }
    
    await action();
  };
}

Best Practices

最佳实践

  • Never run
    npx convex deploy
    unless explicitly instructed
  • Never run any git commands unless explicitly instructed
  • Keep component tables isolated (don't reference main app tables)
  • Export clear TypeScript types for consumers
  • Document all public functions and their arguments
  • Use semantic versioning for component releases
  • Include comprehensive README with examples
  • Test components in isolation before publishing
  • 除非有明确指示,否则不要运行
    npx convex deploy
  • 除非有明确指示,否则不要运行任何git命令
  • 保持组件表的隔离性(不要引用主应用的表)
  • 为使用者导出清晰的TypeScript类型
  • 记录所有公开函数及其参数
  • 组件发布使用语义化版本
  • 包含完整的README和示例
  • 发布前单独测试组件

Common Pitfalls

常见陷阱

  1. Cross-referencing tables - Component tables should be self-contained
  2. Missing type exports - Export all necessary types
  3. Hardcoded configuration - Use component options for customization
  4. No versioning - Follow semantic versioning
  5. Poor documentation - Document all public APIs
  1. 跨表引用 - 组件表应保持独立
  2. 缺失类型导出 - 导出所有必要的类型
  3. 硬编码配置 - 使用组件选项实现自定义
  4. 未做版本控制 - 遵循语义化版本规范
  5. 文档不完善 - 记录所有公开API

References

参考资料