【Next.js 项目实战系列】04-修改 Issue

原文链接

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的点个star,关注一下吧

上一篇【Next.js 项目实战系列】03-查看 Issue

修改 Issue

添加修改 Button

本节代码链接

安装 Radix UI 的 Radix Ui Icons

复制代码
npm i @radix-ui/react-icons
TypeScript 复制代码
# /app/issues/[id]/page.tsx

  ...
  const IssueDeatilPage = async ({ params }: Props) => {
    ...

    return (
      // 添加一个 Grid 以分列显示,设置 initial 为 1,在移动设备为每页 1 栏,平板以上则 2 栏
      <Grid columns={{ initial: "1", md: "2" }} gap="5">
        <Box>
          <Heading as="h2">{issue.title}</Heading>
          ...
        </Box>
        {/*添加一个 Button 用于编辑*/}
+       <Box>
+         <Button>
+           <Pencil2Icon />
+           <Link href={`/issues/${issue.id}/edit`}>Edit Issue</Link>
+         </Button>
+       </Box>
      </Grid>
    );
  };
  export default IssueDeatilPage;

Single Responsbility Principle

本节代码链接

Software entities should have a single responsibility

重构 /app/issues/[id]/page.tsx 以应用 SRP

  • page.tsx
  • IssueDetails.tsx
  • EditIssueButton.tsx
TypeScript 复制代码
# /app/issues/[id]/page.tsx

import prisma from "@/prisma/client";
import { Box, Grid } from "@radix-ui/themes";
import { notFound } from "next/navigation";
import EditIssueButton from "./EditIssueButton";
import IssueDetails from "./IssueDetails";

interface Props {
  params: { id: string };
}
const IssueDeatilPage = async ({ params }: Props) => {
  const issue = await prisma.issue.findUnique({
    where: { id: parseInt(params.id) },
  });

  if (!issue) notFound();

  return (
    <Grid columns={{ initial: "1", md: "2" }} gap="5">
      <Box>
        <IssueDetails issue={issue} />
      </Box>
      <Box>
        <EditIssueButton issueId={issue.id} />
      </Box>
    </Grid>
  );
};
export default IssueDeatilPage;
TypeScript 复制代码
# /app/issues/[id]/IssueDetails.tsx

import { IssueStatusBadge } from "@/app/components";
import { Issue } from "@prisma/client";
import { Card, Flex, Heading, Text } from "@radix-ui/themes";
import ReactMarkdown from "react-markdown";

const IssueDetails = ({ issue }: { issue: Issue }) => {
  return (
    <>
      <Heading as="h2">{issue.title}</Heading>
      <Flex gap="3" my="5">
        <IssueStatusBadge status={issue.status}></IssueStatusBadge>
        <Text>{issue.createdAt.toDateString()}</Text>
      </Flex>
      <Card className="prose">
        <ReactMarkdown>{issue.description}</ReactMarkdown>
      </Card>
    </>
  );
};
export default IssueDetails;
TypeScript 复制代码
# /app/issues/[id]/EditIssueButton.tsx

import { Pencil2Icon } from "@radix-ui/react-icons";
import { Button } from "@radix-ui/themes";
import Link from "next/link";

const EditIssueButton = ({ issueId }: { issueId: number }) => {
  return (
    <Button>
      <Pencil2Icon />
      <Link href={`/issues/${issueId}/edit`}>Edit Issue</Link>
    </Button>
  );
};
export default EditIssueButton;

修改 Issue

页面

本节代码链接

我们可以像这样构建文件结构,在 Issue 目录下创建 _components 以放置该目录下需要重复使用的组件,文件夹名前添加下划线就可以把这个文件夹从路由中移除

复制代码
└─issues
    │  IssueActions.tsx
    │  loading.tsx
    │  page.tsx
    │
    ├─new
    │      loading.tsx
    │      page.tsx
    │
    ├─[id]
    │  │  EditIssueButton.tsx
    │  │  IssueDetails.tsx
    │  │  loading.tsx
    │  │  page.tsx
    │  │
    │  └─Edit
    │          page.tsx
    │
    └─_components
            IssueForm.tsx

将之前的 new/page.tsx 封装为一个组件,并添加一个可选参数,以初始化

TypeScript 复制代码
# /app/issues/_components/IssueForm.tsx

  ...
+ import { Issue } from "@prisma/client";
  ...
  // 添加一个可选参数 issue 类型为之前 prisma 中的 Issue
- const IssueForm = () => {
+ const IssueForm = ({ issue }: { issue?: Issue }) => {
    ...

    return (
      <div className="max-w-xl prose">
        ...
        <TextField.Root>
          <TextField.Input
            // 将该字段初始化为 issue.title (若传入 issue)
+             defaultValue={issue?.title}
            placeholder="Title"
            {...register("title")}
          />
        </TextField.Root>
        <ErrorMessage>{errors.title?.message}</ErrorMessage>
        <Controller
          // 将该字段初始化为 issue.description (若传入 issue)
+           defaultValue={issue?.description}
          name="description"
          control={control}
          render={({ field }) => (
            <SimpleMDE placeholder="Description" {...field} />
          )}
        />
        ...
      </div>
    );
  };
  export default IssueForm;

API

本节代码链接

TypeScript 复制代码
# /app/api/issues/[id]/route.tsx

import { issueSchema } from "@/app/validationSchema";
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/prisma/client";

export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const body = await request.json();
  const validation = issueSchema.safeParse(body);
  if (!validation.success)
    return NextResponse.json(validation.error.format(), { status: 400 });

  const issue = await prisma.issue.findUnique({
    where: { id: parseInt(params.id) },
  });
  if (!issue)
    return NextResponse.json({ error: "Invalid Issue" }, { status: 404 });

  const updatedIssue = await prisma.issue.update({
    where: { id: issue.id },
    data: { title: body.title, description: body.description },
  });

  return NextResponse.json(updatedIssue, { status: 200 });
}

连接

本节代码链接

TypeScript 复制代码
# /app/issues/_components/IssueForm.tsx

  const IssueForm = ({ issue }: { issue?: Issue }) => {
    ...

    return (
      ...
      <form
        className="space-y-3"
        onSubmit={handleSubmit(async (data) => {
          try {
            setSubmitting(true);
            // 判断是否传入了 issue,若有传入则是 Update,若无则是 new
+           if (issue) await axios.patch("/api/issues/" + issue.id, data);
-           await axios.post("/api/issues", data);
+           else await axios.post("/api/issues", data);
            router.push("/issues");
          } ...
        })}
      >
        ...
        <Button disabled={isSubmitting}>
+         {issue ? "Update Issue" : "Submit New Issue"}{" "}
          {isSubmitting && <Spinner />}
        </Button>
      </form>
      ...
    );
  };
  export default IssueForm;

Caching

本节代码链接

NextJS Route Segment Config

  • Data Cache:
    • When we fetch data using fetch()
    • Stored in the file system
    • Permanent unitl we redeploy
    • fetch(".",{cache: "no-store"})
    • fetch(".",{revalidata: 3600})
  • Full Route Cache
    • Used to store the output of statically renderd routes
  • Router Cache (Client-side Cache)
    • To store the payload of pages in browser
    • Lasts for a session
    • Gets refreshed when we reload

提升 Loading 体验

本节代码链接

由于我们要在多个地方用到 IssueForm 的 Skeleton,我们可以将其封装到一个组件里,然后在需要的地方调用。其次,对于静态的页面可以直接使用 loading.tsx,但是对于需要用到 dynamic 函数的页面,应该用另一种方法

  • IssueFormSkeleton.tsx
  • page.tsx
  • loading.tsx
TypeScript 复制代码
# /app/issues/_components/IssueFormSkeleton.tsx

import { Skeleton } from "@/app/components";
import { Box } from "@radix-ui/themes";

const IssueFormSkeleton = () => {
  return (
    <Box className="max-w-xl">
      <Skeleton height="2rem" />
      <Skeleton height="20rem" />
    </Box>
  );
};
export default IssueFormSkeleton;
TypeScript 复制代码
# /app/issues/[id]/edit/page.tsx

import prisma from "@/prisma/client";
import dynamic from "next/dynamic";
import { notFound } from "next/navigation";
import IssueFormSkeleton from "./loading";

const IssueForm = dynamic(() => import("@/app/issues/_components/IssueForm"), {
  ssr: false,
  loading: () => <IssueFormSkeleton />,
});

interface Props {
  params: { id: string };
}

const EditIssuePage = async ({ params }: Props) => {
  const issue = await prisma.issue.findUnique({
    where: { id: parseInt(params.id) },
  });

  if (!issue) notFound();

  return <IssueForm issue={issue} />;
};
export default EditIssuePage;
TypeScript 复制代码
# /app/issues/[id]/edit/loading.tsx

import IssueFormSkeleton from "@/app/issues/_components/IssueFormSkeleton";
export default IssueFormSkeleton;

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的点个star,关注一下吧

下一篇讲删除 Issue

下一篇【Next.js 项目实战系列】05-删除 Issue

相关推荐
知兀13 小时前
【前端】受控和非受控组件
前端·javascript·react
醉颜凉1 天前
React触摸事件全解析:掌握移动端交互开发的核心秘籍
react·前端开发·触摸事件·移动端交互·react事件
递归尽头是星辰2 天前
Java 智能体工程化:从 Spring AI 出发读懂 AgentScope
react·智能体·springai·javaai·agentscope
moMo4 天前
NestJS 入门:用"模块化 + 装饰器"读懂大型后端框架
next.js
天道kabuto5 天前
前端每日知识点:Next.js 渲染与缓存:从 Pages Router 到 Cache Components 的范式迁移
next.js
Richown5 天前
React 高级模式:并发渲染下的状态机驱动架构——从 Finite State Machine 到生产级实现
区块链·react
LayZhangStrive6 天前
Agent开发 - 实现人类与Manus智能体的终端窗口命令交互
ai·交互·agent·react·终端·manus
名字还没想好☜7 天前
React 实现暗黑模式切换:localStorage 持久化、SSR 首屏闪烁与跟随系统主题
前端·javascript·react.js·ecmascript·react·next.js
Linguwen7 天前
AI外贸建站04|三个账号第一次联动:让Codex写一个网页,从生成到上线走完整条流水线
next.js·外贸独立站·企业官网·外贸建站·ai建站
Linguwen8 天前
AI外贸建站03|建站三件套:GitHub、Next.js、Codex各干什么,为什么别再用模板建站
next.js·外贸独立站·企业官网·外贸建站·ai建站