前言

最近在看三国演义,开篇第一段话:话说天下大势,分久必合,合久必分。我把这段话用来解释next.js+redux水合作用再失当不过了。

什么是水合?

水合是咱们在next.js我的项目中引入next-redux-wrapper插件之后给出的一个新概念,它是连贯和对立客户端和服务端数据的一个重要纽带。

英文名叫HYDRATE,中文叫水合又叫水化,我在网上搜寻的答复:

水合物指的是含有水的化合物,其范畴相当宽泛。其中水能够以配位键与其余局部相连,如水合金属离子,也能够是以共价键相结合,如水合三氯乙醛。也能够指是天然气中某些组分与水分在肯定温度、压力条件下造成的红色晶体,外观相似致密的冰雪,密度为0.88\~0.90 g/cm^3^。水合物是一种笼形晶体包络物,水分子借氢键联合造成笼形结晶,气体分子被突围在晶格之中。

看得我一头雾水,于是联合我本人的了解我来解释下何为水合,如果解释的不对,也心愿大家对我批评指正。

艰深的说就是同一个水源进去多个分支的水流,最初水流又从新汇聚成新的水源,再反复这个过程。有点相似git下面的分支,有一个总分支master,还有子分支dev/test/uat等等,离开开发完又合并到总分支master。

不晓得我这样解释能不能帮忙你们了解,而在代码层面就是:关上一个新页面,或者切换新的路由的时候,Redux数据源Store会分流到所有Pages中的页面,最初在Reducer中合并服务端和客户端数据成新的Store数据源,再反复这样的过程。

具体的过程next-redux-wrapper插件官网给出了解释:

Using next-redux-wrapper ("the wrapper"), the following things happen on a request:

  • Phase 1: getInitialProps/getStaticProps/getServerSideProps

    • The wrapper creates a server-side store (using makeStore) with an empty initial state. In doing so it also provides the Request and Response objects as options to makeStore.
    • In App mode:

      • The wrapper calls the _app's getInitialProps function and passes the previously created store.
      • Next.js takes the props returned from the _app's getInitialProps method, along with the store's state.
    • In per-page mode:

      • The wrapper calls the Page's getXXXProps function and passes the previously created store.
      • Next.js takes the props returned from the Page's getXXXProps method, along with the store's state.
  • Phase 2: SSR

    • The wrapper creates a new store using makeStore
    • The wrapper dispatches HYDRATE action with the previous store's state as payload
    • That store is passed as a property to the _app or page component.
    • Connected components may alter the store's state, but the modified state will not be transferred to the client.
  • Phase 3: Client

    • The wrapper creates a new store
    • The wrapper dispatches HYDRATE action with the state from Phase 1 as payload
    • That store is passed as a property to the _app or page component.
    • The wrapper persists the store in the client's window object, so it can be restored in case of HMR.

Note: The client's state is not persisted across requests (i.e. Phase 1 always starts with an empty state). Hence, it is reset on page reloads. Consider using Redux persist if you want to persist state between requests.

为什么要用水合?

水合的目标是达到服务端和客户端数据的和解最初对立数据源。

如果咱们不必水合就会呈现上面两个问题(目前为止我遇到的问题):

1、当关上页面或者导航到新页面后,客户端数据会失落

2、路由切换页面的时候以后页面会呈现反复渲染问题,能够参考我之前写得一篇文章:Next.js-页面反复渲染引出的水合问题,就是因为客户端数据失落,导致触发useSelector办法,最终导致反复渲染。

怎么在理论我的项目中利用水合?

接下来,咱们花工夫重点介绍如何解决水合问题(默认你们都装置了next-redux-wrapper插件)。

首先,咱们参考next-redux-wrapper文档配置一下next.js我的项目,这里就不做介绍,大家能够看看它的在线文档,上面是我的配置代码,大家能够参考下。

store.js

import {configureStore, combineReducers, MiddlewareArray} from '@reduxjs/toolkit';import {createWrapper, HYDRATE} from 'next-redux-wrapper';import logger from "redux-logger";const combinedReducers = combineReducers({  [authSlice.name]: authSlice.reducer,  [userSlice.name]: userSlice.reducer,  [homeSlice.name]: homeSlice.reducer,  [notifySlice.name]: notifySlice.reducer,  [fileSpaceSlice.name]: fileSpaceSlice.reducer,  [rankSlice.name]: rankSlice.reducer,});export const store = configureStore({  reducer: combinedReducers,  devTools: false,  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)})const makeStore = () => storeexport const wrapper = createWrapper(makeStore, { storeKey: 'key',debug:false })

_app.js

import {useState, useEffect} from 'react'import {Provider} from 'react-redux'import {wrapper} from '@/store'const MyApp = ({Component, ...rest}) => {  const {store, props} = wrapper.useWrappedStore(rest);  return <Provider store={store}>    <Component {...props.pageProps} />  </Provider>}export default MyApp

暂停一下,尽管咱们当初曾经配置好了,然而还没有真正的解决水合问题,解决水合问题,重点是解决如何合并服务端和客户端数据,咱们来看看next-redux-wrapper插件官网给出的解决办法,如下所示:

    import {HYDRATE} from 'next-redux-wrapper';    // create your reducer    const reducer = (state = {tick: 'init'}, action) => {      switch (action.type) {        case HYDRATE:          const stateDiff = diff(state, action.payload) as any;          const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria          return {            ...state,            ...action.payload,            page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated          };        case 'TICK':          return {...state, tick: action.payload};        default:          return state;      }    };

或者

    const reducer = (state, action) => {      if (action.type === HYDRATE) {        const nextState = {          ...state, // use previous state          ...action.payload, // apply delta from hydration        };        if (state.count) nextState.count = state.count; // preserve count value on client side navigation        return nextState;      } else {        return combinedReducer(state, action);      }    };

下面的第1段代码,判断state和action.payload有没有不同,不同的话则合并.

第2段代码,判断state.count是否有值,有值则合并。

这些都能够解决事实我的项目中的一些问题,然而不能解决所有问题,于是我本人提出了一个解决方案:

每次进入一个页面的时候,咱们记录下以后进入的是哪个页面,有了这个,咱们就能够在Reducer中调度HYDRATE时判断是不是以后页面来合并数据。

上面咱们来看看如何实现?咱们取user.js页面为例子,

pages/user.js页面中的getServerSideProps办法

import {wrapper} from '@/store'import {setCurrentHydrate} from '@/store/slices/systemSlice'export const getServerSideProps = wrapper.getServerSideProps(store => async (ctx) => {  await store.dispatch(setCurrentHydrate('user'))  return {    props: {    }  };});

咱们认真看看下面这段代码,它在getServerSideProps阶段,调用了systemSlice中的setCurrentHydrate()办法,并且参数是'user',记录的就是以后页面,其它页面也是如此,惟一不同的点是参数不同。

setCurrentHydrate办法实现代码如下:

systemSlice.js

const initialState = {  // 以后渲染的页面  currentHydrate: ''}  reducers: {    reset: () => initialState,    setCurrentHydrate: (state, action) => {      // 设置以后选中的页面name      state.currentHydrate = action.payload;    },  },export const {setCurrentHydrate} = systemSlice.actions

而后,咱们以userSlice.js为例,编写如何合并客户端和服务端数据

import {HYDRATE} from "next-redux-wrapper";const initialState = {  a: null,  b: null,  c: null,}extraReducers: {    // action.payload 是后盾getServerSideProps办法返回的数据    // 体现在__NEXT_REDUX_WRAPPER_HYDRATE__的action.payload数据中    // state 是store中原始数据,如果是第一次进来 则是initialState数据    // 体现在__NEXT_REDUX_WRAPPER_HYDRATE__的prev state数据中    [HYDRATE]: (state, action) => {      let nextState = {        ...state,        ...action.payload.user,      }      if(action.payload.system.currentHydrate !== 'user'){        nextState.a = state.a        nextState.b = state.b        nextState.c = state.c      }      // nextState是合并后并保留到store中的数据      // 体现在__NEXT_REDUX_WRAPPER_HYDRATE__的next state数据中      return nextState    },

看看action.payload.system.currentHydrate !== 'user'这个判断 ,意思是如果以后页面不是user,那么则合并客户端数据,否则不合并,代表了页面切换路由的时候会水合数据下的场景。

看到这里,咱们解决了下面提的第1个问题,然而还有一个问题没有解决,就是所有pages下的页面useSelector办法会导致页面反复渲染问题,如何解决呢?

解决办法:还是通过判断currentHydrate来决定是否要反复渲染,代码如下:

import {createSelector} from "@reduxjs/toolkit";  const { userInfo} = useSelector((state) => {    return {      ...state.auth,      hydrate: state.system.currentHydrate    }  }, (_old, _new) => _old.hydrate !== _new.hydrate);

通过判断新老hydrate数据是否雷同,不雷同则不必从新渲染,否则从新渲染。

这样就解决了所有问题,完结撒花!

留神:

如果你应用了redux-logger打印状态日志插件,那么你会看到每次关上新页面或者路由跳转的时候控制台都会打印上面这样的代码:

阐明:它是总水合,离开看的话对应reducer的[HYDRATE]办法外面的水合操作。

action.payload 是后盾getServerSideProps办法返回的数据prev state 是store中原始数据,如果是第一次进来 则是initialState数据nextState 是合并后并保留到store中的数据

总结

每次进入一个页面的时候,咱们记录下以后进入的是哪个页面,有了这个,咱们就能够在Reducer中调度HYDRATE时判断是不是以后页面来合并数据。

为什么我要提这样的计划?

在答复这个问题之后,咱们来看看三个场景:
1、第1次关上页面
2、刷新以后页面
3、导航到其它页面

这三个场景下,第1、2场景页面数据都是最新的,只拿到了服务端数据,而第3种状况下,可能在跳转前页面就曾经有各种操作了,所以会产生客户端数据,这时候你如果跳转页面而没有正确水合的话,以后页面保留在Redux中的客户端数据就会清空,所以我的计划就是:

1、正确水合客户端和服务端数据
2、跳转页面之后,以后页面不要反复渲染

是不是有点难了解,如果大家没了解,能够再想想,或者发私信我。