Nextjs15 实战 - React Notes CURD 实现

本专栏内容均可在Github:notes_04 找到

完整项目使用技术栈: Nextjs15 + MySQL + Redis + Auth + Prisma + i18n + strapi + Docker + vercel

一、本节目标

本篇我们来实现右侧笔记CURD部分。

一、效果

当点击 New 按钮的时候进入编辑界面:

当点击具体笔记的 Edit 按钮的时候进入该笔记的编辑页面:

二、笔记预览

回忆下之前的路由设计,当点击 New 的时候,导航至 /note/edit 路由,当点击 Edit 的时候,导航至 /note/edit/xxxx 路由。

新增 app/note/edit/page.tsx

ts 复制代码
import NoteEditor from "../../components/NoteEditor";

export default async function EditPage() {
  return <NoteEditor noteId={""} initialTitle="Untitled" initialBody="" />;
}

新增 app/note/edit/loading.tsx

ts 复制代码
export default function EditSkeleton() {
  return (
    <div
      className="note-editor skeleton-container"
      role="progressbar"
      aria-busy="true"
    >
      <div className="note-editor-form">
        <div className="skeleton v-stack" style={{ height: "3rem" }} />
        <div className="skeleton v-stack" style={{ height: "100%" }} />
      </div>
      <div className="note-editor-preview">
        <div className="note-editor-menu">
          <div
            className="skeleton skeleton--button"
            style={{ width: "8em", height: "2.5em" }}
          />
          <div
            className="skeleton skeleton--button"
            style={{ width: "8em", height: "2.5em", marginInline: "12px 0" }}
          />
        </div>
        <div
          className="note-title skeleton"
          style={{ height: "3rem", width: "65%", marginInline: "12px 1em" }}
        />
        <div className="note-preview">
          <div className="skeleton v-stack" style={{ height: "1.5em" }} />
          <div className="skeleton v-stack" style={{ height: "1.5em" }} />
          <div className="skeleton v-stack" style={{ height: "1.5em" }} />
          <div className="skeleton v-stack" style={{ height: "1.5em" }} />
          <div className="skeleton v-stack" style={{ height: "1.5em" }} />
        </div>
      </div>
    </div>
  );
}

你可能会问,同级的 page.js 又没有数据请求,添加 loading.js 有什么用?

同级的 page.js 确实没有请求,但 loading.js 会将 page.js 和其 children 都包裹在 <Suspense> 中,所以 app/note/edit/[id]/page.tsx 中的请求也会触发该 loading.js

app/note/edit/[id]/page.tsx 代码如下:

ts 复制代码
import NoteEditor from "../../../components/NoteEditor";
import { getNote } from "@/lib/redis";

export default async function EditPage({ params }: { params: { id: string } }) {
  const noteId = params.id;
  const note = await getNote(noteId);

  // 让效果更明显
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
  await sleep(500);

  if (note === null) {
    return (
      <div className="note--empty-state">
        <span className="note-text--empty-state">
          Click a note on the left to view something! 🥺
        </span>
      </div>
    );
  }

  return (
    <NoteEditor
      noteId={noteId}
      initialTitle={note.title}
      initialBody={note.content}
    />
  );
}

我们抽离了一个 <NoteEditor> 组件用于实现编辑功能,app/components/NoteEditor.tsx 代码如下:

ts 复制代码
"use client";

import { useState } from "react";
import NotePreview from "@/components/NotePreview";
import { useFormStatus } from "react-dom";

export default function NoteEditor({ noteId, initialTitle, initialBody }) {
  const { pending } = useFormStatus();
  const [title, setTitle] = useState(initialTitle);
  const [body, setBody] = useState(initialBody);
  const isDraft = !noteId;

  return (
    <div className="note-editor">
      <form className="note-editor-form" autoComplete="off">
        <label className="offscreen" htmlFor="note-title-input">
          Enter a title for your note
        </label>
        <input
          id="note-title-input"
          type="text"
          value={title}
          onChange={(e) => {
            setTitle(e.target.value);
          }}
        />
        <label className="offscreen" htmlFor="note-body-input">
          Enter the body for your note
        </label>
        <textarea
          value={body}
          id="note-body-input"
          onChange={(e) => setBody(e.target.value)}
        />
      </form>
      <div className="note-editor-preview">
        <form className="note-editor-menu" role="menubar">
          <button
            className="note-editor-done"
            disabled={pending}
            type="submit"
            role="menuitem"
          >
            <img
              src="/checkmark.svg"
              width="14px"
              height="10px"
              alt=""
              role="presentation"
            />
            Done
          </button>
          {!isDraft && (
            <button
              className="note-editor-delete"
              disabled={pending}
              role="menuitem"
            >
              <img
                src="/cross.svg"
                width="10px"
                height="10px"
                alt=""
                role="presentation"
              />
              Delete
            </button>
          )}
        </form>
        <div className="label label--preview" role="status">
          Preview
        </div>
        <h1 className="note-title">{title}</h1>
        <NotePreview>{body}</NotePreview>
      </div>
    </div>
  );
}

因为需要控制输入框的状态,所以 <NoteEditor> 使用了客户端组件,我们在 <NotePreview> 中引用了 <NotePreview>组件,用于实现编辑时的实时预览功能。

三、笔记新增、更新、删除

当点击 Done 的时候,导航至对应的笔记预览页面 /note/xxxx。当点击 Delete 的时候,导航至首页。

正常开发笔记的增加、更新和删除功能,为了实现前后端交互,可能要写多个接口来实现,比如当点击删除的时候,调用删除接口,接口返回成功,前端跳转至首页。但既然我们都用了 Next.js 15 了,没必要这么麻烦,Server Actions 直接搞定,省的一个个写接口了。

修改 app/components/NoteEditor.tsx

ts 复制代码
"use client";

import { useState } from "react";
import NotePreview from "./NotePreview";
import { useFormStatus } from "react-dom";
import { deleteNote, saveNote } from "../actions";
export default function NoteEditor({
  noteId,
  initialTitle,
  initialBody,
}: {
  noteId: string;
  initialTitle: string;
  initialBody: string;
}) {
  const { pending } = useFormStatus();
  const [title, setTitle] = useState(initialTitle);
  const [body, setBody] = useState(initialBody);
  const isDraft = !noteId;
  return (
    <div className="note-editor">
      <form className="note-editor-form" autoComplete="off">
        <label className="offscreen" htmlFor="note-title-input">
          Enter a title for your note
        </label>
        <input
          id="note-title-input"
          type="text"
          value={title}
          onChange={(e) => {
            setTitle(e.target.value);
          }}
        />
        <label className="offscreen" htmlFor="note-body-input">
          Enter the body for your note
        </label>
        <textarea
          value={body}
          id="note-body-input"
          onChange={(e) => setBody(e.target.value)}
        />
      </form>
      <div className="note-editor-preview">
        <form className="note-editor-menu" role="menubar">
          <button
            className="note-editor-done"
            disabled={pending}
            type="submit"
            formAction={() => saveNote(noteId, title, body)}
            role="menuitem"
          >
            Done
          </button>
          {!isDraft && (
            <button
              className="note-editor-delete"
              disabled={pending}
              formAction={() => deleteNote(noteId)}
              role="menuitem"
            >
              Delete
            </button>
          )}
        </form>
        <div className="label label--preview" role="status">
          Preview
        </div>
        <h1 className="note-title">{title}</h1>
        <NotePreview>{body}</NotePreview>
      </div>
    </div>
  );
}

新增:app/actions.tsx

ts 复制代码
"use server";

import { redirect } from "next/navigation";
import { addNote, updateNote, delNote } from "@/lib/redis";

export async function saveNote(noteId: string, title: string, body: string) {
  const data = JSON.stringify({
    title,
    content: body,
    updateTime: new Date(),
  });

  if (noteId) {
    updateNote(noteId, data);
    redirect(`/note/${noteId}`);
  } else {
    const res = await addNote(data);
    redirect(`/note/${res}`);
  }
}

export async function deleteNote(noteId: string) {
  delNote(noteId);
  redirect("/");
}

至此 新增和删除可以正常运行了:

相关推荐
满分观察网友z4 分钟前
揭秘 Intersection Observer:让你的网页“活”起来!
前端·javascript
小佐_小佑5 分钟前
如何用 pnpm patch 给 element-plus 打补丁修复线上 bug(以 2.4.4 修复 PR#15197 为例)
前端
nvvas11 分钟前
前端Nodejs报错:Browserslist: caniuse-lite is outdated. Please run: npx update...
前端
Data_Adventure41 分钟前
Vite 项目中使用 vite-plugin-dts 插件的详细指南
前端·vue.js
八戒社44 分钟前
如何使用插件和子主题添加WordPress自定义CSS(附:常见错误)
前端·css·tensorflow·wordpress
xzboss1 小时前
DOM转矢量PDF
前端·javascript
一无所有不好吗1 小时前
纯前端vue项目实现版本更新(纯代码教程)
前端
萌萌哒草头将军1 小时前
🏖️ 舒服,原来写代码还可以这么享受😎!沉浸式敲代码神器!
javascript·vue.js·react.js
安全系统学习1 小时前
内网横向之RDP缓存利用
前端·安全·web安全·网络安全·中间件
Hilaku1 小时前
为什么我不再相信 Tailwind?三个月重构项目教会我的事
前端·css·前端框架