Next.js-页面重复渲染引出的水合问题

案例

我从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重新渲染的机制,我们使用了lodashisEqual方法来判断。

注意:react-redux自带的shallowEqual方法是浅比较,所以是数组对象的情况下比较是有问题的,所以这里我们使用了lodashisEqual方法来深度比较判断。

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不变。

image.png

这是为什么呢?于是我又看了一下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是通过getServerSidePropsServer服务端渲染得到的,而latestNews, likeUrls, reportUrls是Client客户端渲染拿的!!!

心机之蛙一直摸你肚子!

结合上面官方给的文档,再加上这个发现,那就是说我没有正确水合服务端和客户端数据!

image.png

果然,我打印水合数据的时候,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也是不变的,所以也就不会导致重复渲染了,终于成功了!!!

image.png

注意:[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、触发水合的场景有:每次当用户打开具有getStaticPropsgetServerSideProps的页面时,都会调度HYDRATE操作。这可能发生在初始页面加载期间和常规页面导航期间。
4、当触发HYDRATE时,只有服务端数据会保存到store中,客户端数据不会自动存储到store中,所以可以将前置的state的客户端数据主动合并到store中。
5、useSelector方法的第2个参数通过判断是否重新渲染,true时不重新渲染,false时重新渲染。
6、next-redux-wrapperstar这么少,是不是跟它的思想难度(水合)有关???而且好多外网大佬都不建议使用任何一种状态管理器的。
7、通过分析可知,getServerSideProps服务端获取的数据,通过水合会存储到redux Store中,也会存储到客户端dom中
image.png
8、到目前为止SSR遇到的两个大问题,1、cookie,2、水合

引用

next-redux-wrapper
redux toolkit useSelector


全栈工程师进阶
日常学习总结与分享,包括:前端、后台与运维,讲解的知识点包括:javascript、vuejs、reactjs、springb...

Awbeci

3k 声望
193 粉丝
0 条评论
推荐阅读
Helm3-安装Kakfa
前言本文介绍如何在k8s集群中使用helm来创建kafka,供大家参考学习。准备阿里云K8S集群安装helm安装Kafka我们首先添加一下helm库,并且搜索到kafka {代码...} 我们安装的Chart版本:22.1.3,App版本:3.4.0,接着...

Awbeci阅读 189

Next.js-页面顶部添加loading bar功能
next.js框架是主流的SSR框架,强大的开箱即用加上社区非常活跃让它能够在众多框架中脱颖而出。最近为了实现next.js页面之间来回跳转加上loading效果提升用户体验写下这篇文章,希望能够帮助到大家。

Awbeci1阅读 2.9k

Next.js-cookie鉴权+续期1
前端通过axios(或者fetch也可以)调用后台接口的时候通过request请求头header的cookie属性(前端是你的浏览器中存在Cookie)带到后台,前提是要同源,如:前端地址是:www.baidu.com,后台是:www.baiud.com/api或者...

Awbeci阅读 2.3k

Next.js-cookie鉴权+续期2
最近在使用Next.js的时候发现用户认证和刷新token时候跟之前单页面应用SPA的token认证和刷新token方案有所出入,实现起来也更复杂,于是自己参考B站、掘金、思否和简书的SSR网站折腾了一段时间终于解决了这个问题...

Awbeci阅读 2.2k

React-制作全局Tooltip文字提示组件
最近项目中使用antd的tooltip组件的时候发现它有点不稳定,经常会出现漂浮到左上角的情况,让人困惑之余还不知道如何解决,再加上它是在每个dom上面添加的tooltip这样数据量一大的话就会产生冗余的dom元素,于是...

Awbeci阅读 2k

React-Query:啥都没干,就被淘汰了?
在前端领域,也存在同样的现象。作为前端缓存库中的佼佼者,React-Query一直拥有大量受众,官方推出的React-Query课程都卖出了8w+份。

卡颂1阅读 435

封面图
Next.js-你需要知道的知识点
在开发SSR网站的时候,我相信大家或多或少会遇到好多问题,但是了解SSR网站的本质之后,这些都不是问题,下面就分享一下我的总结,希望能够帮助到大家!

Awbeci阅读 676

Awbeci

3k 声望
193 粉丝
宣传栏