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
是类型验证器,用于为添加到表中的每个数据提供类型。
对于这个项目,由于它是一个书籍收藏应用,结构将包括title
、author
和isCompleted
。您可以添加更多字段。
现在您已经定义了您的架构,让我们在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;
您可以根据需要创建自己的组件。我添加了两个输入字段title
,author
和submit
按钮。这是基本结构。现在我们可以在后端创建CRUD方法。
如何创建CRUD函数
在convex文件夹中,您可以为CRUD函数创建一个单独的queries.ts文件。
创建函数
在convex/queries.ts中:
您可以定义一个名为createBooks
的函数。您可以使用Convex的mutation
函数来创建、更新和删除数据。读取数据将在query
下进行。
mutation
函数需要以下参数:
-
args
:我们需要存储在数据库中的数据。 -
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";
//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
将状态中的title
和author
传递出去。该端点函数是异步的,因此我们可以将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
如有任何错误或疑问,请通过 LinkedIn 或 Instagram 联系我。
感谢您的阅读!
Source:
https://www.freecodecamp.org/news/build-crud-app-react-and-convex/