Next.js-頁面重複渲染引出的水合問題

Awbeci發表於2023-03-02

案例

我從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

相關文章