场景
在使用Nextjs开发项目时,会遇到一些场景是需要对一些状态进行全局共享的。例如用户的登录信息、资源信息、权限信息等。那么往往就需要将这些信息存储在全局中进行使用。那么Nextjs
是React
的SSR
框架,那么遇到这种情况大家应该会想到使用Redux
或者Mobx
等状态管理工具进行全局的数据管理。
在我的项目中是使用Nextjs
的Page Router
进行页面开发的,并且使用官方推介的next-redux-wrapper
搭配@reduxjs/toolkit
作为状态同步
以及Store分片
。
本文不会讨论和说明next-redux-wrapper和@reduxjs/toolki等使用方式,感兴趣可以到官网进行了解
问题
现在存在的问题是我目前有PageA和PageB两个页面,两个页面均有设置getServerSideProps
来进行数据拉取,并且在对应页面所绑定的SliceReducers
都有绑定HYDRATE
的Action
,对服务端SSR
和页面跳转的时候调用SSP
的数据进行同步到客户端。
PageA和PageB分别将对应的参数写入到自己所在的分片中,实现Hydrate Action
分发到不同的分片中。
JavaScript
import { HYDRATE } from 'next-redux-wrapper';
export const PageASlice = createSlice({
name: 'pageA',
initialState: {},
reducers: {
//...
},
extraReducers: (builder) => {
builder
.addMatcher(HYDRATE, (state, action) => {
state = {
...state,
...action.payload.pageA,
};
return state;
});
},
});
export const PageBSlice = createSlice({
name: 'pageB',
initialState: {},
reducers: {
//...
},
extraReducers: (builder) => {
builder
.addMatcher(HYDRATE, (state, action) => {
state = {
...state,
...action.payload.pageB,
};
return state;
});
},
});
以上简单描述了基本的项目信息
在一次调试中,发现一个问题,当PageA跳转到PageB的时候,发现PageA会触发一次渲染。因为在PageA中做了一些数据判断,当发现没有数据的时候,会重定向到404页面。导致跳转到PageB的时候,或者在PageA进行浏览器后退行为回到任何页面的时候,会触发没有数据的逻辑判断,跳转到404。
分析
预期中应该是PageA跳转到PageB应该PageA会被卸载,不应该再重复渲染一次。更加不应该会出现触发PageA的渲染逻辑。经过断点调试,发现Nextjs的跳转页面的执行逻辑如下:
- 匹配跳转路由的路由信息表
- 目标页面是否配置
getServerSideProps
- 当配置了
getServerSideProps
时,发起网络请求到服务端获取getServerSideProps
结果 - 因为使用了
next-redux-wrapper
缘故,getServerSideProps
会返回完整的Store
数据 - 触发
Hydrate Action
更新客户端数据
因为服务器是无状态,所以每次触发getServerSideProps
时,整个Store
都是初始状态,通过执行不同页面的 getServerSideProps
来更新Store
的数据,然后返回本次SSP
后的Store
给客户端触发 Hydrate Action
。
通过上面的图片就可以发现,因为是从PageA -> PageB,所以只调用PageB的SSP
,从而导致返回的数据中,只PageA的数据是为空的。next-redux-wrapper
在接收到SSP
的返回后,会默认触发Hydrate Action
,从而引起不同页面的Reducer
监听到 Hydrate Action
触发,然后进行Store
的更新。
然后在Nextjs中,切换路由状态时,并不会立马卸载当前页面的组件,而是完成了SSP
并且触发完Hydrate Action
后再进行页面切换。这样就导致因为更新Store
引起了PageA进行重新渲染,而这时PageA并没有卸载。
这是next-redux-wrapper的官方解释Hydrate的过程链接
解决方案
其实解决的思路很简单,期望的是那个页面触发SSP
,那么那个对应的SliceReducers
就和服务器返回的数据进行同步。 其他不是当前页面的SliceReducers
不进行 Hydrate
,保持当前客户端的状态。
改造一下分片,为每一个分片都添加一个NEED_HYDRATE
的状态和 一个changeSliceHydrateState
的reducer
,然后HYDRATE
的reducer
中添加判断,当SSP
返回的数据中,对应当前分片的数据的NEED_HYDRATE
状态为true
时才进行数据同步操作,否则不进行数据同步。
JavaScript
export const PageASlice = createSlice({
name: 'pageA',
initialState: {
NEED_HYDRATE: false
},
reducers: {
//...
changeSliceHydrateState: (state, { payload }) => {
state.NEED_HYDRATE = true;
}
},
extraReducers: (builder) => {
builder
.addMatcher(HYDRATE, (state, action) => {
if (action.payload.pageA.NEED_HYDRATE) {
state = {
...state,
...action.payload.pageA,
};
}
return state;
});
},
});
另外在对应页面的getServerSideProps
中,主动调用 changeNeedHyrate
来更新状态。
JavaScript
import { changeNeedHyrate } from '@/actions/pageA';
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async (context) => {
store.dispatch(changeNeedHyrate(true));
return {
props: {
// ...
},
};
},
);
经过上面的改造,那么在进行PageA -> PageB时,调用了PageB的getServerSideProps
,所以在SSP
返回的数据中,PageB的 NEED_HYDRATE
为true
,因为没有调用PageA的 getServerSideProps
,所以在返回的数据中,PageA的 NEED_HYDRATE
为默认值false
,所以PageA不会进行数据同步,从而问题解决。
当然这个方法不是一个完美的方案,例如我的页面需要依赖两个切片Store,那么就需要在getServerSideProps
中设置两个切分Store中的NEED_HYDRATE
,从而使得在客户端时,两个切片能正确同步。
结论
当然在这之前也尝试一些不同的解决方案,例如:
- 在
HYDRATE
的reducer
中添加其他判断逻辑,例如判断当前是否有数据,当有数据时,判断更新的数据是否一致等与当前页面业务逻辑耦合的判断条件来实现同步机制。 - 通过对PageA包裹一个高阶组件,PageA中不要直接使用
useSelector
,而是由高阶组件通过通过Props
或者再套一层Context
来阻断Redux
的更新直接影响我们PageA,然后由高阶组件判断路由变化等因素,决定是否通知PageA进行更新。
但是这些方法都对业务有不同程度的入侵,所以最终通过在SSP
时,添加对应分片的更新标识,统一根据标识来控制是否进行数据同步。这样对也业务的入侵和感知度最低,也更适合目前我所在项目中落地。
如果你有更好的方案,欢迎一起探讨!
其他文章对于该问题的一些解决方式: