Skip to content

Slackクローン 7. ワークスペース

公開日

表紙

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
Queries | Convex Developer Hub favicon https://docs.convex.dev/functions/query-functions
Queries | Convex Developer Hub

Mutation

Mutations | Convex Developer Hub
Mutations insert, update and remove data from the database, check authentication
Mutations | Convex Developer Hub favicon https://docs.convex.dev/functions/mutation-functions
Mutations | Convex Developer Hub
  • 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.
Toast favicon https://ui.shadcn.com/docs/components/toast
Toast

モーダル作成

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.
Jotai, primitive and flexible state management for React favicon https://jotai.org/
Jotai, primitive and flexible state management for React
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
Mutations | Convex Developer Hub favicon https://docs.convex.dev/functions/mutation-functions#calling-mutations-from-clients
Mutations | Convex Developer Hub
  • 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 };
};
 

ワークスペース作成モーダル

workspace-component

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>
  );
};