安装组件库
这里我们选用的是shadcn
# 初始化
npx shadcn-ui@latest init
# 安装button
npx shadcn-ui@latest add button
# 安装input
npx shadcn-ui@latest add input
# 安装dialog
npx shadcn-ui@latest add dialog
# 安装table
pnpm dlx shadcn-ui@latest add table
# 安装lebel
npx shadcn-ui@latest add label
通过trpc创建API
我们将使用 tRPC 和 Drizzle ORM 实现简单 API 路由器 appRouter。它将包含几个处理消息记录的路由,包括:
getMessages:一个查询,返回所有消息记录。setMessage:一个突变(mutation),接受一个包含name、email和message的对象作为输入,并插入一条新的消息记录,返回插入的记录。updateMessage:一个突变,接受一个包含id、name、email和message的对象作为输入,更新指定id的消息记录。deleteMessage:一个突变,接受一个消息记录的id作为输入,删除指定id的消息记录。
通过这些路由,用户可以进行消息记录的增删改查操作。具体代码如下:
import { z } from 'zod'
import { db } from './db/db'
import { publicProcedure, router } from './trpc'
import { messages } from './db/schema'
import { eq } from 'drizzle-orm'
export const appRouter = router({
greeting: publicProcedure.query(() => 'hello tRPC v10!'),
getMessages: publicProcedure.query(async ({ ctx }) => {
return db.query.messages.findMany()
}),
setMessage: publicProcedure
.input(
z.object({
name: z.string(),
email: z.string(),
message: z.string()
})
)
.mutation(async ({ ctx, input }) => {
const result = await db.insert(messages).values(input).returning()
return result[0]
}),
updateMessage: publicProcedure
.input(
z.object({
id: z.number(),
name: z.string(),
email: z.string(),
message: z.string()
})
)
.mutation(({input, ctx }) => {
return db.update(messages)
.set({ name: input.name, email: input.email, message: input.message })
.where(eq(messages.id, input.id))
}),
deleteMessage: publicProcedure.input(z.number()).mutation(({input}) => {
return db.delete(messages).where(eq(messages.id, input))
})
})
export type AppRouter = typeof appRouter
处理tRPC client
我们首先定义一个 tRPC 客户端,用于与后端 API 进行通信。首先,它导入了 AppRouter 类型和必要的 tRPC 客户端和 React 库。接着,使用 createTRPCReact 创建一个 React 专用的 tRPC 客户端 trpcClientReact。然后,通过 createClient 方法配置了 tRPC 客户端的链接,使用 httpBatchLink 指定后端 API 的 URL 为 http://localhost:3000/api/trpc。这个客户端允许在 React 组件中方便地调用 tRPC API。
我们调整utils/api.ts中的代码,具体如下:
import { AppRouter } from "@/server/_app";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from '@trpc/react-query'
export const trpcClientReact = createTRPCReact<AppRouter>({})
export const trpcClient = trpcClientReact.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc'
})
]
})
定义客户端相关组件
创建TrpcProvider
具体代码如下:
'use client'
import type { ReactNode } from 'react'
import React, { useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { trpcClient, trpcClientReact } from '@/utils/api'
export default function TrpcProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
return (
<trpcClientReact.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpcClientReact.Provider>
)
}
创建列表页面
接下来,我们创建一个用于显示消息列表的页面组件。这个组件将使用表格的形式显示消息数据,并提供编辑和删除功能。具体代码如下:
'use client'
import AddMessage, { FormProps } from '@/components/feature/AddMessage'
import { Button } from '@/components/ui/button'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table'
import { MessageProps, queryMessageSchema } from '@/server/db/validate-schema'
import { trpcClientReact } from '@/utils/api'
import { PlusIcon, Pencil1Icon, TrashIcon } from '@radix-ui/react-icons'
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table'
import { useEffect, useState } from 'react'
import z from 'zod'
export default function Home() {
const { mutateAsync } = trpcClientReact.deleteMessage.useMutation()
const [resetForm, setResetForm] = useState<FormProps>()
const [data, setData] = useState<MessageProps[]>([])
const [open, setOpen] = useState<boolean>(false)
const { data: messages, refetch } = trpcClientReact.getMessages.useQuery()
const columns: ColumnDef<MessageProps>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => <div className="capitalize">{row.getValue('id')}</div>
},
{
accessorKey: 'email',
header: 'Email',
cell: ({ row }) => <div className="lowercase">{row.getValue('email')}</div>
},
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => <div className="lowercase">{row.getValue('name')}</div>
},
{
accessorKey: 'message',
header: 'Message',
cell: ({ row }) => <div className="lowercase">{row.getValue('message')}</div>
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: ({ row }) => <div className="lowercase">{row.getValue('createdAt')}</div>
},
{
accessorKey: 'updatedAt',
header: 'Updated At',
cell: ({ row }) => <div className="lowercase">{row.getValue('updatedAt')}</div>
},
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
console.log(row)
return (
<div className="flex gap-1">
<Button
onClick={() => {
setResetForm({
id: row.original.id,
name: row.original.name!,
email: row.original.email,
message: row.original.message
})
setOpen(true)
}}
>
<Pencil1Icon />
</Button>
<Button
onClick={async () => {
await mutateAsync(row.original.id)
refetch()
}}
>
<TrashIcon />
</Button>
</div>
)
}
}
]
useEffect(() => {
if (messages?.length) {
setData(messages)
}
}, [messages])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel()
})
return (
<div className="container max-w-3xl mx-auto space-y-4 mt-12">
<header className="flex justify-between items-center">
<h2 className="text-3xl">Messages</h2>
<Button onClick={() => setOpen(true)}>
<PlusIcon />
</Button>
</header>
<main>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<AddMessage
open={open}
onSuccess={() => {
setOpen(false)
refetch()
}}
resetFromData={resetForm}
/>
</main>
</div>
)
}
创建新增和编辑组件
我们还需要一个组件 AddMessage 来处理消息的新增和编辑。该组件将使用 react-hook-form 来管理表单状态和验证。
import React, { useEffect } from 'react'
import { Label } from '../ui/label'
import { SubmitHandler, useForm } from 'react-hook-form'
import { Input } from '../ui/input'
import { MessageProps } from '@/server/db/validate-schema'
import { Button } from '../ui/button'
import { trpcClientReact } from '@/utils/api'
import { Dialog, DialogContent, DialogHeader } from '../ui/dialog'
export type FormProps = {
name: string
email: string
message: string
id: number
}
export default function AddMessage({
open,
onSuccess,
resetFromData
}: {
open: boolean
onSuccess: (v: boolean) => void
resetFromData?: FormProps
}) {
const {
register,
handleSubmit,
reset,
formState: { errors }
} = useForm<FormProps>()
const { mutateAsync: setMessage } = trpcClientReact.setMessage.useMutation()
const { mutateAsync: updateMessage } = trpcClientReact.updateMessage.useMutation()
if (status === 'success') {
onSuccess(false)
}
const onSubmit: SubmitHandler<FormProps> = async (data) => {
if (resetFromData) {
await updateMessage(data)
} else {
await setMessage(data)
}
onSuccess(false)
}
useEffect(() => {
if (resetFromData) {
reset(resetFromData)
}
}, [resetFromData])
return (
<Dialog open={open} onOpenChange={() => onSuccess(!open)}>
<DialogContent>
<DialogHeader>
<h2 className="text-3xl font-bold">New Message</h2>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
<div>
<Label>Name</Label>
<Input {...register('name', { required: 'Name is required' })} />
<span className="text-red-500">{errors.name?.message}</span>
</div>
<div>
<Label>Email</Label>
<Input {...register('email', { required: 'Email is required' })} />
<span className="text-red-500">{errors.email?.message}</span>
</div>
<div>
<Label>Message</Label>
<Input {...register('message', { required: 'Message is required' })} />
<span className="text-red-500">{errors.message?.message}</span>
</div>
<Button type="submit">{resetFromData ? 'Update' : 'Submit'}</Button>
</form>
</DialogContent>
</Dialog>
)
}
总结
本文主要展示了如何使用 tRPC 和 React Query 创建一个消息管理系统。我们定义了客户端相关组件,创建了消息列表页面,并实现了新增和编辑功能。希望这篇文章能帮助你理解如何结合使用 tRPC 和 React Query 构建高效的全栈应用。如果你有任何问题或建议,欢迎在评论区留言!
Repo: github.com/valcosmos/n…