Reputation: 123
I'm currently developing a micro-frontend application using React, webpack and module federation. It consists of one container app, and two "children" / remotes. The container app consist of an app bar, empty side drawer and main div, and the remotes it imports expose one independent React app for the actual page content, plus one single component which should render the navigation items for the side drawer, each:
The remote MFEs have their own Redux store each, which works fine for the page content part, as this includes an App.tsx with a Redux store provider. So far, also the navigation component worked fine, since all it did was push routes into the browser history.
Now I've run into a problem: the exposed navigation component of one remote also has to select data from it's Redux store and dispatch actions. This does not work so far, since it's a single exposed component, and when it's rendered in the container app, there is not Redux store provider for the childs Redux store. How could I solve this? I've read a few times that sharing redux state between micro frontends is a bad practice, so I was trying to avoid this so far. The data the navigation needs access to is basically just a boolean, which indicates that the application is in an elevated "service mode", making the remote MFE render a few more items (e.g. a "delete all" button which is usually hidden). So maybe this could also be shared through local storage or similar, what are some best practices here?
Here's my webpack config and some relevant code for better understanding:
// container app webpack config (development)
...
plugins: [
new ModuleFederationPlugin({
name: "container_app",
remotes: {
config_app: "config_app@http://localhost:3001/remoteEntry.js",
commissioning_app: "commissioning_app@http://localhost:3002/remoteEntry.js",
},
shared: {
...
},
}),
],
...
// config_app webpack config (development)
...
plugins: [
new ModuleFederationPlugin({
name: "config_app",
filename: "remoteEntry.js",
exposes: {
"./ConfigApp": "./src/bootstrap",
"./ConfigAppNavigation": "./src/components/UI/Misc/ConfigAppNavigation",
},
shared: {
...
},
}),
],
...
// MiniDrawer.tsx in container_app, which uses ConfigAppNavigation to render the navigation items
// (depending on current route, otherwise navigation items are rendered from other MFE child)
...
const ConfigAppNavigation = lazy(() => import("config_app/ConfigAppNavigation"));
const MiniDrawer: React.FC = () => {
...
<Switch>
...
<Route path="/">
<Suspense fallback={<span>loading ...</span>}>
<ConfigAppNavigation onNavigate={onDrawerClose} />
</Suspense>
</Route>
</Switch>
...
}
...
And as stated, before switching to an MFE design, the component which is now ConfigAppNavigation selected / changed the mentioned boolean value from config_app's Redux store, which now with this setup doesn't work.
Upvotes: 1
Views: 1970
Reputation: 79
Step 1: Set Up Redux in the Shell/Host Application Install Redux Packages: In your shell or host application, install the necessary Redux libraries.
npm install @reduxjs/toolkit react-redux
Create a Redux Store: In your host app, create a store.js file with the Redux store configuration.
// store.js
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
const store = configureStore({
reducer: rootReducer,
});
export default store;
Set Up a Root Reducer: Create a rootReducer.js file where you combine different slices. Here you can add multiple reducers for different parts of the app.
// rootReducer.js
import { combineReducers } from 'redux';
import someSliceReducer from './someSlice';
const rootReducer = combineReducers({
someSlice: someSliceReducer,
// add other reducers here as needed
});
export default rootReducer;
Provide the Store: Wrap your host application with the Redux Provider to make the store accessible to both local and imported components.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Step 2: Create and Expose Redux Slices Each slice represents a part of your state. Define a slice for the specific state you want to share across micro frontends.
Define a Slice: Create a new slice for shared state, like meeting data.
// someSlice.js
import { createSlice } from '@reduxjs/toolkit';
const someSlice = createSlice({
name: 'someSlice',
initialState: {
sharedData: null,
},
reducers: {
setSharedData: (state, action) => {
state.sharedData = action.payload;
},
},
});
export const { setSharedData } = someSlice.actions;
export default someSlice.reducer;
Export the Action: You’ll need setSharedData to update the state in the host and micro frontend components.
Step 3: Configure Micro Frontends to Use the Host Store Now, we want micro frontends to access and use the host's store. Here are a few ways to do it:
Expose the Store as a Module: In your host app's index.js, expose the store so other apps can import it.
// index.js
import store from './store';
window.store = store; // Expose globally
Access the Store in Micro Frontends: In each micro frontend, access the global store via window.store and pass it to the Provider.
import React from 'react';
import { Provider } from 'react-redux';
import YourComponent from './YourComponent';
function MicroFrontendApp() {
return (
<Provider store={window.store}>
<YourComponent />
</Provider>
);
}
export default MicroFrontendApp;
Step 4: Use the Shared State in Components Now that your store is accessible, you can interact with it in both local and remote components.
Access State in Components: Use useSelector to access the state and useDispatch to modify it.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setSharedData } from './someSlice';
const SomeComponent = () => {
const sharedData = useSelector((state) => state.someSlice.sharedData);
const dispatch = useDispatch();
const handleUpdate = () => {
dispatch(setSharedData('New Data'));
};
return (
<div>
<h1>Shared Data: {sharedData}</h1>
<button onClick={handleUpdate}>Update Shared Data</button>
</div>
);
};
export default SomeComponent;
Step 5: Test and Debug Ensure State Synchronization: Check that changes made in the host app are reflected in the micro frontends and vice versa. Debugging: Redux DevTools can help visualize the state and track actions. Ensure the browser has the Redux DevTools extension installed.
Upvotes: 0