前言
最近在看三国演义,开篇第一段话:话说天下大势,分久必合,合久必分。我把这段话用来解释 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 theRequest
andResponse
objects as options tomakeStore
.In App mode:
- The wrapper calls the
_app
‘sgetInitialProps
function and passes the previously created store.- Next.js takes the props returned from the
_app
‘sgetInitialProps
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 aspayload
- That store is passed as a property to the
_app
orpage
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 aspayload
- That store is passed as a property to the
_app
orpage
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 = () => store
export 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、跳转页面之后,以后页面不要反复渲染
是不是有点难了解,如果大家没了解,能够再想想,或者发私信我。