Next.js 初学者常犯的 29 个错误及如何避免

352 阅读8分钟

Next.js 作为一个强大的 React 框架,为开发者提供了许多便利。然而,对于初学者来说,在使用过程中难免会遇到一些陷阱。本文将详细介绍 29 个 Next.js 初学者常犯的错误,并提供相应的解决方案,帮助你更好地掌握这个框架。

1. 过高层级使用 "use client"

在 Next.js 中,我们有服务器组件(Server Components)和客户端组件(Client Components)的概念。服务器组件可以直接标记为 async,并在其中使用 fetch 请求第三方 API 获取数据。而客户端组件主要用于处理交互、状态管理、用户操作和事件监听等。 错误做法是直接将外部的服务器组件标记为客户端组件,这可能导致内部的服务器组件报错,特别是当内部服务器组件被标记为 async 时。 正确做法是在组件树中尽可能低的层级使用 "use client" 指令,只将真正需要客户端交互的组件标记为客户端组件。

2. 没有为 "use client" 重构代码

即使是很小的组件,如果可以拆分成客户端组件,也应该进行拆分。这样可以优化性能,减少不必要的客户端渲染。

3. 误以为没有 "use client" 的组件就是服务器组件

事实上,一个组件是否为服务器组件取决于其父组件。如果父组件是客户端组件,那么子组件默认也是客户端组件,除非明确使用了服务器组件的导入方式。

4. 误以为将服务器组件包裹在客户端组件中就会变成客户端组件

正确的做法是通过 children 或 props 将服务器组件传递给客户端组件,而不是直接在客户端组件中导入服务器组件。这种方式允许我们在客户端组件中使用服务器组件的渲染结果。

// 正确做法
function ClientComponent({ children }) {
  return <div>{children}</div>;
}

function ParentComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

5. 在服务器组件中使用状态管理

服务器组件每次请求都会重新渲染,不会保持状态。因此,在服务器组件中使用 Context API、Zustand 或 Redux 等状态管理工具是没有意义的。状态管理应该只在客户端组件中使用。

6. 使用 'use server' 创建服务器组件

'use server' 指令用于创建服务器操作(Server Actions),而不是服务器组件。服务器组件是默认的,不需要特殊标记。

7. 不小心将敏感数据从服务器泄露到客户端

在将数据从服务器组件传递到客户端组件时,要特别注意不要泄露敏感信息。如果需要传递敏感数据,应该先进行加密或者只传递必要的信息。 如果想确保某个服务器组件不被客户端组件导入,可以使用 'server-only' 包: import 'server-only';

8. 认为客户端组件只在客户端运行

实际上,客户端组件在服务器端也会执行一次,用于生成初始 HTML。这就是为什么在客户端组件中的 console.log 也会在服务器控制台中看到。但是,客户端组件的交互功能和状态更新只会在浏览器中执行。

9. 错误使用浏览器 API

在客户端组件中使用 localStorage 等浏览器 API 时,需要注意服务器端渲染的问题。可以使用以下方法之一:

使用条件判断:

if (typeof window !== "undefined") {
  // 使用浏览器 API
}

在 useEffect 中使用:

useEffect(() => {
  // 使用浏览器 API
}, []);

使用动态导入并禁用服务器端渲染:

const DynamicComponent = dynamic(() => import("@/components/Component"), { ssrfalse });

10. 遇到水合(Hydration)错误

水合错误通常发生在服务器渲染的内容与客户端渲染的内容不匹配时。常见原因包括:

使用了依赖客户端状态的内容(如 localStorage) HTML 标签嵌套规则错误

解决方法:

使用 suppressHydrationWarning 属性忽略特定元素的水合警告 确保服务器端和客户端渲染的内容一致 检查 HTML 结构的正确性

11. 错误处理第三方组件

当使用没有正确标记 "use client" 的第三方组件时,可以创建一个包装组件:

'use client';

import ThirdPartyComponent from 'third-party-library';

export default function WrappedComponent(props) {
  return <ThirdPartyComponent {...props} />;
}

对于使用了浏览器 API 的第三方组件,可以使用动态导入:

const DynamicComponent = dynamic(() => import('@/components/ThirdPartyWrapper'), { ssrfalse });

12. 为获取数据而使用路由处理程序

在 Next.js 13 之后,大多数数据获取操作应该在服务器组件中直接进行,而不是使用 API 路由。对于数据修改操作(POST、DELETE、PUT 等),应该使用服务器操作(Server Actions)。 路由处理程序(API 路由)主要用于处理 webhooks 等场景,例如处理第三方服务的回调请求。

13. 担心在不同地方获取相同数据

Next.js 的数据获取机制有内置的缓存功能。你可以在不同的组件中多次使用相同的 fetch 请求,而不必担心性能问题。Next.js 会自动处理缓存,避免重复请求。 如果使用 ORM 或数据库查询,可以使用 Next.js 的 unstable_cache 函数来手动缓存结果:

import { unstable_cache } from 'next/cache';

const getCachedData = unstable_cache(
  async () => {
    // 数据获取逻辑
  },
  ['cache-key'],
  { revalidate60 } // 缓存 60 秒
);

14. 数据获取时出现"瀑布效应"

当多个数据请求按顺序处理时,可能会出现请求瀑布,影响性能。为了避免这种情况,如果几个请求间没有相互依赖的关系,可以使用 Promise.allPromise.allSettled 并行处理多个请求:

const [data1, data2] = await Promise.all([
  fetch('/api/data1').then(res => res.json()),
  fetch('/api/data2').then(res => res.json())
]);

15. 向服务器组件或路由处理程序提交数据

应该使用服务器操作(Server Actions)来处理数据提交,而不是传统的表单提交或 API 调用。服务器操作可以直接在服务器上执行,无需额外的 API 路由。

export default function Form() {
  async function handleSubmit(formData) {
    'use server';
    const title = formData.get('title');
    // 处理数据提交
  }

  return (
    <form action={handleSubmit}>
      <input name="title" />
      <button type="submit">Submit</button>
    </form>
  );
}

16. 页面没有及时反映数据变化

Next.js 会缓存服务器组件的渲染结果。在进行数据修改后,需要使用 revalidatePathrevalidateTag 来使缓存失效:

import { revalidatePath } from 'next/cache';

async function updateData() {
  await db.update();
  revalidatePath('/data');
}

17. 认为服务器操作只能在服务器组件中使用

服务器操作也可以在客户端组件中使用。可以配合 useTransition 来提供更好的用户体验:

'use client';

import { useTransition } from 'react';

export default function ClientComponent() {
  const [isPending, startTransition] = useTransition();

  async function handleClick() {
    startTransition(async () => {
      await serverAction();
    });
  }

  return <button onClick={handleClick}>
    {isPending ? 'Loading...' : 'Click me'}
  </button>;
}

18. 忘记验证和保护服务器操作

服务器操作需要进行适当的数据验证和身份认证。可以使用 Zod 等库进行数据验证,并在操作执行前检查用户权限。

import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1).max(100),
});

async function serverAction(formData) {
  const result = schema.safeParse({
    title: formData.get('title'),
  });

  if (!result.success) {
    // 处理验证错误
    return;
  }

  // 执行操作
}

19. 误解 'use server' 的作用

'use server' 指令用于标记服务器操作,而不是用来确保代码只在服务器上运行。它会创建一个可以从客户端调用的 API 端点。

20. 误解动态路由(params 和 searchParams)

在 Next.js 中,动态路由参数通过 params 获取,而查询参数通过 searchParams 获取。这两者只能在页面组件中直接访问。

export default function Page({ params, searchParams }) {
  // params: { slug: 'post-1' }
  // searchParams: { sort: 'asc' }
}

21. 错误处理 searchParams

服务端组件 page 运行在服务器上,所以要通过一次 request 才能收到新的 searchParams,在客户端组件中,可以使用 useSearchParams hook 来获取和更新查询参数:

'use client';

import { useSearchParams } from 'next/navigation';

export default function ClientComponent() {
  const searchParams = useSearchParams();
  const sort = searchParams.get('sort');

  return <div>Sorted by: {sort}</div>;
}

22. 忘记处理加载状态

Next.js 提供了自动的加载状态处理机制。你可以创建一个与页面组件同级的 loading.js 文件,它会在页面加载时自动显示:

// app/dashboard/loading.js
export default function Loading() {
  return <div>Loading...</div>;
}

23. Suspense 使用不当

应该将 Suspense 放置在合适的位置,只包裹需要异步加载的内容:

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <AsyncComponent />
      </Suspense>
    </div>
  );
}

24. Suspense 放置位置不正确

Suspense 应该放在比数据获取组件更高的层级,以确保正确捕获异步操作。

25. 忘记为 Suspense 添加 key 属性

当 Suspense 包裹的内容可能会改变时,应该为 Suspense 添加 key 属性,以确保在内容变化时重新触发加载状态:

<Suspense key={id} fallback={<div>Loading...</div>}>
  <AsyncComponent id={id} />
</Suspense>

26. 意外地将页面排除在静态渲染之外

某些因素会导致页面变为动态渲染,如使用动态路由参数、searchParams、cookies 或某些 headers。要优化性能,应该仔细考虑哪些页面需要动态渲染,哪些可以保持静态。

27. 硬编码密钥

应该使用环境变量来存储敏感信息,而不是直接在代码中硬编码。Next.js 默认支持 .env.local 文件: DATABASE_URL=your_database_url_here 在代码中可以通过 process.env.DATABASE_URL 访问。

28. 没有区分客户端和服务器工具函数

创建单独的客户端和服务器工具函数文件夹,以避免在错误的环境中使用不兼容的 API。

29. 在 try/catch 块中使用 redirect()

redirect() 函数通过抛出错误来工作,因此不应该在 try/catch 块中使用。正确的用法是:

import { redirect } from 'next/navigation';

export default function Page() {
  redirect('/new-page');
}

结论 通过了解和避免这些常见错误,你可以更有效地使用 Next.js,创建高性能、可维护的 Web 应用。