iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 11 篇

Day-011. 一些讓你看來很強的全端 TRPC 伴讀 - TodoList (下)

  • 分享至 

  • xImage
  •  

今天我們繼續完成剩下的部分,昨天已經完成 GET / posts 的 api,也順利接值了,今天繼續完 CREATE DELETE UPDATE 的部分。

完成 api

昨天的 post 資料我們是透過 prisma studio 去添加的,但實際開發中我們並不會這麼做,主要是讓讀者可以大概知道 prisma studio 有什麼內容,所以接下來就來實現如何 create post。

首先先回顧一下昨日定義的 schema

// @/validate/api/post
import { z } from "zod";

export const getPostSchema = z.object({
  post_id: z.string()
})
export type GetPostSchema = z.infer<typeof getPostSchema>

export const createPostSchema = z.object({
  title: z.string().min(1, { message: 'title required' }),
  content: z.string().optional(),
  published: z.boolean().default(false)
})
export type CreatePostSchema = z.infer<typeof createPostSchema>


export const togglePostPuPublishedSchema = z.object({
  id: z.number(),
  published: z.boolean()
})
export type TogglePostPuPublishedSchema = z.infer<typeof togglePostPuPublishedSchema>


export const deletePostSchema = z.object({
  id: z.number()
})
export type DeletePostSchema = z.infer<typeof deletePostSchema>

之後創建 addPost 的 api,在 trpc 中並不像 RESTful API 有各種 https methods 例如 GET、POST 、DELETE 等等,取而代之的就是除了 GET 以外是透過 .query() ,其餘都是 .mutation() 涵蓋一切,用起來跟 graphql 一樣。

這邊的 addPost 邏輯很簡單:

  1. 預防重複的 post 添加到 db,會先找尋 duplicatePost 如果有救throw error。
  2. 根據 prisma.post.create 結果返回 postid。
// ~src/server/api/post.ts
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import { TRPCError } from "@trpc/server";
import {
  getPostSchema,
  createPostSchema,
  togglePostPuPublishedSchema,
  deletePostSchema
} from "@/validate/api/post";

export const postsRouter = router({
//..
  addPost: publicProcedure
    .input(createPostSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx
      const duplicatePost = await prisma.post.findFirst({
        where: {
          title: input.title
        }
      })
      if (duplicatePost) {
        throw new TRPCError({ code: 'CONFLICT', message: 'Title already exists' })
      }
      const { id: postId } = await prisma.post.create({
        data: input,
        select: {
          id: true
        }
      })

      return { message: 'success create post', id: postId }

    })
})

在 prisma 中可以透過 select 選擇你要 return 的 fields,這樣的寫法假如你的 data 有很多 key 可以大大減少不需要 return 的資料,除了提升 query 效能外,可讀性也有幫助。

const { id: postId } = await prisma.post.create({
    data: input,
    select: {
      id: true
    }
})

之後我們到 PostForm 引入 addPost ,api.posts.addPost.useMutation 會 return
mutateAsync 的 function,我們在 onSubmit 把我們 form data 傳參數給他,然後在 useMutation 中還有 onSuccess 跟 onError callback function,用來驗證 mutateAsync 成功與否。

// ~src/components/PostForm.tsx
import { RouterInputs, api } from '@/utils/api'
import { createPostSchema, type CreatePostSchema } from '@/validate/api/post'
import { zodResolver } from '@hookform/resolvers/zod'
import React from 'react'
import { FieldError, FieldErrors, SubmitHandler, useForm } from 'react-hook-form'
import { Input } from './Input'
import { Button } from './Button'
import { TRPCClientError } from '@trpc/client'
import { queryClient } from './Provider'

export const PostForm = () => {
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {
      console.log('onSuccess')
    },
    onError: (e) => {
      if (e instanceof TRPCClientError) {
        console.log('TRPCClientError')
      }
    }
  })
  const { register, formState: { errors }, handleSubmit } = useForm<CreatePostSchema>({
    resolver: zodResolver(createPostSchema),
    mode: 'onChange',
    defaultValues: {
      published: false
    }
  })
  const onSubmit: SubmitHandler<CreatePostSchema> = async (data) => {
    await createPost(data)
  }

  return (
    <div
      className="
        bg-white
          px-4
          py-8
          shadow
          sm:rounded-lg
          sm:px-10
        "
    >
     // ..

    </div>
  );
}

但這時你發現我明明按下 submit ,卻沒有新增 post。

此時你重新整理畫面後資料就出現了。

但顯然這不是我們要的結果,雖然說我們成功新增 post data 到 db ,但我們 client 端得 post lists 並沒有新的內容。

原因在於 query cache 並沒有更新。

重新整理前

重新整理後

所以問題是因為 react query 得 cache 並沒有 update 內容,相信有用過 react query 的讀者肯定知道可以透過 query key 方式透過呼叫 queryClient.invalidateQueries 去更新 query cache 內容,但 trpc 有提供 useContext 整合 invalidate 功能。

import { queryClient } from './Provider'

export const PostForm = () => {
  const utils = api.useContext()
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey:['yourKEY']
        })
    }

透過 useContext 我們就可以根據 route 去 invalidate query cache 得內容摟~

export const PostForm = () => {
  const utils = api.useContext()
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {

      utils.posts.getPosts.invalidate()
      console.log('onSuccess')
    },

這樣我們每次 create post ,getPosts 內容就會重新 update 了。

這邊簡單補充 trpc invalidate 功能。

如果你希望每次執行 useMutation 自動更新所有 query cache 內容,而不是一個一個根據 router invalidate 的話你可以這樣寫。

export const trpc = createTRPCReact<AppRouter, SSRContext>({
  overrides: {
    useMutation: {
      /**
       * This function is called whenever a `.useMutation` succeeds
       **/
      async onSuccess(opts) {
        /**
         * @note that order here matters:
         * The order here allows route changes in `onSuccess` without
         * having a flash of content change whilst redirecting.
         **/
        // Calls the `onSuccess` defined in the `useQuery()`-options:
        await opts.originalFn();
        // Invalidate all queries in the react-query cache:
        await opts.queryClient.invalidateQueries();
      },
    },
  },
});

或是你只想在特定的 useMutation 中 invalidate all query cache,可以這樣做

export const PostForm = () => {
  const utils = api.useContext()
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {

      utils.invalidate()
      console.log('onSuccess')
    },

剩下的 Update toggle 跟 delete post 就很簡單了,就執行 prisma.post.update 跟prisma.post.delete function 。

import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import { TRPCError } from "@trpc/server";
import {
  getPostSchema,
  createPostSchema,
  togglePostPuPublishedSchema,
  deletePostSchema
} from "@/validate/api/post";

export const postsRouter = router({
  // ..

  addPost: publicProcedure
    .input(createPostSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx
      const duplicatePost = await prisma.post.findFirst({
        where: {
          title: input.title
        }
      })
      if (duplicatePost) {
        throw new TRPCError({ code: 'CONFLICT', message: 'Title already exists' })
      }
      const { id: postId } = await prisma.post.create({
        data: input,
        select: {
          id: true
        }
      })

      return { message: 'success create post', id: postId }
    }),
  togglePostPublish: publicProcedure
    .input(togglePostPuPublishedSchema)
    .mutation(async ({ input, ctx }) => {
      const { id, published } = input
      const { prisma } = ctx
      await prisma.post.update({
        where: {
          id
        },
        data: {
          published
        }
      })
    }),
  deletePost: publicProcedure
    .input(deletePostSchema)
    .mutation(async ({ input, ctx }) => {
      const { id } = input
      const { prisma } = ctx
      await prisma.post.delete({
        where: {
          id
        }
      })

    })
})

最後再把 api 加上,這樣你就完成所有 crud 功能摟~

import { PostForm } from "@/components/PostForm";
import { api } from "@/utils/api";
import { AiFillDelete } from "react-icons/ai";

export default function Home() {
  const utils = api.useContext()
  const { data: posts, isLoading, isError, error } = api.posts.getPosts.useQuery()
  const { mutateAsync: togglePost } = api.posts.togglePostPublish.useMutation({
    onSuccess: () => {
      utils.posts.invalidate()
    }
  })
  const { mutateAsync: deletePost } = api.posts.deletePost.useMutation({
    onSuccess: () => {
      utils.posts.invalidate()
    }
  })
  if (isLoading) return 'isLoading'
  if (isError) return error.message
  return (
    <div className="bg-gray-100 min-h-screen overflow-y-auto p-4">
      <h2 className="text-center text-3xl">Create posts</h2>
      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <PostForm />
        <ul className="flex flex-col gap-[1rem] justify-center mt-5">
          {posts.map((post, index) => (
            <li key={post.id} className="flex items-center justify-between">
              <label
                htmlFor=""
                className={`
                  text-2xl 
                  ${!!post.published && "line-through"}
                `}
                onClick={async () => {
                  await togglePost({ id: post.id, published: !post.published })
                }}
              >{post.title}</label>
              <AiFillDelete
                color="red"
                className="cursor-pointer"
                size={20}
                onClick={async () => {
                  await deletePost({ id: post.id })
                }}
              />
            </li>
          ))}
        </ul>
      </div>

    </div>
  );
}

相關連結

完整的 demo 可以在這邊查看: https://github.com/Danny101201/next_demo/tree/main

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day-010. 一些讓你看來很強的全端 TRPC 伴讀 - TodoList (上)
下一篇
Day-012. 一些讓你看來很強的全端 TRPC 伴讀 - Links
系列文
一些讓你看來很強的全端- trcp 伴讀 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言