7. ワークスペース作成
テーブルを作成し、任意のワークスペースを登録できるようにする。
テーブル追加
workspaceテーブルを追加する。
スキーマの指定
convex/schema.ts
const schema = defineSchema({
...authTables,
// Your other tables...
workspaces: defineTable({
name: v.string(),
userId: v.id("users"),
joinCode: v.string(),
}),
});
queryとmutationの追加
作成・取得ができるようにFunctionを追加する。
Query
Queries | Convex Developer Hub
Queries are the bread and butter of your backend API. They fetch data from the

Mutation
Mutations | Convex Developer Hub
Mutations insert, update and remove data from the database, check authentication

- args: パラメータとして受け取りたい値を指定する
- handler: 登録処理を記載
convex/workspaces.ts
import { getAuthUserId } from "@convex-dev/auth/server";
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
export const create = mutation({
args: {
name: v.string(),
},
handler: async (ctx, args) => {
// const userId = await auth.getUserId(ctx); 非推奨
const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Unauthorized");
}
const joinCode = "123456";
const workspaceId = await ctx.db.insert("workspaces", {
name: args.name,
userId,
joinCode,
});
// const workspace = await ctx.db.get(workspaceId);
return workspaceId;
},
});
export const get = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("workspaces").collect();
},
});
shadcn/ui関連
トースターを使うのでインストール
Toast
A succinct message that is displayed temporarily.

モーダル作成
jotaiの導入
useStateの感覚で使える状態管理ライブラリ。
モーダルの表示非表示を制御する。
Jotai, primitive and flexible state management for React
Jotai takes a bottom-up approach to global React state management with an atomic model inspired by Recoil. One can build state by combining atoms and renders are optimized based on atom dependency. This solves the extra re-render issue of React context and eliminates the need for memoization.

terminal
bun add jotai
src/features/workspaces/store/use-create-workspace-modal.ts
import { atom, useAtom } from "jotai";
const modalState = atom(false);
export const useCreateWorkspaceModal = () => {
return useAtom(modalState);
};
各種モーダルを呼び出すコンポーネントを作成
モーダルなどの共通コンポーネントはどこからでも呼び出すので、共通化して一括で読み込ませる。
src/components/modals.tsx
"use client";
import { CreateWorkspaceModal } from "@/features/workspaces/components/create-workspace-modal";
import { useEffect, useState } from "react";
export const Modals = () => {
const [mounted, setMounted] = useState(false);
//ハイドレーションエラーを回避するために、マウントされたときにモーダルを表示する
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
<CreateWorkspaceModal />
</>
);
};
ワークスペース作成用API
convexに用意したワークスペース用のmutationを呼び出す。
呼び出し方のサンプルは以下参照。 useMutation
で対象の関数を指定する。 ここではconvex/workspaces.ts
でexportしている create
関数。
Mutations | Convex Developer Hub
Mutations insert, update and remove data from the database, check authentication

useMutation
はmutationを呼び出すためのフックuseCallback
は関数をメモ化するためのフックvalues
: ワークスペース名options
: 成功時、エラー時、終了時のコールバック関数を指定
メモ化することで、再レンダリング時に再生成されるのを防ぐ。が、この辺の勘所はイマイチ。
src/features/workspaces/api/use-create.workspaces.ts
import { useMutation } from "convex/react";
import { useCallback, useMemo, useState } from "react";
import { api } from "../../../../convex/_generated/api";
import { Id } from "../../../../convex/_generated/dataModel";
type RequestType = { name: string };
type ResponseType = Id<"workspaces"> | null; //ワークスペースのIDを返す
// type ResponseType = Doc<"workspaces">; //ワークスペース自体?を返す
type Options = {
onSuccess?: (data: ResponseType) => void;
onError?: (error: Error) => void;
onSettled?: () => void;
throwError?: boolean;
};
export const useCreateWorkspace = () => {
const [data, setData] = useState<ResponseType>(null);
const [error, setError] = useState<Error | null>(null);
const [status, setStatus] = useState<
"success" | "error" | "pending" | "settled" | null
>(null);
// const [isPending, setIsPending] = useState(false);
// const [isSuccess, setIsSuccess] = useState(false);
// const [isError, setIsError] = useState(false);
// const [isSettled, setIsSettled] = useState(false);
const isPending = useMemo(() => status === "pending", [status]);
const isSuccess = useMemo(() => status === "success", [status]);
const isError = useMemo(() => status === "error", [status]);
const isSettled = useMemo(() => status === "settled", [status]);
const mutation = useMutation(api.workspaces.create);
const mutate = useCallback(
async (values: RequestType, options?: Options) => {
try {
setData(null);
setError(null);
// setIsSuccess(false);
// setIsError(false);
// setIsSettled(false);
// setIsPending(true);
setStatus("pending");
const response = await mutation(values);
options?.onSuccess?.(response);
return response;
} catch (error) {
options?.onError?.(error as Error);
if (options?.throwError) {
throw error;
}
} finally {
setStatus("settled");
options?.onSettled?.();
}
},
[mutation]
);
return { mutate, data, error, isPending, isSuccess, isError, isSettled };
};
ワークスペース作成モーダル
src/features/workspaces/components/create-workspace-modal.tsx
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { useCreateWorkspace } from "../api/use-create.workspaces";
import { useCreateWorkspaceModal } from "../store/use-create-workspace-modal";
export const CreateWorkspaceModal = () => {
const [open, setOpen] = useCreateWorkspaceModal();
const [name, setName] = useState("");
const router = useRouter();
const { mutate, isPending } = useCreateWorkspace();
const handleClose = () => {
setOpen(false);
setName("");
};
const handleSunmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutate(
{ name },
{
onSuccess(id) {
toast.success("Workspace created");
router.push(`/workspace/${id}`);
handleClose();
},
}
);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a workspace</DialogTitle>
</DialogHeader>
<form onSubmit={handleSunmit} className="space-y-4">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isPending}
required
autoFocus
placeholder="Workspace name e.g. 'Work', 'Personal', 'Home'"
/>
<div className="flex justify-end">
<Button disabled={isPending}>Create</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};