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;
您可以按照自己的需求創建組件。我新增了兩個輸入字段 title
和 author
,以及一個 submit
按鈕。這是基本結構。現在我們可以在後端建立 CRUD 方法。
如何創建 CRUD 函式
在 convex 資料夾中,您可以為 CRUD 函式建立一個單獨的 queries.ts 文件。
建立函式
在 convex/queries.ts 中:
您可以定義一個函式 createBooks
。您可以使用 Convex 的 mutation
函式來建立、更新和刪除數據。讀取數據將屬於 query
。
mutation
函式期待這些參數:
-
agrs
:我們需要儲存在數據庫中的數據。 -
handler
:處理將日期儲存在數據庫中的邏輯。handler
是一個異步函式,它有两个參數:ctx
和args
。在此處,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
將從狀態中傳遞的 title
和 author
。端點函數是異步的,因此我們可以將 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
如果有任何錯誤或疑問,請在LinkedIn、Instagram上聯繫我。
感謝您的閱讀!
Source:
https://www.freecodecamp.org/news/build-crud-app-react-and-convex/