Next.js中结合Redux实现SSR服务端数据获取方法
Next.js + Redux 服务端渲染实现方案
核心逻辑:getServerSideProps运行在Node服务端,无法直接访问浏览器端的Redux单例store,需要为每次服务端请求创建独立store实例,提前dispatch异步action拉取数据,再将序列化后的store状态传回客户端完成水合,保证前后端状态一致。
1. 改造Redux store创建逻辑,支持服务端多实例
禁止导出全局单例store,否则服务端多用户请求会出现状态串流问题,改为工厂函数创建实例:
// store/index.js 改造示例 import { createStore, applyMiddleware, combineReducers } from 'redux'; import thunk from 'redux-thunk'; // 引入项目中已有的reducer import productReducer from './reducers/ProductReducer'; import preLoadingReducer from './reducers/PreLoadingReducer'; import categoryReducer from './reducers/CategoryReducer'; const rootReducer = combineReducers({ products: productReducer, preLoading: preLoadingReducer, categories: categoryReducer }); // 接收预加载状态参数,支持服务端状态注入 export const initializeStore = (preloadedState = {}) => { return createStore( rootReducer, preloadedState, applyMiddleware(thunk) // 项目用的其他中间件(如saga)正常放在这里 ) }
2. 改造_app.js入口,统一处理Redux状态水合
在应用入口接收服务端传回的初始Redux状态,初始化客户端store,避免hydration不匹配报错:
// pages/_app.js import { Provider } from 'react-redux'; import { initializeStore } from '../store'; import { useMemo } from 'react'; function MyApp({ Component, pageProps }) { // 客户端仅初始化一次store,用服务端传回的状态做初始值 const store = useMemo(() => { return initializeStore(pageProps.initialReduxState || {}); }, [pageProps.initialReduxState]); return ( <Provider store={store}> <Component {...pageProps} /> </Provider> ) } export default MyApp;
3. 重写页面getServerSideProps逻辑,服务端预拉数据
在服务端函数中创建独立store实例,直接复用你已经写好的Redux action拉取数据,完成后将store状态回传:
// 页面顶部引入store工厂函数 import { initializeStore } from '../../../store'; export const getServerSideProps = async (context) => { // 从上下文取路由参数,和客户端router.query.maincat拿到的值完全一致 const { maincat } = context.query; // 为当前请求创建独立store实例 const store = initializeStore(); try { // 直接dispatch项目中已有的异步action,和客户端写法完全一致 await store.dispatch(ProductAction.fetchMainCatProduct(maincat)); // 需要服务端预加载的其他数据(比如品牌、筛选项)也可以在这里dispatch // await store.dispatch(CategoryAction.fetchBrandsAndFilters()); } catch (err) { // 接口报错可以自定义处理,比如返回404页面 return { notFound: true } } // 拿到所有action执行完成后的全量store状态 const initialReduxState = store.getState(); return { props: { initialReduxState, // 必须传回给客户端做水合 // 其他自定义页面props正常添加 } } }
4. 调整页面客户端加载逻辑,避免重复请求
服务端已经预拉了首屏商品数据,首屏加载不需要再重复跑客户端请求,只在客户端路由跳转时触发加载即可。
首先在react引入中加上useRef:
import { useCallback, useEffect, useState, useRef } from "react";
然后修改原来的useEffect逻辑:
// 标记是否为首次水合渲染 const isInitialMount = useRef(true); useEffect(() => { // 首屏SSR已经带了数据,直接跳过加载逻辑 if (isInitialMount.current) { isInitialMount.current = false; setItems(false); props.setPreLoading(false); return; } // 仅客户端路由切换时走加载逻辑 props.setPreLoading(true); setItems(true); loadProducts().then(() => { setItems(false); setTimeout(() => { props.setPreLoading(false); }, 1000); }); }, [dispatch, setItems, loadProducts, MainCat]);
注意事项
- 服务端必须每次请求新建store实例,绝对不能复用全局单例,否则会出现不同用户的状态互相污染
- 服务端dispatch异步action必须加
await等请求完成后再取store.getState(),否则拿到的是空初始状态 - Redux中存储的状态必须是可序列化的,不要存函数、DOM节点这类特殊对象,否则序列化传回客户端时会报错
- 排序、筛选这类纯客户端交互的逻辑不需要改动,和之前写法一致,正常dispatch对应action即可,只有首屏直出需要的数据在服务端预拉
内容的提问来源于stack exchange,提问作者Amir Hossein
相关产品推荐
相关产品推荐

