drizzle+trpc实现简单的CRUD操作

532 阅读4分钟

安装组件库

这里我们选用的是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),接受一个包含 nameemailmessage 的对象作为输入,并插入一条新的消息记录,返回插入的记录。
  • updateMessage:一个突变,接受一个包含 idnameemailmessage 的对象作为输入,更新指定 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…