CRUD操作はすべてのアプリケーションの基盤であり、新しい技術を学ぶ際にはそれに熟练することが基本です。

このチュートリアルで、ReactとConvexを使用してCRUDアプリケーションを構築する方法を学びます。ここでは、本のコレクションというプロジェクトを作成し、ユーザーが本を読み終えると、本のステータスを更新できるように追加します。

目次

Convexとは何か?

ConvexはBaasプラットフォームであり、バックエンド開発を簡素化します。Convexにはリアルタイム数据库が含まれており、データベースのクエリや変更を行うための方法が提供されているため、別途サーバー側のロジックの書き込みは不要です。

前提条件

このチュートリアルに従うためには、Reactの基本を知る必要があります。このプロジェクトではTypeScriptを使用しますが、選択的なものであり、JavaScriptだけでも従っていただけます。

プロジェクトの設定方法

プロジェクト用の別のフォルダを作成し、好みの名前にしてください。私はBooksと名付けます。そのフォルダにConvexとReactを設定します。

このようなコマンドを使用してReactアプリを作成することができます。

npm create vite@latest my-app -- --template react-ts

JavaScriptだけを使用している場合は、最後のtsを取り除くだけです。すなわち:

npm create vite@latest my-app -- --template react

Convexの設定方法

Convexを同一フォルダにインストールする必要があります。そのためのコマンドはこうです。

npm install convex

次に、npx convex devを実行します。初めてこの操作を行う場合は、認証を求めるはずです。そうでない場合は、プロジェクト名を求めるはずです。

あなたは作成したプロジェクトを見ることができます。

ConvexとReactアプリを設定した後、ConvexのバックエンドをReactアプリに接続する必要があります。

src/main.tsxで、AppコンポーネントをConvexReactClientで包んでみてください。

import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import "./index.css"

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

createRoot(document.getElementById("root")!).render(
  <ConvexProvider client={convex}>
    <App />
  </ConvexProvider>
);

Convexを設定したとき、.env.localが作成されました。このファイルでバックエンドのURLを見ることができます。

次の行で、URLを使用してReact Convexクライアントをインスタンス化します。

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

スキーマの作成方法

主要なプロジェクトディレクトリにconvexディレクトリがあるはずです。データベースのクエリと変数をここで処理します。

schema.tsファイルをconvexフォルダーに作成します。

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  books: defineTable({
    title: v.string(),
    author: v.string(),
    isCompleted: v.boolean(),
  }),
});

ドキュメントのスキーマをdefineSchemaで定義し、defineTableでテーブルを作成することができます。Convexはスキーマの定義とテーブルの作成に必要な機能を提供しています。

vは型検証器で、テーブルに追加する各データに型を提供するために使用されます。

このプロジェクトは本のコレクションアプリであるため、構造はtitleauthorisCompletedがあります。追加のフィールドを追加することもできます。

スキーマを定義したら、Reactで基本のUIを設定してみましょう。

UIの作成方法

srcフォルダー内に、componentという名前のフォルダーとHome.tsxという名前のファイルを作成します。ここでUIを定義することができます。

import { useState } from "react";
import "../styles/home.css";

const Home = () => {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  return (
    <div className="main-container">
      <h1>Book Collections</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="book title"
        />
        <br />
        <input
          type="text"
          name="author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="book author"
        />
        <br />
        <input type="submit" />
      </form>
      {books ? <Books books={books} /> : "Loading..."}
    </div>
  );
};

export default Home;

あなたは自由にコンポーネントを作成することができます。私はtitleauthorの2つの入力フィールドとsubmit按钮を追加しました。これは基本的な構造です。今後、バックエンドでCRUDメソッドを作成することができます。

CRUD関数の作成方法

convexフォルダー内で、CRUD関数用のqueries.tsファイルを別に作成することができます。

関数の作成

convex/queries.ts中:

你可以定义一个名为createBooks的函数。您可以使用Convex的mutation函数来创建、更新和删除数据。数据的读取将属于query

mutation函数期待这些参数:

  • agrs: 我们需要存储在数据库中的数据。

  • handler: 处理将日期存储在数据库中的逻辑。handler是一个异步函数,它有两个参数: ctxargs。这里,ctx是我们要用来处理数据库操作的上下文对象。

新しいデータを挿入する際には、`insert`メソッドを使用します。`insert`の最初の引数はテーブル名で、第二の引数は插入する必要があるデータです。

最後に、データベースからデータを返すことができます。

以下はコードです。

import { mutation} from "./_generated/server";
import { v } from "convex/values";

export const createBooks = mutation({
  args: { title: v.string(), author: v.string() },
  handler: async (ctx, args) => {
    const newBookId = await ctx.db.insert("books", {
      title: args.title,
      author: args.author,
      isCompleted: false,
    });
    return newBookId;
  },
});

読み取り関数

在`convex/queries.ts`中:

import { query } from "./_generated/server";
import { v } from "convex/values";

//read
export const getBooks = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("books").collect();
  },
});

この読み取り操作で、Convexの内蔵`query`関数を使用しました。ここで、`args`は空であることを想定しており、用户からデータを取得していません。同様に、`handler`関数は异步であり、`ctx`オブジェクトを使用してデータベースをクエリーし、データを返すことができます。

更新関数

在`convex/queries.ts`中:`updateStatus`関数を作成します。ここでは、`isCompleted`ステータスのみを更新します。

ここで、ドキュメントIDとステータスを用户から取得する必要があります。`args`には、用户から来る`id`と`isCompleted`を定義します。

`handler`で、データを更新する`patch`メソッドを使用します。`patch`メソッドには2つの引数を期待します:最初の引数はドキュメントの`id`で、第二の引数は更新後のデータです。

import { mutation } from "./_generated/server";
import { v } from "convex/values";

//update
export const updateStatus = mutation({
  args: { id: v.id("books"), isCompleted: v.boolean() },
  handler: async (ctx, args) => {
    const { id } = args;
    await ctx.db.patch(id, { isCompleted: args.isCompleted });
    return "updated"
  },
});

削除関数

在`convex/queries.ts`中:

deleteBooks関数を作成し、mutation関数を使用します。削除するドキュメントのIDが必要です。argsでIDを定義し、handlerctxオブジェクトのdeleteメソッドを使用し、IDを渡します。これにより、ドキュメントを削除します。

import { mutation } from "./_generated/server";
import { v } from "convex/values";

//delete
export const deleteBooks = mutation({
  args: { id: v.id("books") },
  handler: async (ctx, args) => {
    await ctx.db.delete(args.id);
    return "deleted";
  },
});

今まで、バックエンドのCRUD機能を完了しました。次に、UIにも適用する必要があります。Reactに飛び戻りましょう。

UIの更新

Reactアプリで既に基本的なUIといくつかの入力フィールドを作成しました。これらを更新しましょう。

src/component/Home.tsx にて:

import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Books } from "./Books";
import { useState } from "react";
import "../styles/home.css";

const Home = () => {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  const books = useQuery(api.queries.getBooks);
  const createBooks = useMutation(api.queries.createBooks);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    createBooks({ title, author })
      .then(() => {
        console.log("created");
        setTitle("");
        setAuthor("");
      })
      .catch((err) => console.log(err));
  };
  return (
    <div className="main-container">
      <h1>Book Collections</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="book title"
        />
        <br />
        <input
          type="text"
          name="author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="book author"
        />
        <br />
        <input type="submit" />
      </form>
      {books ? <Books books={books} /> : "Loading..."}
    </div>
  );
};

export default Home;

Convexからapiを使用して、バックエンドAPI関数を利用できます。ご覧のように、私たちは2つのAPI関数を呼び出しています。データを読む場合はuseQueryを使用し、データを変更する場合はuseMutationを使用します。このファイルで、作成と読み取りの2つの操作を行っています。

この方法で全てのデータを取得しました:

 const books = useQuery(api.queries.getBooks);

オブジェクトの配列はbooks変数に格納されます。

このコード行で、バックエンドからcreate機能を取得しました:

const createBooks = useMutation(api.queries.createBooks);

UIでCreate機能を使用する方法

UIでcreate機能を使用しましょう。

入力フィールドがformタグに含まれているため、これらのフォームの送信を処理するためにonSubmit属性を使用します。

//In the Home.tsx

const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    createBooks({ title, author })
      .then(() => {
        console.log("created");
        setTitle("");
        setAuthor("");
      })
      .catch((err) => console.log(err));
  };

送信を押すと、handleSubmit関数をトリガーにする。

私たちはcreateBooksを使用して、状態からtitleauthorを渡す。エンドポイント関数は非同期なので、handleSubmitを非同期として使用したり、.thenを使用したりすることができます。私は.then方法を使用して非同期データを処理しました。

データベースから取得したデータを表示するためには、別のコンポーネントを作成することができます。返されるデータはHome.tsxにありますので、Books.tsxコンポーネントにpropsとしてデータを渡します。

Books.tsxでは以下のようになります。

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);
  const [id, setId] = useState("");

  const deleteBooks = useMutation(api.queries.deleteBooks);
  const updateStatus = useMutation(api.queries.updateStatus);

  const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

  const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
  };

  const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

Books.jsxコンポーネントで、データベースからデータを表示し、レコードの更新と削除の機能を処理することができます。

これらの機能のそれぞれについて、一つ一つ手順を踏まえていきましょう。

データの表示方法

Home.tsxコンポーネントで、propsとして渡されたデータを取得することができます。TypeScriptを使用している場合、クエリから返されるオブジェクトについて型を定義しました。JavaScriptを使用している場合はこれを無視することができます。

`books.types.ts`を作成します。

export type book = {
    _id: string,
    title: string,
    author: string,
    isCompleted: boolean
}

データを表示するためにmap関数を使用することができます。

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

これは基本的な構造です。私たちはタイトル、著者、ステータスを表示し、更新と削除のボタンも表示しました。

今、機能を追加しましょう。

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);
  const [id, setId] = useState("");

  const deleteBooks = useMutation(api.queries.deleteBooks);
  const updateStatus = useMutation(api.queries.updateStatus);

  const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

  const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
  };

  const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

これはすべてのコンポーネントのコードです。私が何をしたのか説明しましょう。

まず、更新を切り替える必要があります。そのために、私はhandleClick関数を定義し、文書IDをそこに渡しました。

//handleClick
 const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

handleClick中、ID状態を更新し、更新状態を切り替えることができます。これにより、クリック時に更新入力を切り替え、もう一回クリックすると閉じます。

次に、handleUpdateがあります。データを更新するために、私はイベントオブジェクトと文書IDをそれに渡しました。入力を取得するために、私はFormDataを使用できます。

const updateStatus = useMutation(api.queries.updateStatus);

const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

useMutationを使用してupdateStatus関数を取得する必要があります。IDと完了状態を関数に渡し、.thenを使用して非同期部分を処理します。

削除機能には、文書IDだけが十分です。前のものと同様に、useMutationを使用して削除関数を呼び出し、そしてIDを渡します。

次に、文書IDと約束を処理します。

const deleteBooks = useMutation(api.queries.deleteBooks);

const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
 };

スタイリング

最後に、いくつかのスタイリングを追加する必要があります。私は基本的なスタイリングを追加しました。本が完了していない場合、赤色になり、本が完了している場合、緑色になります。

以下はスクリーンショットです。

これで終わりです、みんな!

私のリポジトリに完全なコードを確認できます: convex-curd

要約

この記事では、本のコレクションアプリを構築することでCRUD(Create、Read、Update、Delete)操作を実装しました。ConvexとReactのセットアップを始め、CRUDロジックを書くことから始めます。

このチュートリアルでは、フロントエンドとバックエンドの両方をカバーし、サーバーレスアプリケーションの構築方法を示しました。

ここで完全なコードを見つけることができます:convex-curd

もし何か間違いや疑問があれば、LinkedInInstagramでご連絡ください。

お読みいただき、ありがとうございました!