Next.js 切换动态路由后,为什么页面还保留着上一个分类的数据?

Next.js 切换动态路由后,为什么页面还保留着上一个分类的数据?

最近在开发一个文章分类页时遇到了这样的问题:

用户在 /articles/deal-news/fashion 页面底部点击 Related topics,进入另一个分类后,页面标题已经变成了新分类,但文章卡片的内容和数量仍然属于上一个分类。

手动刷新页面后,数据又恢复正常。

这是一个典型的 React 状态与 Next.js 动态路由组件复用问题。

问题代码

分类页面接收服务端返回的数据,并将其保存到本地状态中:

javascript 复制代码
const CategoryPage = ({
  category,
  initialPosts,
  totalPages,
}: CategoryPageProps) => {
  const [page, setPage] = useState(1);
  const [posts, setPosts] = useState(initialPosts);

  return (
    <main>
      <h1>{category.name}</h1>

      {posts.map((post) => (
        <ArticleCard key={post.slug} post={post} />
      ))}
    </main>
  );
};

Related topics 使用 Next.js 的 Link 进行跳转:

ini 复制代码
<Link href="/articles/deal-news/beauty">
  Beauty
</Link>

从表面上看没有问题:新路由会获取新的分类数据,然后将新的 categoryinitialPosts 传给页面。

但实际结果却是:

  • category.name 更新了
  • initialPosts 更新了
  • posts 没有更新
  • page 仍然保留旧值

为什么会发生?

假设用户进行了下面的跳转:

bash 复制代码
/articles/deal-news/fashion
              ↓
/articles/deal-news/beauty

虽然 URL 中的分类参数发生了变化,但这两个地址使用的是同一个动态路由文件:

css 复制代码
pages/articles/[articleType]/[identifier].tsx

对 React 来说,跳转前后渲染的组件结构仍然是:

xml 复制代码
<CategoryPage />

组件类型没有变化,组件所处的位置也没有变化。因此 React 会复用当前的 CategoryPage 实例,而不是先卸载再重新创建。

于是,新的 props 会传进来,但组件内部已有的 state 会被保留。

useState 的初始值只使用一次

问题的关键在这一行:

scss 复制代码
const [posts, setPosts] = useState(initialPosts);

很多人会下意识地认为:

initialPosts 更新时,posts 也会跟着更新。

实际上不会。

useState(initialPosts) 中的 initialPosts 只在组件第一次挂载时使用。之后即使 props 发生变化,React 也不会重新初始化 state。

可以用一个更小的例子说明:

typescript 复制代码
const Counter = ({ initialCount }: { initialCount: number }) => {
  const [count] = useState(initialCount);

  return (
    <div>
      <p>属性:{initialCount}</p>
      <p>状态:{count}</p>
    </div>
  );
};

第一次渲染:

ini 复制代码
<Counter initialCount={10} />

页面显示:

复制代码
属性:10
状态:10

随后父组件传入新的值:

ini 复制代码
<Counter initialCount={20} />

页面会变成:

复制代码
属性:20
状态:10

initialCount 已经更新,但 count 依然保留第一次初始化时的值。

文章分类页遇到的就是同一个问题。

为什么刷新页面后正常?

刷新页面时,React 应用会重新启动,CategoryPage 也会重新挂载。

此时:

scss 复制代码
useState(initialPosts);

接收到的是当前分类的文章数据,所以页面显示正常。

这也是判断此类问题的一个常见线索:

客户端跳转后数据错误,但刷新后恢复正常,通常需要检查组件状态是否在路由之间被复用。

最小修复:使用 key 标识不同分类

这个页面中的文章列表和页码都完全属于当前分类。因此分类改变时,让整个 CategoryPage 重新挂载是合理的。

ini 复制代码
const ArticleIdentifierPage = (props: PageProps) =>
  props.pageType === "category" ? (
    <CategoryPage
      key={props.category.tagId}
      {...props}
    />
  ) : (
    <BlogDetailPage {...props} />
  );

当分类从 fashion 变成 beauty 时,组件的 key 也会发生变化:

ini 复制代码
key="fashion"
      ↓
key="beauty"

React 会把它们识别为两个不同的组件实例:

  1. 卸载旧的 CategoryPage
  2. 创建新的 CategoryPage
  3. 使用新分类数据初始化 posts
  4. page 重新初始化为 1
  5. 将加载状态等其他本地状态一起重置

这样,新分类页面就不会继续使用上一个分类的文章数据。

也可以使用 useEffect 吗?

可以,例如:

scss 复制代码
useEffect(() => {
  setPage(1);
  setPosts(initialPosts);
}, [category.tagId, initialPosts]);

但是这种方案需要手动列出所有应该重置的状态。

当前组件除了 postspage,还有加载状态、引用或未来可能新增的分类相关状态。如果漏掉其中一个,切换分类时仍然可能残留旧数据。

另外,useEffect 是在一次渲染之后执行的,页面可能短暂出现"新标题配旧卡片"的状态。

如果需求就是"分类改变后,这是一个全新的分类页面",使用 key 更直接:

ini 复制代码
<CategoryPage key={category.tagId} />

如果需求是"只同步某一个状态,同时保留用户在页面上的其他操作",则更适合使用 useEffect

不要把所有 props 都复制到 state

这类 Bug 背后还有一个常见设计问题:把 props 无条件复制到 state。

例如,如果文章列表不需要在客户端修改,可以直接使用 props:

javascript 复制代码
const CategoryPage = ({ initialPosts }: CategoryPageProps) => (
  <div>
    {initialPosts.map((post) => (
      <ArticleCard key={post.slug} post={post} />
    ))}
  </div>
);

这样 props 更新时,页面自然会重新渲染,也就不存在同步问题。

但如果页面支持分页、筛选或客户端请求,就需要本地状态:

scss 复制代码
const [posts, setPosts] = useState(initialPosts);

一旦选择将 props 作为 state 的初始值,就需要明确回答一个问题:

当数据所属的业务对象发生变化时,这些 state 应该保留,还是重新初始化?

在这个案例中,"业务对象"就是分类。分类改变,文章列表和页码都应该重置,因此使用分类 ID 作为 key 是合适的。

总结

这个 Bug 不是接口返回错误,也不是 Next.js 缓存导致的,而是下面三个条件共同造成的:

  1. 不同分类使用同一个动态路由组件
  2. Next.js 客户端跳转时复用了组件实例
  3. useState(initialPosts) 只在首次挂载时读取初始数据

最终修复只有一行:

ini 复制代码
<CategoryPage key={props.category.tagId} {...props} />

理解这行代码的重点,不是"加一个 key 可以刷新页面",而是:

key 定义了 React 眼中的组件身份。身份改变时,旧状态就不应该继续属于新页面。

相关推荐
程序员鱼皮12 分钟前
爆火的《牛来》模型正式发布!DeepSeek 的排名又下降了。。
前端·后端·ai编程
MocMoc17 分钟前
从零落地全栈 Monorepo:Turbo + npm workspaces 实战(附一个记账小应用)
前端
灯澜忆梦19 分钟前
【基于GO的Web开发15】gin路由和路由组
前端·后端·golang·gin
Southern Wind25 分钟前
从一段文字到一部动态漫:如何打造全流程分镜创作工作台 StoryCanvas AI
前端·javascript·vue.js·人工智能
烂蜻蜓31 分钟前
Flask入门教程(七):请求与响应——Web应用的核心数据流
前端·python·flask
爱勇宝38 分钟前
从前端到全栈,我最后学会的是:先做产品
前端·后端
console.log('npc')41 分钟前
Grill-Me 技能使用教程
前端·大模型·产品·skill·需求
真空回流焊炉1 小时前
数字功率芯片真空共晶设备实操教程与要点解析
前端·人工智能