CRA模板集成Redux-Saga等工具时useSelector报initialLoad属性未定义错误
报错问题
使用useSelector钩子获取数据时触发报错:Cannot read property 'initialLoad' of undefined,经排查是初始状态为undefined导致。
相关代码
selector.ts
import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; const selectSlice = (state: RootState) => state.initialLoad || initialState; export const selectInitialLoad = createSelector([selectSlice], state => state);
index.ts
import { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from 'utils/@reduxjs/toolkit'; import { useInjectReducer, useInjectSaga } from 'utils/redux-injectors'; import { initialLoadSaga } from './saga'; import { InitialLoadState, RepoErrorType } from './types'; export const initialState: InitialLoadState = { name: 'initial-load', layouts: [], loading: false, error: null, }; const slice = createSlice({ name: 'initialLoad', initialState, reducers: { loadLayout(state) { state.loading = true; state.error = null; state.layouts = []; }, layoutLoaded(state, action: PayloadAction<[]>) { const repos = action.payload; state.layouts = repos; state.loading = false; }, layoutError(state, action: PayloadAction<string | RepoErrorType>) { state.error = 'error'; state.loading = false; }, }, }); export const { actions: appLayoutActions } = slice; export const useAppLayoutSlice = () => { useInjectReducer({ key: slice.name, reducer: slice.reducer }); useInjectSaga({ key: slice.name, saga: initialLoadSaga }); return { actions: slice.actions }; };
types.ts
/* --- STATE --- */ export interface InitialLoadState { name: string; loading: boolean; error?: RepoErrorType | string | null; layouts: []; } export enum RepoErrorType { RESPONSE_ERROR = 1, USER_NOT_FOUND = 2, USERNAME_EMPTY = 3, USER_HAS_NO_REPO = 4, GITHUB_RATE_LIMIT = 5, } /* If you want to use 'ContainerState' keyword everywhere in your feature folder, instead of the 'InitialLoadState' keyword. */ export type ContainerState = InitialLoadState;
reducers.ts
/** * Combine all reducers in this file and export the combined reducers. */ import { combineReducers } from '@reduxjs/toolkit'; import { InjectedReducersType } from 'utils/types/injector-typings'; /** * Merges the main reducer with the router state and dynamically injected reducers */ export function createReducer(injectedReducers: InjectedReducersType = {}) { // Initially we don't have any injectedReducers, so returning identity function to avoid the error if (Object.keys(injectedReducers).length === 0) { return state => state; } else { return combineReducers({ ...injectedReducers, }); } }
configureStore.ts
/** * Create the store with dynamic reducers */ import { configureStore, getDefaultMiddleware, StoreEnhancer, } from '@reduxjs/toolkit'; import { createInjectorsEnhancer } from 'redux-injectors'; import createSagaMiddleware from 'redux-saga'; import { createReducer } from './reducers'; export function configureAppStore() { const reduxSagaMonitorOptions = {}; const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions); const { run: runSaga } = sagaMiddleware; // Create the store with saga middleware const middlewares = [sagaMiddleware]; const enhancers = [ createInjectorsEnhancer({ createReducer, runSaga, }), ] as StoreEnhancer[]; const store = configureStore({ reducer: createReducer(), middleware: [...getDefaultMiddleware(), ...middlewares], devTools: process.env.NODE_ENV !== 'production', enhancers, }); return store; }
临时解决方案
当前临时修复是在selector中增加state存在性判断:
import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; const selectSlice = (state: RootState) => state && state.initialLoad || initialState; export const selectInitialLoad = createSelector([selectSlice], state => state);
根本原因
你使用的是动态注入reducer的架构,reducers.ts中定义的逻辑是:当没有任何注入的reducer时,返回state => state恒等函数作为根reducer。Redux初始化时会首次调用根reducer,传入的默认state为undefined,恒等函数直接返回undefined,导致store的根state初始值为undefined,此时selector中直接访问state.initialLoad就会触发属性读取报错。
未清理的CRA模板中默认存在提前注入的公共reducer(比如路由reducer、全局配置reducer等),初始化时injectedReducers不为空,会走combineReducers逻辑,combineReducers会自动生成初始的根state对象,不会出现根state为undefined的情况,所以运行正常。清理模板后你删除了所有默认提前注入的reducer,就触发了根state为undefined的问题。
正确修复方案
- 优先修改公共reducer逻辑,从根源避免根state为undefined。将
reducers.ts中的空reducer回退逻辑修改为默认返回空对象:
if (Object.keys(injectedReducers).length === 0) { return (state = {}) => state; }
修改后无需改动selector代码,即可正常运行。
- 如果不想修改公共逻辑,也可以将selector的取值改为可选链写法,代码更简洁规范:
const selectSlice = (state: RootState) => state?.initialLoad || initialState;
- 确保reducer注入时机早于selector调用时机,在应用根组件中提前调用
useAppLayoutSlice注入全局需要的reducer,不要在业务子组件中才调用注入逻辑,也可以避免该问题。
内容的提问来源于stack exchange,提问作者Francies Fernandes
相关产品推荐
相关产品推荐

