大家的阅读是我发帖的动力,本文首发于我的博客:deerblog.gu-nami.com/,欢迎大家来玩,转载请注明出处喵。

前言

Next.js 是一个广受欢迎的 React 服务端渲染(Server Side Rendering,SSR)框架。Next.js 的页面会先在服务端渲染一次,然后把结果传给浏览器,也就是客户端,再在客户端渲染一遍,并且运行客户端特有的逻辑。如果使用 Redux,一般情况下,在服务端渲染的时候,初始化了的 Redux 全局状态会被创建。然而服务器返回的是只有 HTML 标签的页面,客户端无法获得服务端 Redux 的状态,会引起水合错误,依赖 Redux 的组件渲染异常等影响体验的问题。我们需要同步服务端的 Redux 状态。

在 Page Router 模式下,Redux 同步状态已经有了成熟的解决方案,可以使用 next-redux-wrapper 完成,但是它并不适用于 App Router 模式的应用。这里参考 Redux 文档 的方法,给出一些个人在 Next.js 上同步 Redux 状态的小技寄巧,也只是一些个人做法,大佬们肯定有更优雅的方法的。

解决思路

先来看createStore的入参:

export declare function createStore<S, A extends Action, Ext, StateExt>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext

第二个参数preloadedStatestore的初始状态,只要在服务端 / 客户端中,都传入一样的内容,就可以创建两个状态一模一样的store

服务端渲染时,初始状态可在服务端组件(React Server Component,RSC)中直接查询获得。在客户端中,如果走网络请求,则在首次渲染中是拿不到服务端状态的。我们不妨把状态写入 HTML 中,带往客户端,然后客户端就可以同步服务端 Redux 的状态了。

我们先来以我博客的统计数据为例,这是目前的效果:

服务端渲染阶段初始化 Redux

先写一个创建 Redux 的代码(代码很大一部分是从老项目中迁移的,当时并没有用上 Redux Toolkit,请见谅):

import { thunk } from 'redux-thunk'
import { compose, createStore, applyMiddleware, StoreEnhancer, Store, EmptyObject } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension' import rootReducer, { combinedStateType } from './rootReducers' let storeEnhancers: StoreEnhancer
if (process.env.NODE_ENV === 'production') {
storeEnhancers = compose(applyMiddleware(thunk))
} else {
storeEnhancers = compose(composeWithDevTools(applyMiddleware(thunk)))
} export type reduxStoreType = Store<EmptyObject & combinedStateType> export const configureStore = (initialState = {}) => {
return createStore(rootReducer, initialState, storeEnhancers)
}

我们可以在Provider中完成初始化,毕竟 RSC 需要以组件的形式组织代码,而且后面useSelectoruseDispatch之类的钩子也需要它。这里ReduxProvider接收data参数作为初始状态。

import { configureStore, reduxStoreType } from "./index"
export default function ReduxProvider ({
children, data
}: {
children: React.ReactNode, data: any
}) {
const storeRef = useRef<reduxStoreType | null>(null)
const initialState = data
if (!storeRef.current) {
storeRef.current = configureStore(initialState)
}
return <Provide store={storeRef.current}>{children}</Provider>
}

导出一个获取方法,以便 React 组件外的代码可以使用 Redux。

// ...
let reduxStore: reduxStoreType | null = null
export default function ReduxProvider (/* ... */) {
// ...
storeRef.current = reduxStore = configureStore(initialState)
// ...
}
export const getStore = () => reduxStore

在 src/app/layout.tsx 中使用这个ReduxProvider。到这里,其实服务端初始化 Redux 已经完成了。

import { getArticleData } from "@/request/ssr/article";
import RootLayoutInner from "./layoutInner";
import ReduxProvider from "@/redux/reduxProvider"; export default async function RootLayout ({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const data = {
article: await getArticleData()
} return <html lang="en">
<head>
{/* ... */}
</head>
<body>
<ReduxProvider data={data}>
<RootLayoutInner >
{ children }
</RootLayoutInner>
</ReduxProvider>
</body>
</html>
}

getArticleData函数获取服务端的数据传进了ReduxProvider中,成为了 Redux 的初始状态,服务端已经可以渲染出具有 Redux 初始状态的页面了。

来看看现在的效果,emmmmmm... 似乎并不太好:

看看控制台... 给了我们几个水合错误。

Uncaught Error: Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "53" Client: "0"

可以发现服务端传回来的 HTML 是有数据的,但是客户端渲染时并没有拿到数据。接下来客户端就需要同步这个状态了。

客户端同步 Redux 状态

我们可以通过<script>标签,在客户端把初始状态挂到window.DATA_TO_SYNC_REDUX上。在客户端环境中,直接从这里取初始化的值。

// ...
const id = 'redux-initializer-json-data'
export default function ReduxProvider (/* ... */) {
children: React.ReactNode, data: any
}) {
const storeRef = useRef<reduxStoreType | null>(null)
let initialState
if (!BROWSER_ENV) {
initialState = data
} else {
try {
// @ts-ignore
initialState = JSON.parse(window.DATA_TO_SYNC_REDUX)
} catch (error) {
logger.log(error)
initialState = {}
}
} if (!storeRef.current) {
storeRef.current = reduxStore = configureStore(initialState)
} const text = `window.DATA_TO_SYNC_REDUX=\`${(JSON.stringify(data))}\``
BROWSER_ENV && setTimeout(() => {
document.getElementById(id)?.remove()
}, 100) return [
<script key={id} id={id}>{text}</script>,
<Provider key='redux-provider' store={storeRef.current}>{children}</Provider>
]
}

看起来到这里已经完成了,页面正常运行,但是一打开控制台,马上就给了我们一大堆报错,(虽然不管也行):

Uncaught Error: Text content does not match server-rendered HTML.
Warning: Text content did not match.
Server: "window.DATA_TO_SYNC_REDUX=`{&quot;article&quot;:{...}}`"
Client: "window.DATA_TO_SYNC_REDUX=`{"article":{...}}`"

看起来我们<script>的代码不知道为什么在服务端被转码了,在客户端第一次渲染渲染时又被转了回来,造成了水合错误。这里搜了一下也没有发现什么优雅的解决方法,我们就手动转码一下,绕开 HTML 的特殊字符。

const tokens: Record<string, string> = {
'!lt;': '<',
'!gt;': '>',
'!nbsp;': ' ',
'!amp;': '&',
'!quot;': '"'
}
const invTokens: Record<string, string> = {
'<': '!lt;',
'>': '!gt;',
' ': '!nbsp;',
'&': '!amp;',
'"': '!quot;'
}
export function pseudoHtml2Escape (htmlString: string) {
return htmlString.replace(/(!(lt|gt|nbsp|amp|quot);)/ig, function (t: string) {
return tokens[t]
})
}
export function escape2PseudoHtml (escapeString: string) {
const res = escapeString.replace(/(<|>| |&|")/g, function (_, t: string) {
return invTokens[t]
})
return res
} export default function ReduxProvider (/* ... */) {
// ...
initialState = JSON.parse(pseudoHtml2Escape(window.DATA_TO_SYNC_REDUX))
// ...
const text = `window.DATA_TO_SYNC_REDUX=\`${escape2PseudoHtml(JSON.stringify(data))}\``
// ...
}

到这里同步服务端状态已经完成了。来看看最终效果:

结语

本文简单地实现了 Next.js App Router 下客户端同步服务端 Redux 状态的方法。其中状态的传递主要通过 HTML 代码来进行,总感觉是不是不太优雅。大体流程如下所示:

graph LR
subgraph "服务端渲染"
RootLayout -->|传入数据|B[ReduxProvider]-->|初始化|Redux
B -->|传入初始值| script
end
subgraph "浏览器"
script -->|记录初始值| window
ReduxProvider --> |获取数据|window
ReduxProvider-->|初始化|A[Redux]
end

Next App Router 模式下,如何同步服务端 Redux 初始状态?的更多相关文章

  1. win10操作系统下oracle11g客户端/服务端的下载安装配置卸载总结

    win10操作系统下oracle11g客户端/服务端的下载安装配置卸载总结 一:前提 注意:现在有两种安装的方式 1. oracle11g服务端(64位)+oracle客户端(32位)+plsql(3 ...

  2. linux下svn(subversion)服务端添加工程及配置权限

    linux下svn(subversion)服务端添加工程及配置权限 转载请注明源地址:http://www.cnblogs.com/funnyzpc/p/9010507.html 此篇我只是将所做过的 ...

  3. Spring Boot框架下实现Excel服务端导入导出

    Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.今天 ...

  4. 线程同步——用户模式下线程同步——Interlocked实现线程同步

    线程同步分为用户模式下的线程同步和内核对象的线程同步. 当然用户模式下的线程同步实现速度比内核模式下快,但是功能也有局 //1.利用原子访问: Interlocked系列函数,关于Interlocke ...

  5. 微信app支付android客户端以及.net服务端实现

    由于公司运营需要,需要在客户端(android/ios)增加微信以及支付宝支付,在调用微信app支付时遇到一些问题,也算是一些踩过的坑,记录下来 ,希望能对.net开发者服务端网站更快的集成微信app ...

  6. Linux下搭建svn服务端

    安装 使用yum安装非常简单: yum -y install subversion (压缩包安装比这麻烦的多) Tortoise本是window下客户端工具,但也可以建仓库,作为服务端.Linux只有 ...

  7. Rsync 实现服务器文件的同步——服务端的安装配置

    一.安装rsync 直接使用yum命令进行安装即可. yum -y install rsync 二.配置文件 网上大多教程都说安装是默认没有配置文件的,但是经过我的尝试,yum安装下默认是有配置文件的 ...

  8. windows下搭建svn服务端、客户端

    1.安装SVN服务器subversion以及客户端TortoiseSVN,在网上下载windows版的subversion,TortoiseSVN并安装,比如我的服务端安装在了D:\Program F ...

  9. CentOS7下安装SVN服务端

    CentOS7下安装SVN服务 1. yum命令即可方便的完成安装# sudo yum install subversion 测试安装是否成功:# svnserve --version 更改svn的默 ...

  10. spring-oauth-server实践:使用授权方式四:client_credentials 模式的客户端和服务端交互

    spring-oauth-server入门(1-11)使用授权方式四:client_credentials 模式的客戶端 一.客户端逻辑 1.界面入口(credentials_access_token ...

随机推荐

  1. RabbitMQ(六)——路由模式

    RabbitMQ系列 RabbitMQ(一)--简介 RabbitMQ(二)--模式类型 RabbitMQ(三)--简单模式 RabbitMQ(四)--工作队列模式 RabbitMQ(五)--发布订阅 ...

  2. 程序员转型AI:行业分析

    系列目录 1.程序员转型AI:行业分析 2.程序员转型AI:转型计划 3.程序员转型AI:落地实践 4.程序员转型AI:展望未来 一.背景分析 进入2025年,AI已经爆发式增长,且进入实际商业变现阶 ...

  3. ABB喷涂机器人控制柜维护保养

    ABB喷涂机器人的管理与维护保养目的是减少机器人的故障率和停机时间,充分利用机器人这一生产要素,最大限度地提高产效率.喷涂机器人维修与保养在企业生产中尤为重要,直接影响到系统的寿命,必须精心维护. A ...

  4. Go1.24版本终于来了!各位开发者,准备好迎接这些激动人心的新功能了吗?让我们一起来探讨下Go1.24中有哪些精彩的亮点?

    前言 Gopher们,Go 1.24.0 正式发布了!与 Go 1.23.0 相比,这个版本带来了众多改进.让我们一同看看 Go 1.24.0 都有哪些新变化吧! 在 Windows 下,请在 htt ...

  5. 【Python】将网格数据写入到VTK文件

    1. vtk 文件格式 根据官网进行总结 vtk文件组成:5个部分. 第一部分,第一行:表明文件版本.写"# vtk DataFile Version 2.0"就行 第二部分,第二 ...

  6. 应急响应靶场之vulntarget-n

    vulntarget-n 用户名密码:root/Vulntarget@123 一.分析history命令 1.先将历史命令导出 history > 1.txt 2.分析history 1)篡改网 ...

  7. 记录网站从http升级到https遇到的问题

    1.静态资源(js.css)引入问题 在使用http是之后,如果你的站点出现引入外部的js.css等,你需要修改你的资源引入,cdn的话可以写成://cdn.bootscdn.com/jquery.m ...

  8. 解决 /usr/bin/env: php: No such file or directory 问题

    前言 composer 报错 env: php: No such file or directory 找不到 php 的执行文件,原因是脚本文件 env 会通过 $PATH 所指定的路径去寻找 php ...

  9. Git 命令使用体验的神器 -- tig

    tig, 就是把 Git 这个单词倒过来念, 它是一个命令行工具, 日常使用中我用它来取代 Git 最高频的几个操作, 如 git log, git diff 以及 git blame等, 使用常见安 ...

  10. Linux下使用sz/rz命令从服务器下载或上传文件

    简介 Linux中rz命令和sz命令都可用于文件传输,而rz命令主要用于文件的上传,sz命令用于从Linux服务器下载文件到本地. 安装 yum安装 yum -y install lrzsz 源码安装 ...