Skip to content
返回

Next.js 客户端初始值和服务器组件初始值不一致导致的闪烁问题解决方案

假设我正在开发一个网站,这个网站支持很多种货币类型用来显示页面上的金额,然后 URL 里有个查询参数可以用来指定货币。

为了在多个组件之间共享状态,所以我将货币存放进了 store。以 Zustand 为例:

import { create } from 'zustand';

export const useCurStore = create(set => {
  return {
    cur: 'USD', // 默认使用 USD
  }
})

然后,在服务端组件内,我会读取 searchParams 里的货币值并用一个客户端组件写入 store,用另一个客户端组件读取货币值:

// 服务器端页面组件
export default async function Home({ searchParams }) {
  const { cur: curInSearchParams } = await searchParams

  return <div>
    <SetCur cur={curInSearchParams} />
    <ShowCur/>
</div>
}
// 用于将货币值写入 store 的客户端组件
'use client'
export function SetCur({cur}) {
  useEffect(() => {
    useCurStore.setState({cur})
  }, [])
  return null
}
// 用于显示货币值的客户端组件
'use client'
export function ShowCur({curInSearchParams}) {
    return <div>{useCurStore(state => state.cur) ?? curInSearchParams}</div>
}

然后就会发现:会出现闪烁现象。当访问 /?cur=CNY 的时候,会看到网页上先是显示 USD,然后会立刻变成 CNY。确认了一下 HTML,发现是 <div>USD</div>

如何解决?

AI 绕了一大圈都没解决,最后我想到了一个办法。

本质上还是因为使用了 store 里的默认值,但是在服务器端渲染的时候,我要以 search params 里的值为准,不能使用 store 里的默认值。顺着这个思路,有了以下解决方案。

1. store 里使用 null 作为默认值

import { create } from 'zustand';

export const useCurStore = create(set => {
  return {
    cur: null, // null 表示初始值得从别的地方确定,例如 search params、cookies、params 等
  }
})

2. 读取 store 里的值时,如果读到了 null,则使用从 search params 读出来的值

其余部分不变,仅在读取货币值时,接收服务器组件传过来的 search params 里的货币值,并作为 store 中 null 的替代值:

// 用于显示货币值的客户端组件
export function ShowCur({curInSearchParams}) {
    return <div>{useCurStore(state => state.cur) ?? curInSearchParams}</div>
}

大功告成。

此方案的弊端

这个方案有一个弊端,就是我们需要把 search params 里的值传递给每一个用到了 store 的客户端组件,代码看上去可能会像这样子:

export default async function Home({ searchParams }) {
  const { cur: curInSearchParams } = await searchParams

  return <div>
    <PricesTable defaultCur={curInSearchParams} />
    <PricesChart defaultCur={curInSearchParams} />
  </div>
}

但是也没空去细想别的方案了,所以就这样吧。

后续

然后发现如果在服务器端读取了 searchParams 会导致这个页面无法启用 ISR,也就是每次都会动态渲染,具体表现为 next preview 时会报错:

[Error]: Dynamic server usage: Route / couldn't be
  rendered statically because it used ``await searchParams`, `searchParams.then`,
  or similar`. See more info here:
  https://nextjs.org/docs/messages/dynamic-server-error

所以得做个权衡:


分享这篇文章:

上一篇
深度 AI 编程一个月之后的体验报告。
下一篇
Cloudflare Workers + D1 数据库踩坑记录