We are creating an application on the React + Redux Toolkit + RTK Query + Webpack Module Federation + Nx stack. The idea is to split a large application into modules, create a global store and fill it with reducers as modules are lazily loaded.
The problem is that when loading a module, the slice is not added to the rootReucer.
Folder structure:
apps/
|--shell/
|--moneyback/
libs/
|--store/
|--auth/
This is a global store, it is assumed that when the application starts it will be empty.
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>();
export const dynamicMiddleware = createDynamicMiddleware();
export const store = configureStore({
devTools: process.env.ENV !== 'production',
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat([
dynamicMiddleware.middleware,
]),
});
import { rootReducer } from '@core-backoffice/store';
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
addTokens: (state, { payload }: PayloadAction<PayloadLogin>) => {
const { token, refresh } = payload;
state.token = token;
state.refresh = refresh;
},
addUserData: (state, { payload }: PayloadAction<IUser>) => {
const { email, fullName, roles, userId, workspaces } = payload;
state.email = email;
state.fullName = fullName;
state.roles = roles;
state.userId = userId;
state.workspaces = workspaces;
},
logout: () => initialState,
},
selectors: {
selectToken: (state) => state.token,
selectRefreshToken: (state) => state.refresh,
},
});
declare module '@core-backoffice/store' {
export interface LazyLoadedSlices extends WithSlice<typeof authSlice> {}
}
const injectedSlice = authSlice.injectInto(rootReducer);
export const { addTokens, logout, addUserData } = injectedSlice.actions;
export const { selectRefreshToken, selectToken } = injectedSlice.selectors;
useDispatch, useSelector and authApi are used in useAuth and useCheckAuth hooks
export function App() {
useCheckAuth();
const location = useLocation();
const rootPath = useRootPath('/');
const token = useAppSelector((store) => store.auth?.token);
const { logout } = useAuth();
return (
<React.Suspense fallback={null}>
<Routes>
<Route path="/" element={<NxWelcome title="shell" />} />
<Route path="/moneyback/*" element={<MoneybackApp />} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</React.Suspense>
);
}
export default App;
The apps/moneyback/app.tsx
also uses useDispatch, useSelector and servicesApi hooks and also uses useAuth and useCheckAuth
import { rootReducer } from '@core-backoffice/store';
import { createSlice, WithSlice } from '@reduxjs/toolkit';
const initialState: { services: string[] } = {
services: [],
};
export const servicesSlice = createSlice({
name: 'servicesSlice',
initialState,
reducers: {
addService: (state) => {
state.services = [String(Math.random())];
},
},
selectors: {
selectServices: (state) => state.services,
},
});
declare module '@core-backoffice/store' {
export interface LazyLoadedSlices extends WithSlice<typeof servicesSlice> {}
}
const injectedSlice = servicesSlice.injectInto(rootReducer);
export const { addService } = injectedSlice.actions;
export const { selectServices } = injectedSlice.selectors;
(image) authSlice in global store
After launching the application, authSlcie and authApi were added to the store.
All code was written according to the documentation redux toolkit code splitting
I expect that after switching to loclhost/moneyback, the moneyback module will be lazily loaded and servicesApi and servicesSlice will be added to the store. However, this does not happen.
Big thanks to @phry for solving the issue. Here's what we got in the end
export interface LazyLoadedSlices {}
const keyReduxStore = Symbol.for('reduxStore') as unknown as number;
const keyRootReducer = Symbol.for('rootReducer') as unknown as number;
const keyDynamicMiddleware = Symbol.for(
'dynamicMiddleware'
) as unknown as number;
const combineReducers = () => {
return combineSlices({}).withLazyLoadedSlices<LazyLoadedSlices>();
};
window[keyRootReducer] = window[keyRootReducer] || combineReducers();
export const rootReducer = window[keyRootReducer] as unknown as ReturnType<
typeof combineReducers
>;
window[keyDynamicMiddleware] =
window[keyDynamicMiddleware] || createDynamicMiddleware();
export const dynamicMiddleware = window[
keyDynamicMiddleware
] as unknown as DynamicMiddlewareInstance<any, Dispatch<UnknownAction>>;
const setupStore = () => {
return configureStore({
devTools: process.env.ENV !== 'production',
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware),
});
};
window[keyReduxStore] = window[keyReduxStore] || setupStore();
export const store = window[keyReduxStore] as unknown as AppStore;
export type RootState = ReturnType<typeof rootReducer>;
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore['dispatch'];
This could happen if your bundler somehow bundles libs/store
twice in different bundles.
If that happens, you could get around that by doing something like
window[Symbol.for("ReduxStore")] = window[Symbol.for("ReduxStore")] || configureStore(...)
export const store = window[Symbol.for("ReduxStore")]
in your libs/store