在上一篇文章中(Next.js 新手容易犯的错误 | All about "use client" (1)),我们探讨了 Next.js 中服务端组件和客户端组件的运行机制以及常见的使用误区。
这篇文章将作为续集,进一步分析更多开发中容易遇到的问题,并提供实用的解决方案,帮助你在项目中更好地规避这些坑点。
5. 任何状态管理工具都需要在客户端组件中使用
比如下面这个例子:
"use client"
import { createContext } from "react";
export const ThemeContext = createContext(null);
export default function ThemeContextProvider({ children }: { children: React.ReactNode }) {
return (
<ThemeContext.Provider value="light">
{children}
</ThemeContext.Provider>
);
}
虽然代码中没有react的钩子,但是我们仍然要使用"use client"。
原因是所有状态管理解决方案,比如 Context API 或 Zustand,都只能在客户端使用。
这是因为状态管理的目标本质上是为了追踪一些内容,但是我们无法在服务端追踪这些内容。对于服务端来说,它始终是一个请求-响应的循环,也就是服务端收到一个请求,处理后再返回一个响应,返回响应之后不会再追踪这个请求的任何内容。但是用户在网站交互的过程中,浏览器是一直处于活跃状态的。所以这些状态管理的部分只能放在客户端处理,不能在服务端中使用。
6. 使用 use server
指令来标记服务端组件
比如对于product组件,我们使用了async,这只能在服务端组件中运行。
'use server' // 这是不对的
export default async function Product() {
const res = await fetch("https://fakestoreapi.com/products/3");
const product = await res.json();
return (
<section className="flex flex-col items-center gap-2">
<p>{product.title}</p>
</section>
);
}
于是有些开发者就会显式地在顶部声明"use server"。
但是在nextjs的app路由中,所有的内容默认就是服务端组件,所以不需要使用"use server",遵循默认的循环就可以。
7. 敏感信息暴露在客户端
比如这个例子,我们从数据库中获取了用户数据,并且还包括了一个密码。这个数据将会作为props传递给其他的组件。
import FavoriteBtn from "@components/favorite-btn";
import Product from "@components/product";
export default function Home() {
const user = {
email: "john@gmail.com",
password: "123456", // 敏感信息(应避免直接传递)
};
return (
<main className="flex flex-col items-center mt-32 gap-12 text-xl">
<h1 className="text-5xl font-semibold">My Store</h1>
<Product />
<FavoriteBtn user={user} /> {/* 将用户数据作为 props 传递 */}
</main>
);
}
如果,接收数据的这个组件正好是一个客户端的组件,那么这些数据在客户端中就会被看到。
"use client";
import { HeartFilledIcon, HeartIcon } from "@radix-ui/react-icons";
import { useState } from "react";
export default function FavoriteBtn({ user }: { user: { email: string; password: string } }) {
console.log(user); // 在客户端打印用户数据(可能导致敏感信息泄露)
const [isFavorite, setIsFavorite] = useState(false);
return (
<button
onClick={() => setIsFavorite((prev) => !prev)}
className="p-2 rounded-md bg-zinc-300"
>
{isFavorite ? <HeartFilledIcon /> : <HeartIcon />}
</button>
);
}
当然我们不希望用户的密码泄露给客户端,我们需要避免直接去传递敏感数据。Next.js 自动处理了服务端和客户端的网络边界,要注意检查客户端组件是不是在不必要的情况下接收了敏感数据。
8. 客户端组件在页面初始渲染的时候也会在服务端运行一次
还是之前的例子,page页面中,product组件时服务端组件,favoriteBtn组件时客户端组件。
// page.tsx
import FavoriteBtn from "@/components/favorite-btn";
import Product from "@/components/product";
export default function Mistake1() {
return (
<main className="flex flex-col items-center mt-32 gap-12 text-xl">
<h1 className="text-5xl font-semibold">My Store</h1>
<Product />
<FavoriteBtn />
</main>
);
}
在product和favoriteBtn组件中分别打印出来 'hi this is server component' 和 'hi this is client component'。
// product.tsx
export default async function Product() {
console.log('hi this is server component')
const res = await fetch("https://fakestoreapi.com/products/3");
const product = await res.json();
return (
<section className="flex flex-col items-center gap-2">
<p>{product.title}</p>
</section>
);
}
// favorite-btn.tsx
'use client'
import { HeartFilledIcon, HeartIcon } from "@radix-ui/react-icons";
import { useState } from "react";
export default function FavoriteBtn() {
console.log('hi this is client component')
const [isFavorite, setIsFavorite] = useState(false);
return (
<button
onClick={() => setIsFavorite((prev) => !prev)}
className="p-2 rounded-md bg-zinc-300"
>
{isFavorite ? <HeartFilledIcon /> : <HeartIcon />}
</button>
);
}
运行的时候,服务端组件在服务端运行,所以在浏览器的控制台中可以看到server的标签:
但是这里打印出来的内容,在终端中也会打印出来,因为服务端的代码输出的内容本身就会在终端中显示:
但是,客户端的代码,在终端中也显示出来了。这就是因为客户端组件,在页面初始渲染的时候,也会在服务端运行一次用于预渲染HTML,激活了之后才能在客户端交互。
总结下来就是:
- 服务端组件:
-
- 只在服务端运行。
- 生成 HTML 并将其发送到客户端。
- 客户端组件:
-
- 会在客户端运行,但在页面的初始渲染时,也会在服务端运行一次(用于预渲染 HTML)。
- 激活(hydrate)后才能在客户端进行交互。
- 日志行为:
-
- 服务端组件的日志出现在服务端的终端中(浏览器中会有server标签)。
- 客户端组件的日志会同时出现在服务端(初次渲染时)和客户端(激活后运行)中。
9. 正确使用和浏览器相关的API
假设我们有一个收藏按钮组件(FavoriteButton
),其功能是记录用户是否收藏了某个项目,并将状态保存在 localStorage
中。
"use client";
export default function FavoriteButton() {
const isFavorite = localStorage.getItem("isFavorite"); // 直接访问 localStorage
return (
<button>
{isFavorite ? "Unfavorite" : "Favorite"}
</button>
);
}
这时候,客户端组件在服务端预渲染的时候,会出现报错:ReferenceError: localStorage is not defined。
这是因为localStorage是浏览器的API,当组件在服务端运行的时候,localStorage并不存在。
解决的方法有三种:
1是在使用API之前,先检查代码是否在客户端环境中运行:
"use client";
export default function FavoriteButton() {
let isFavorite = false;
if (typeof window !== "undefined") {
isFavorite = localStorage.getItem("isFavorite") === "true";
}
return (
<button>
{isFavorite ? "Unfavorite" : "Favorite"}
</button>
);
}
其中typeof window !== "undefined"
检查 window
是否可用,仅在客户端环境中访问 localStorage
。
2是使用useEffect,因为useEffect只会在客户端运行:
"use client";
import { useEffect, useState } from "react";
export default function FavoriteButton() {
const [isFavorite, setIsFavorite] = useState(false);
useEffect(() => {
const favorite = localStorage.getItem("isFavorite") === "true";
setIsFavorite(favorite);
}, []);
return (
<button>
{isFavorite ? "Unfavorite" : "Favorite"}
</button>
);
}
3是使用next/dynamic模块进行动态导入,并禁用服务器渲染:
import dynamic from "next/dynamic";
const FavoriteButton = dynamic(() => import("./FavoriteButton"), { ssr: false });
export default function Home() {
return (
<main>
<h1>My Store</h1>
<FavoriteButton />
</main>
);
}
禁用服务器渲染后,组件只会在客户端运行,但是可能会略微延迟组件的加载时间。
理解服务端组件和客户端组件的运行机制,并合理使用 use client,可以有效避免常见问题,提高开发效率。
希望本文的总结能为你提供思路,让项目开发更加顺畅。