CRUD 操作是每个应用程序的基础,因此在学习新技术时熟练掌握它们是至关重要的。

在这个教程中,你将学习如何使用 React 和 Convex 构建一个 CRUD 应用程序。我们将通过构建一个名为“图书收藏”的项目来覆盖这些操作。在这个项目中,用户将能够添加书籍并在完成一本书后更新其状态。

目录

什么是凸形?

凸形是简化后端开发的Baas平台。凸形带有实时数据库,因此您无需分别编写服务器端逻辑,因为它提供了查询和修改数据库的方法。

先决条件

为了遵循本教程,您必须了解React的基础知识。在这个项目中,我将使用TypeScript,但这不是必须的,您也可以跟随JavaScript进行操作。

如何设置您的项目

为项目创建一个单独的文件夹,并按您的意愿命名它——我将命名为书籍。 我们将在那个文件夹中设置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仪表板查看您创建的项目。

现在我们已经设置了Convex和React应用,接下来需要将Convex后端连接到React应用。

src/main.tsx中,用ConvexReactClient包裹您的App组件:

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目录。我们将在这里处理数据库查询和突变。

convex文件夹中创建一个schema.ts文件:

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的文件。在这里,您可以定义用户界面。

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;

您可以根据需要创建自己的组件。我添加了两个输入字段titleauthorsubmit按钮。这是基本结构。现在我们可以在后端创建CRUD方法。

如何创建CRUD函数

convex文件夹中,您可以为CRUD函数创建一个单独的queries.ts文件。

创建函数

convex/queries.ts中:

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

mutation函数需要以下参数:

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

  • 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 方法需要两个参数:第一个参数是文档的 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。在handler中,使用ctx对象的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函数:如果你要读取数据,可以使用useQuery,如果你要改变数据,可以使用useMutation。现在在这个文件中,我们正在进行两个操作,即创建和读取。

我们通过使用这个方法得到了所有的数据:

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

对象数组将存储在books变量中。

我们通过这条代码从后端得到了创建函数:

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

如何在UI中使用创建函数

让我们在UI中使用创建函数。

由于输入字段在form标签中,我们将使用onSubmit属性来处理表单提交。

//在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中,因此我们将数据作为props传递给Books.tsx组件。

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来更新数据,所以我们传递了事件对象以及文档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(创建、读取、更新和删除)操作。我们首先设置 Convex 和 React,并编写 CRUD 逻辑。

本教程涵盖了前端和后端,演示了如何构建无服务器应用。

您可以在这里找到完整代码:convex-curd

如有任何错误或疑问,请通过 LinkedInInstagram 联系我。

感谢您的阅读!