假设我正在开发一个网站,这个网站支持很多种货币类型用来显示页面上的金额,然后 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
所以得做个权衡:
- 如果不想要“闪烁”现象,那么就只能每次都动态渲染
- 如果想要渲染性能,那就得接受“闪烁”现象