五、基于Nextjs+ShadcnUI+ReactQuery+Prisma的简单示例

本章节,我们基于前面学习的知识,做一个简单的TodoList示例。

只是展示前边说过的几个库的用法,所以一切从简,不会考虑太多实际开发中的情况

比如删除是真删除,尽量只用一个表演示下各种用法

项目的创建以及依赖的安装在前面的章节已经说过了,这里就不重复了。

完整代码存放于GitHub

Schema

1
2
3
4
5
6
7
8
9
model Todo {
id Int @id @default(autoincrement())
content String
finished Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")

@@map("todos")
}

模型更改后需要同步到数据库:

1
npx prisma migrate dev --name init

静态页面

我们添加shadcn ui的组件:

  • button
  • input
  • checkbox

先封装两个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// InputSection
"use client";
import { ChangeEvent, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";

export default function InputSection() {
const [inputVal, setInputVal] = useState("");
const handleInputValChange = (evt: ChangeEvent<{value: string}>) => {
setInputVal(evt.target.value)
};
return (
<div className="flex items-center space-x-4">
<Input
placeholder="Please enter here..."
value={inputVal}
onChange={handleInputValChange}
/>
<Button>Add</Button>
</div>
);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
// TodoItem
import { Button } from "./ui/button";
import { Checkbox } from "./ui/checkbox";

export default function TodoItem() {
return (
<div className="flex items-center space-x-2">
<Checkbox />
<span>内容</span>
<Button size="sm">Delete</Button>
</div>
)
}

在页面中引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import InputSection from "@/components/input-section";
import TodoItem from "@/components/todo-item";

export default function Home() {
return (
<main className="pt-8">
<div className="w-[800px] mx-auto">
{/* 录入部分 */}
<InputSection />
{/* 未完成的事项 */}
<div className="my-6">
<TodoItem />
</div>
{/* 已完成的事项 */}
<div>
<TodoItem />
</div>
</div>
</main>
);
}

API

我们创建的api目录结构为:

1
2
3
4
5
- app
- api
- route.ts
- [id]
- route.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/api/todo/route.ts
import prisma from "@/db/prisma"
// 查
export async function GET() {
const todos = await prisma.todo.findMany()
return Response.json(todos)
}
// 增
export async function POST(request: Request) {
const body = await request.json()
const todo = await prisma?.todo.create({
data: body
})
return Response.json(todo)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// app/api/todo/[id]/route.ts
import prisma from "@/db/prisma";
// 改
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
const finished = await request.json();
const id = Number(params.id);
const res = await prisma.todo.update({
where: {
id,
},
data: {
finished,
},
});
return Response.json(res);
}
// 删
export async function DELETE(_: any, { params }: { params: { id: string } }) {
const res = await prisma.todo.delete({
where: {
id: Number(params.id)
}
})
return Response.json(res)
}

对接接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// page.tsx
"use client";
import InputSection from "@/components/input-section";
import TodoItem from "@/components/todo-item";
import { TodoItemType } from "@/types/todo";
import { useQuery } from "@tanstack/react-query";

export default function Home() {
const getAllTodos = async () => {
const r = await fetch("/api/todo");
const res = await r.json();
return res;
};
const query = useQuery({
queryKey: ["todos"],
queryFn: getAllTodos,
});
return (
<main className="pt-8">
<div className="w-[800px] mx-auto">
{/* 录入部分 */}
<InputSection />
{/* 未完成的事项 */}
<h2 className="mt-6">Todo:</h2>
<div className="mb-6">
{query.data
?.filter((todo: TodoItemType) => !todo.finished)
.map((todo: TodoItemType) => {
return <TodoItem key={todo.id} todo={todo} />;
})}
</div>
{/* 已完成的事项 */}
<h2>Finished:</h2>
<div>
{query.data
?.filter((todo: TodoItemType) => todo.finished)
.map((todo: TodoItemType) => {
return <TodoItem key={todo.id} todo={todo} />;
})}
</div>
</div>
</main>
);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// input-section.tsx
"use client";
import { ChangeEvent, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useMutation, useQueryClient } from "@tanstack/react-query";

export default function InputSection() {
const queryClient = useQueryClient();
const [inputVal, setInputVal] = useState("");
const handleInputValChange = (evt: ChangeEvent<{ value: string }>) => {
setInputVal(evt.target.value);
};
const handleAdd = async (data: { content: string }) => {
const res = await fetch("/api/todo", {
method: 'POST',
body: JSON.stringify(data)
});
const r = await res.json()
return r
};
const mutation = useMutation({
mutationFn: handleAdd,
onSuccess: () => {
setInputVal('')
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
return (
<div className="flex items-center space-x-4">
<Input
placeholder="Please enter here..."
value={inputVal}
onChange={handleInputValChange}
/>
<Button
onClick={() => {
mutation.mutate({
content: inputVal,
});
}}
>
Add
</Button>
</div>
);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// todo-item.tsx
'use client'
import { TodoItemType } from "@/types/todo";
import { Button } from "./ui/button";
import { Checkbox } from "./ui/checkbox";
import { useMutation, useQueryClient } from "@tanstack/react-query";

export default function TodoItem({todo}: {todo: TodoItemType}) {
const queryClient = useQueryClient()
const patchMutation = useMutation({
mutationFn: async (checked: boolean) => {
const r = await fetch(`/api/todo/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify(checked)
})
const res = await r.json()
return res
},
onSuccess: () => {
queryClient.invalidateQueries({queryKey: ['todos']})
}
})
const deleteMutation = useMutation({
mutationFn: async () => {
const r = await fetch(`/api/todo/${todo.id}`, {
method: 'DELETE'
})
return await r.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})

return (
<div className="flex items-center space-x-2 mb-3 last:mb-0">
<Checkbox checked={todo.finished} onCheckedChange={(checked: boolean) => {
patchMutation.mutate(checked)
}} />
<span className={
todo.finished ? 'line-through' : ''
}>{todo.content}</span>
<Button size="sm" onClick={() => {
deleteMutation.mutate()
}}>Delete</Button>
</div>
)
}

至此,我们就完成了一个很简单的具备CRUD功能的TODOList

作者

胡兆磊

发布于

2024-06-24

更新于

2024-07-02

许可协议