案例
我从2020年开始一直使用next.js
做为我的前端SSR框架,使用@reduxjs/toolkit
做为全局状态管理器,使用next-redux-wrapper
协助next.js连接和合并redux中store数据并且保持不变,否则会导致数据重复渲染性能问题,但是最近发现一个很奇怪的问题:就是router.push
路由跳转的时候导致当前页面重复渲染问题!
换句话说也就是:我从PageA跳转到PageB,应该是只有PageB页面才会渲染,但是现在不但PageB渲染了,PageA也渲染了!!!
分析与操作
经过我使用排除代码的方式分析发现,原来每次路由导航的时候都会触发useSelector
这个方法,而这个方法是react-redux
插件的。
为什么会导航的时候会触发useSelector
呢?我又在网上搜索相关话题,终于被我找到一篇文章react-redux使用useSelector获取数据导致组件重复渲染的问题
通过redux中的hooks – useSelector 获取store中的数据时,只要store中的数据发生了改变,即使组件中并没有获取修改的数据,组件也会进行重新渲染。
也就是说造成重复渲染的原因是因为redux中store数据源变化了导致的。
于是我们使用文中提到的设置useSelector
的第2个参数,相同的时候返回true
会阻止重复渲染,不同的时候返回false
重新渲染的机制,我们使用了lodash
的isEqual
方法来判断。
注意:react-redux
自带的shallowEqual
方法是浅比较,所以是数组对象的情况下比较是有问题的,所以这里我们使用了lodash
的isEqual
方法来深度比较判断。
import _ from 'lodash'
const {userInfo, latestNews, likeUrls, reportUrls} = useSelector((state) => state.home, (_old, _new)=>{
console.log('old=',_old,',new=',_new)
return _.isEqual(_old, _new)
});
但是问题是,我只是做了一个路由跳转为什么会导致redux中store数据源前后不一致呢?
于是我加了个打印日志的代码,如下:
发现确实不一样了,而且主要集中在latestNews, likeUrls, reportUrls
这三个数据源上,而userInfo
不变。
这是为什么呢?于是我又看了一下next-redux-wrapper
文档,发现里面这段代码和一段描述:
State reconciliation during hydration
Each time when pages that have getStaticProps or getServerSideProps are opened by user the HYDRATE action will be dispatched. This may happen during initial page load and during regular page navigation. The payload of this action will contain the state at the moment of static generation or server side rendering, so your reducer must merge it with existing client state properly.
翻译成中文
水合过程中的状态调节
每次当用户打开具有getStaticProps或getServerSideProps的页面时,都会调度HYDRATE操作。这可能发生在初始页面加载期间和常规页面导航期间。此操作的有效负载将包含静态生成或服务器端呈现时的状态,因此您的reducer必须将其与现有客户端状态正确合并。
难道我没有正解合并客户端数据吗?
于是我又看了一下我的水合代码:
const initialState = {
userInfo: null,
latestNews: [],
likeUrls: [],
reportUrls: []
}
[HYDRATE]: (state, action) => {
console.log('HYDRATE action.payload=',action.payload);
return {
...state,
...action.payload.home,
};
},
感觉没啥问题啊!于是我又看看PageA的getServerSideProps
方法,发现有个区别:
userInfo
是通过getServerSideProps
Server服务端渲染得到的,而latestNews, likeUrls, reportUrls
是Client客户端渲染拿的!!!
心机之蛙一直摸你肚子!
结合上面官方给的文档,再加上这个发现,那就是说我没有正确水合服务端和客户端数据!
果然,我打印水合数据的时候,action.payload
是拿不到客户端的数据的,都是空的。
注意:虽然客户端通过接口获取的数据保存到了store中,但是水合的时候是拿不到的,水合的时候只能拿到服务端数据。
接下来我们只要正确水合客户端和服务端数据就可解决问题,所以我们修改下代码:
[HYDRATE]: (state, action) => {
console.log('HYDRATE action.payload=',action.payload);
const _merge = {
...state,
...action.payload.home,
}
console.log('state.latestNews=',state.latestNews)
// 这里的latestNews、likeUrls、reportUrls都是客户端数据,所以都要正确的水合到redux store中
if(state.latestNews){
_merge.latestNews = state.latestNews
}
if(state.likeUrls){
_merge.likeUrls = state.likeUrls
}
if(state.reportUrls){
_merge.reportUrls = state.reportUrls
}
return _merge
},
加好之后,我们再跳转页面发现水合数据成功了,useSelector
因为进行了深度比较判断store也是不变的,所以也就不会导致重复渲染了,终于成功了!!!
注意:[HYDRATE]
中state属性值必须与initialState
做相反判断,否则会报下面错。
Error: Hydration failed because the initial UI does not match what was rendered on the server.
解决:
只要if判断跟initialState
值相反即可,如下代码。
const initialState = {
v1: [],
v2: null,
v3: true
}
[HYDRATE]: (state, action) =>{
const _merge = {
...state,
...action.payload.home,
}
// 相反就是length>0
if(state.v1.length > 0){
_merge.v1 = state.v1
}
//相反就是v2不能为空
if(state.v2){
_merge.v2 = state.v2
}
//相反就是v3=false
if(!state.v3){
_merge.v3 = state.v3
}
return _merge
},
总结
1、SSR的水合思想个人觉得理解起来是有点难度的,毕竟之前做前端开发是没有遇到相同思想的问题
2、next-redux-wrapper文档其实也说明了很清楚,客户端数据要水合到store中,否则会有问题的。
3、触发水合的场景有:每次当用户打开具有getStaticProps
或getServerSideProps
的页面时,都会调度HYDRATE
操作。这可能发生在初始页面加载期间和常规页面导航期间。
4、当触发HYDRATE
时,只有服务端数据会保存到store中,客户端数据不会自动存储到store中,所以可以将前置的state的客户端数据主动合并到store中。
5、useSelector
方法的第2个参数通过判断是否重新渲染,true
时不重新渲染,false
时重新渲染。
6、next-redux-wrapper
star这么少,是不是跟它的思想难度(水合)有关???而且好多外网大佬都不建议使用任何一种状态管理器的。
7、通过分析可知,getServerSideProps服务端获取的数据,通过水合会存储到redux Store中,也会存储到客户端dom中
8、到目前为止SSR遇到的两个大问题,1、cookie,2、水合