全栈开发

Nextjs 问题记录

1、server端使用redirect函数跳转

createWorkflow.ts

"use server"

import prisma from "@/lib/prisma";
import { createWorkflowSchema, createWorkflowSchemaType } from "@/schema/workflow";
import { WorkflowStatus } from "@/types/workflow";
import { redirect } from "next/navigation";

export async function CreateWorkflow(form: createWorkflowSchemaType) {
    const {success, data} = createWorkflowSchema.safeParse(form)
    if (!success) {
        throw new Error("invalid form data")
    }
    
    // const {userId} = auth()
    // if (!userId) {
        // throw new Error("unauthenticated")
    // }

    const userId = "test"

    const result = await prisma.workflow.create({
        data: {
            userId,
            status: WorkflowStatus.DRAFT,
            definition: "TODO",
            ...data
        }
    })

    if (!result) {
        throw new Error("failed to create workflow")
    }

    redirect(`/workflow/editor/${result.id}`)
}

client端使用 useMutation捕获server端结果

...
    const { mutate, isPending } = useMutation({
        mutationFn: CreateWorkflow,
        onSuccess: () => {
            // 正常情况下不会执行,因为服务端 redirect 会触发 onError
        },
        onError: (error) => {
            if (error instanceof Error && error.message.includes("NEXT_REDIRECT")) {
                toast.success("Workflow created", { id: "create-workflow" })
            } else {
                // 真正的错误
                console.error("Create workflow error:", error);
                toast.error("Failed to create workflow", { id: "create-workflow" });
            }
        }
    })
...

关键点说明

  1. redirect 的工作原理
    • Next.js 的 redirect 实际上会抛出一个特殊的错误(包含 NEXT_REDIRECT
    • 这个错误会被 React Query 的 onError 捕获
  2. 错误类型判断
    • 通过检查 error.message.includes("NEXT_REDIRECT") 区分是正常跳转还是真实错误
    • 如果是跳转,显示成功提示并关闭对话框
  3. 用户体验优化
    • 在 onError 中先显示成功 toast,再关闭对话框
    • 真实错误会显示错误提示并记录日志

2、Record类型

export type ProductType = {
    id: string | number;
    name: string;
    shortDescription: string;
    description: string;
    price: number;
    sizes: string[];
    colors: string[];
    // Record<string, string> 是一个内置的工具类型 他表示一个对象,其所有键都是字符串类型,所有值都是也都是字符串类型
    // 相当于 { [key: string]: string }
    images: Record<string, string>;
}
  • Record 是一个内置的工具类型
  • 其所有键都是字符串类型,所有值都是也都是字符串类型

3、数组对象遍历

{products.map((product) => (
  <ProductCard key={product.id} product={product} />
))}
  • 在使用 map() 方法渲染列表时,为每个元素提供一个唯一的 key 属性是 React 中一个非常重要且必须遵循的最佳实践。
  • 识别哪些项发生了变化:当列表发生变化(添加、删除、重新排序)时,React 可以通过 key 来高效地确定哪些元素需要更新、创建或销毁。
  • 提高性能:如果没有 key,React 在更新列表时可能需要重新渲染更多的元素,甚至可能导致整个列表被重新渲染,这会降低性能。有了稳定的 key,React 可以实现更精确的差分算法(diffing algorithm),只更新必要的部分。
  • 维持状态:对于包含状态(state)的列表项(如表单输入框),key 可以帮助 React 在列表顺序改变后,正确地将状态与对应的元素关联起来,避免状态错乱。

3、获取页面查询参数并修改其中一个参数,创建新的查询参数

这里的查询参数也就是URL中的?a=1&b=2这样的参数

const searchParams = useSearchParams();
// 获取URL中的PATH路径
const pathname = usePathname();

const selectedCategory = searchParams.get("category");

const handleChange = (value: string | null) => {
    // 根据当前页面的searchParams创建URLSearchParams
    const params = new URLSearchParams(searchParams);
    // 修改当前searchParams中category参数,其他参数保持不变
    params.set("category", value || "all");
    // router.push(`/?category=${value}`);
    // 使用searchParams生成URL查询链接
    router.push(`${pathname}?${params.toString()}`, {scroll: false});
}
  • 可以理解为动态的生成URL中的查询地址 比较经典的重新当前path路径重新组装query查询
  • router.push()函数中使用的scroll: false 在分类切换的时候页面不会回到顶部,而是保持当前位置
  • 获取查询参数:客户端组件使用useSearchParams()函数,page.tsx则使用export函数传参的形式export default async function HomePage({searchParams}: { searchParams: Promise<{ 参数名: string }> }),函数体中可以使用如下方式获取参数值:const category = (await searchParams).参数名;

4、useSate初始化一个对象多个参数

// 会提交product到购物车 需要存储 当前product选择的color、size参数
const [productTypes, setProductTypes] = useState({  // 这里商品状态存储的是一个对象,默认使用第一个size以及第一个color
    color: product.colors[0],
    size: product.sizes[0],
});

// 上面定义了当前商品的color、size的默认值
// 下面定义 修改商品的其中一个参数
const handleProductType = ({type, value}: { type: "color" | "size"; value: string }) => {
    // 上面定义当前商品有 两个属性 color 和 size
    // 使用prev传参 意思是保留之前的 参数对应的值 然后修改 当前type对应的值
    // 比如选择一个尺寸的时候 再去选一个颜色,正确的是应该尺寸会发生改变 ,并记录当前的颜色,而不是尺寸变为默认值
    setProductTypes((prev) => // prev 只是你给函数参数起的一个有意义的名字,React 会自动将当前状态值传递给它。
          // prev 确实是"变化之前的值",但它是"当前最新的变化之前的值" [type]: value 是最新的变化
          // ...prev - 展开操作符 相当于:{ color: prev.color, size: prev.size }    
          // [type]: value - 计算属性名,这里的方括号[]不是数组,而是计算属性名的语法
          // [type]: value 意思是:使用变量type的值作为属性名
          ({...prev, [type]: value}));
}
  • 这里要注重理解setProductTypes传参

修改size:

<select
  name="size"
  id="size"
  className={"ring ring-gray-300 rounded-md px-2 py-1"}
  // 点击size下拉框改变下拉框的值的时候 触发handleProductType函数 并传参
  onChange={(e) => handleProductType({type: "size", value: e.target.value})}
>
  {product.sizes.map((size) => (
    <option key={size} value={size}>{size.toUpperCase()}</option>
  ))}
</select>

修改color:

// 点击商品的颜色圆圈 触发handleProductType函数并传参
<div key={color} className={""}
     onClick={() => handleProductType({type: "color", value: color})}>
    {/*画个带颜色的实心圆*/}
    <div
        className={"w-[14px] h-[14px] rounded-full"}
        style={{backgroundColor: color}}
    />
</div>