Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 0品質スコア 60/100

convex-component-authoring

Convexコンポーネントを自己完結型として作成、構成、公開する方法について説明します。適切な分離、エクスポート、依存関係管理を実装することで、再利用可能で保守性の高いコンポーネントを開発できます。このスキルを習得すると、複数のプロジェクト間でコンポーネントを共有し、チーム内での開発効率を向上させることが可能になります。

description の原文を見る

How to create, structure, and publish self-contained Convex components with proper isolation, exports, and dependency management

SKILL.md 本文

Convex コンポーネント オーサリング

プロジェクト間での共有に向けた、適切な分離、エクスポート、および依存関係管理を備えた自己完結型の再利用可能な Convex コンポーネントを作成します。

ドキュメント ソース

実装する前に、仮定せず最新のドキュメントを取得してください:

説明

Convex コンポーネントとは

Convex コンポーネントは、以下を含む自己完結型パッケージです:

  • データベース テーブル(メイン アプリから分離)
  • 関数(クエリ、ミューテーション、アクション)
  • TypeScript 型およびバリデーター
  • オプションのフロントエンド フック

コンポーネント構造

my-convex-component/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│   ├── index.ts           # メイン エクスポート
│   ├── component.ts       # コンポーネント定義
│   ├── schema.ts          # コンポーネント スキーマ
│   └── functions/
│       ├── queries.ts
│       ├── mutations.ts
│       └── actions.ts
└── convex.config.ts       # コンポーネント設定

コンポーネントの作成

1. コンポーネント設定

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

export default defineComponent("myComponent");

2. コンポーネント スキーマ

// 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. コンポーネント定義

// 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. コンポーネント関数

// 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();
  },
});
// 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. メイン エクスポート

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

// コンシューマー用に型をエクスポート
export type { Id } from "./_generated/dataModel";

コンポーネントの使用

// コンシューマー アプリの 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;
// コンシューマー アプリのコード内
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>
  );
}

コンポーネント設定オプション

// 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;

コンポーネント フックの提供

// 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,
  };
}

コンポーネントの公開

package.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

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

レート リミッター コンポーネント

// 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"]),
});
// 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 };
  },
});
// コンシューマー アプリでの使用
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();
  };
}

ベストプラクティス

  • 明示的に指示されない限り、npx convex deploy を実行しないでください
  • 明示的に指示されない限り、git コマンドを実行しないでください
  • コンポーネント テーブルを分離した状態に保つ(メイン アプリ テーブルを参照しない)
  • コンシューマー向けの明確な TypeScript 型をエクスポート
  • すべてのパブリック関数とその引数をドキュメント化
  • コンポーネント リリースにはセマンティック バージョニングを使用
  • 例を含む包括的な README を含める
  • 公開前にコンポーネントを分離して テストする

よくある落とし穴

  1. テーブル間の相互参照 - コンポーネント テーブルは自己完結型である必要があります
  2. 型のエクスポート漏れ - すべての必要な型をエクスポート
  3. ハードコードされた設定 - コンポーネント オプションを使用してカスタマイズ
  4. バージョニング なし - セマンティック バージョニングに従う
  5. ドキュメント不足 - すべてのパブリック API をドキュメント化

参照

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
aurokin
リポジトリ
aurokin/agentchat
ライセンス
MIT
最終更新
2026/5/11

Source: https://github.com/aurokin/agentchat / ライセンス: MIT

本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: aurokin · aurokin/agentchat · ライセンス: MIT