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。如果你是第一次这样做,它应该会要求你进行认证。否则,它应该会询问项目名称。

您可以Visit凸形控制面板來查看您創建的項目。

既然我們已經 setting

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 instantiated

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

如何創建架构

在您的主项目建设目录中,您应该看到凸形目录。我们将在这里处理数据库查询和突变。

凸形文件夹中创建一个架构.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是类型验证器,它用于为添加到表中的每个数据提供类型。

对于这个项目,因为它是一个书籍收藏应用程序,结构将有标题作者isCompleted。您可以添加更多字段。

既然您已经定义了您的架构,那就让我们在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,以及一個 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` 方法期待兩個參數:第一個參數是 文档的 `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";

//刪除
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函數。正如您所看到的,我們調用了兩個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 中,因此我們將數據傳遞給 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來更新數據,所以我們傳遞了事件對象以及文件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上聯繫我。

感謝您的閱讀!