Shared State Management
Sharing Redux Store Across Micro Frontends

Published: May 12, 2026 · 18 min read

Sharing Redux Store Across Micro Frontends

A user logs in through the Auth React micro frontend at /login. Within the same browser tab, they navigate to /cart — the Cart remote mounts and immediately redirects them back to /login because its useSelector(selectIsLoggedIn) returns false. Open Redux DevTools and there are two stores: one created by Auth, one created by Cart. Each remote dispatched into its own store and the other never knew. This is the cross-cutting state problem at the heart of every multi-remote architecture, and the fix is a properly federated redux store micro frontend singleton — one store, owned by the host, shared by every React and Next.js remote in the application. In the previous article on Content Security Policy in a Next.js MFE, you saw how every directive (script-src, connect-src, frame-src) gates a different category of cross-origin call. The shared store is the equivalent coordination problem on the JavaScript side: every remote that needs cart state, auth tokens, or support tickets must read from and write to the SAME store object, regardless of which webpack bundle it shipped in.

This article walks the complete singleton-store pattern that opens Section 4 — Shared State Management: the store package layout, the slices, the typed hooks, the host-side Provider setup for both a React host and a Next.js host, the webpack/next.config shared block that makes the singleton binding, and the eight production gotchas that account for almost every "state is not propagating" bug.

In this guide, you will:

  • See the cross-remote state problem in concrete terms — Auth dispatches, Cart never sees it
  • Build the federated @myapp/store package with configureStore, slices, typed hooks, and re-exported react-redux primitives
  • Wire up the React host with bootstrap.js + <Provider> and the Next.js host with ClientReduxProvider + ssr: false
  • Compare local development vs production webpack shared blocks (full localhost:PORT URLs vs /path/ URLs, splitChunks, contenthash)
  • Mirror the singleton declaration across the host and every remote with strictVersion + requiredVersion
  • Read and write the same store from a React Auth remote (OTPVerify) and a Next.js Products remote (AddToCartButton)
  • Understand why every remote keeps its own <Provider> wrapper for standalone development without breaking the federated singleton
  • Avoid the eight gotchas that surface only after a real-world deploy — duplicate stores, eager-consumption errors, nested providers, hydration mismatches

Redux store micro frontend architecture diagram showing one singleton store federated across multiple React and Next.js remotes via Module Federation

The Cross-Remote State Problem

A real micro frontend has authentication state, cart state, support tickets, and chat messages — all of which need to be visible to multiple remotes simultaneously. The user logs in through the Auth remote, adds a product to the cart from the Products remote, opens a support ticket through the Support remote, and the host's header must reflect every change. There is no parent component to lift state into; the remotes are loaded lazily and may not even be mounted at the same time.

The cross-remote state problem in concrete terms
# The Cross-Remote State Problem
# ────────────────────────────────
#
#   ┌─────────────────────────────────────────────────────────┐
#   │  HOST (React shell)                                      │
#   │   Header reads:  isLoggedIn ? <Account /> : <LoginCTA /> │
#   └─────────────────────────────────────────────────────────┘
#                             │
#       ┌──────────┬──────────┼──────────┬──────────┐
#       ▼          ▼          ▼          ▼          ▼
#   ┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐
#   │ Auth  │  │ Cart  │  │ Orders│  │Account│  │Support│
#   │remote │  │remote │  │remote │  │remote │  │remote │
#   └───────┘  └───────┘  └───────┘  └───────┘  └───────┘
#
#   USER STORY:
#     1. User opens /login (Auth remote mounts)
#     2. Auth dispatches setIsLoggedIn(true) into ITS OWN store
#     3. User navigates to /cart (Cart remote mounts)
#     4. Cart's useSelector(selectIsLoggedIn) returns FALSE
#     5. Cart redirects user back to /login → infinite loop
#
#   ROOT CAUSE:
#     Each remote bundled its own copy of @reduxjs/toolkit and
#     created a fresh store instance at boot. Auth's store and
#     Cart's store are two unrelated objects in memory. dispatch()
#     in one is invisible to useSelector() in the other.
#
#   THE FIX (this article):
#     Publish the store as a workspace package. Federate it as a
#     Module Federation singleton with strictVersion: true. The
#     host creates the store ONCE; every remote that imports from
#     @myapp/store receives the same reference at runtime.

The naive options all fail at the seams:

ApproachWhy it fails in an MFE
Prop drillingThe host does not directly render every remote's children — props cannot reach a lazy-loaded component three levels down
localStorage pollingRacy, every remote re-implements read/write logic, no schema enforcement, change events not guaranteed
BroadcastChannel / postMessageEvery remote must subscribe to every event, no type safety, easy to ship a typo and silently lose updates
Per-remote storesExact problem from the cross-remote diagram — dispatches stay isolated to whichever store the remote happened to create
Federated singleton storeOne store, one source of truth, useSelector works across remote boundaries with no extra code

A single Redux store, federated as a Module Federation singleton, sidesteps every row above. The host creates the store. Every remote imports useSelector and useDispatch from the same federated package. Module Federation's runtime guarantees there is exactly one store instance regardless of how many remotes are mounted.

The same singleton pattern works for any global state library. Zustand, Jotai, and TanStack Query store instances all federate the same way as long as the store creation function lives inside a singleton package. Redux Toolkit is the example here because it is the most widely used pattern in production MFEs — see Module Federation shared dependencies for the underlying mechanism.

Federated Redux Store Architecture for a Micro Frontend

The architecture below shows the host owning a single store instance and five remotes — four React (Auth, Cart, Orders, Account) and one Next.js (Products) — all consuming the same store via Module Federation singletons. The store package is published as a workspace dependency at version 1.0.0, locked across every webpack and next.config block.

Federated Redux store overview
# Federated Redux Store — One Instance, N Remotes
# ─────────────────────────────────────────────────
#
#   ┌─────────────────────────────────────────────────────────┐
#   │  HOST — Main (React or Next.js)                          │
#   │                                                          │
#   │   bootstrap.js          /         pages/_app.tsx         │
#   │     <Provider store={store}>   ← created exactly once    │
#   │       <BrowserRouter>                                    │
#   │         <App />                                          │
#   │       </BrowserRouter>                                   │
#   │     </Provider>                                          │
#   └─────────────────────────────────────────────────────────┘
#                              │
#       ┌──────────┬───────────┼───────────┬──────────┐
#       ▼          ▼           ▼           ▼          ▼
#   ┌───────┐ ┌───────┐  ┌──────────┐ ┌────────┐ ┌────────┐
#   │ Auth  │ │ Cart  │  │ Products │ │ Orders │ │Account │
#   │ React │ │ React │  │ Next.js  │ │ React  │ │ React  │
#   └───────┘ └───────┘  └──────────┘ └────────┘ └────────┘
#       │         │            │           │          │
#       └─────────┴────────────┴───────────┴──────────┘
#                            │
#                            ▼
#               ┌──────────────────────────┐
#               │   @myapp/store (1.0.0)   │
#               │   singleton: true         │
#               │   strictVersion: true     │
#               │                            │
#               │   Same store reference    │
#               │   served to every import  │
#               └──────────────────────────┘
#
# Module Federation runtime guarantee:
#   - First app to load triggers the store package fetch
#   - Subsequent imports get the SAME reference
#   - dispatch() in Auth → useSelector() in Products sees the change
#   - One Redux DevTools window shows ALL state mutations

The crucial property is that the store is created once, in the host, on the client. Every remote that later imports from the store package receives the host's instance, not a fresh copy. Module Federation's singleton: true flag is what makes this guarantee binding — without it, each remote would silently bundle its own copy and you would be back to the cross-remote state problem.

Step 1 — Build the Federated Store Package

The store lives in a workspace package — the same Turborepo or pnpm workspace pattern from the React MFE monorepo guide. Three files matter: package.json (the versioning contract), src/store.js (the configureStore call), and index.js (the public API every remote imports).

Package manifest with version locking

The package.json is small but every field matters. The version field is what Module Federation matches against requiredVersion: '1.0.0' in every remote's shared block. The peerDependencies declare React without bundling it, so the federated singleton React from the host is the one that gets used.

packages/core/store/package.json
{
  "name": "@myapp/store",
  "version": "1.0.0",
  "main": "./index.js",
  "types": "./index.d.ts",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@reduxjs/toolkit": "^2.6.0",
    "react-redux": "^9.2.0"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}

// Why version "1.0.0" is treated like a contract:
//   The host AND every remote declare requiredVersion: "1.0.0"
//   with strictVersion: true. Bumping this number FORCES every
//   remote to be rebuilt and redeployed in the same release.
//   Module Federation refuses to load a 1.1.0 store next to a
//   1.0.0 store. That refusal is the safety net that keeps the
//   slice shapes identical across remotes deployed at different
//   times — no "the field was renamed but Cart still ships the
//   old store" production bug.

Store factory and singleton

configureStore runs once at module load. The makeStore() factory exists to support per-request server stores (used in rare SSR cases), and store is the default browser singleton that the host imports.

packages/core/store/src/store.js
// packages/core/store/src/store.js
import { configureStore } from '@reduxjs/toolkit';
import { userSlice } from './slices/userSlice';
import { cartSlice } from './slices/cartSlice';
import { ticketSlice } from './slices/ticketSlice';
import { chatSlice } from './slices/chatSlice';

// makeStore() exists so SSR can build a fresh per-request store.
// The default export 'store' is the browser singleton — what the
// host imports inside bootstrap.js / _app.tsx.
export const makeStore = () => {
  return configureStore({
    reducer: {
      user:    userSlice.reducer,
      cart:    cartSlice.reducer,
      tickets: ticketSlice.reducer,
      chat:    chatSlice.reducer,
    },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        // Required because cart items carry Date objects in some
        // payloads. Without this, Redux Toolkit emits a warning on
        // every dispatch — harmless but noisy. Trade-off: do not
        // store anything you cannot rebuild from JSON.
        serializableCheck: false,
      }),
  });
};

// Browser singleton. The host imports THIS reference. Every
// remote that later imports from '@myapp/store' is handed the
// same instance by Module Federation's shared scope.
export const store = makeStore();

// Per-request server factory — used only when an SSR page genuinely
// needs to pre-populate state from cookies (rare in MFE setups).
export const makeServerStore = () => makeStore();

serializableCheck: false is required because the cart slice carries Date objects in some payloads. Without it, Redux Toolkit (opens in a new tab) emits a warning on every dispatch — harmless, but noisy. The trade-off is that you must not store non-serializable values you cannot recreate from JSON.

Step 2 — Define the Slices Every Remote Will Touch

The user slice owns authentication state. The Auth remote dispatches setAt, setIsLoggedIn, and setUser. Every other remote — Cart, Orders, Account, Header — reads with selectors like selectIsLoggedIn and selectAccessToken.

packages/core/store/src/slices/userSlice.js
// packages/core/store/src/slices/userSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  firstName: '',
  lastName: '',
  name: '',
  email: '',
  phoneNumber: null,
  avatarUrl: null,
  at: null,                 // <- access token, read by axios interceptor
  isLoggedIn: false,
  profileUpdating: false,
  address: null,
};

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUser: (state, action) => {
      state.firstName   = action.payload.firstName;
      state.lastName    = action.payload.lastName;
      state.name        = action.payload.name;
      state.email       = action.payload.email;
      state.phoneNumber = action.payload.phoneNumber || null;
      state.avatarUrl   = action.payload.avatarUrl   || null;
    },
    setAt: (state, action) => {
      state.at = action.payload.at;
    },
    setIsLoggedIn: (state, action) => {
      state.isLoggedIn = action.payload.isLoggedIn;
    },
    setAddress: (state, action) => {
      state.address = action.payload.address;
    },
    clearUser: (state) => {
      state.firstName  = '';
      state.lastName   = '';
      state.name       = '';
      state.email      = '';
      state.at         = null;
      state.isLoggedIn = false;
      state.address    = null;
    },
  },
});

// Selectors are the public read surface. Adding a field is safe;
// renaming one is a BREAKING change because every consumer
// references it by name → bump the package version.
export const selectUser        = (state) => state.user;
export const selectIsLoggedIn  = (state) => state.user.isLoggedIn;
export const selectAccessToken = (state) => state.user.at;
export const selectUserName    = (state) => state.user.name;
export const selectEmail       = (state) => state.user.email;
export const selectAddress     = (state) => state.user.address;

export const {
  setUser, setAt, setIsLoggedIn, setAddress, clearUser,
} = userSlice.actions;

export default userSlice.reducer;

The selectors are the API surface that remotes depend on. Adding a new field to initialState is safe; renaming a field requires bumping the package version because every consumer references the field by name.

The cart slice owns line items and totals. The Products remote dispatches addToCart, the Cart remote dispatches updateQuantity, and the host's header reads selectTotalCartItems for the badge.

packages/core/store/src/slices/cartSlice.js (excerpt)
// packages/core/store/src/slices/cartSlice.js (excerpt)
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  items: [],
  selectedItems: [],
  totalItems: 0,
  totalAmount: 0,
  appliedCoupon: null,
};

export const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addToCart: (state, action) => {
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) {
        existing.qty += 1;
      } else {
        state.items.push({ ...action.payload, qty: action.payload.qty || 1, selected: true });
      }
      state.totalItems = state.items.reduce((s, i) => s + i.qty, 0);
    },
    removeFromCart: (state, action) => {
      state.items = state.items.filter(i => i.id !== action.payload);
      state.totalItems = state.items.reduce((s, i) => s + i.qty, 0);
    },
    updateQuantity: (state, action) => {
      const item = state.items.find(i => i.id === action.payload.id);
      if (item) item.qty = action.payload.qty;
      state.totalItems = state.items.reduce((s, i) => s + i.qty, 0);
    },
    clearCart: (state) => {
      state.items = [];
      state.selectedItems = [];
      state.totalItems = 0;
      state.totalAmount = 0;
    },
  },
});

export const selectCartItems       = (state) => state.cart.items;
export const selectTotalCartItems  = (state) => state.cart.totalItems;
export const selectAppliedCoupon   = (state) => state.cart.appliedCoupon;

export const {
  addToCart, removeFromCart, updateQuantity, clearCart,
} = cartSlice.actions;

export default cartSlice.reducer;

Step 3 — Export Selectors, Hooks, and Provider

Every remote should import everything Redux-related through @myapp/store — never from react-redux directly. The index.js is the only entry point remotes touch. It re-exports actions, selectors, hooks, and — critically — Provider, useSelector, and useDispatch from react-redux.

packages/core/store/src/hooks.js
// packages/core/store/src/hooks.js
import { useDispatch, useSelector } from 'react-redux';

// Typed wrappers — every remote imports these instead of the
// raw react-redux primitives. Keeps the singleton negotiation
// honest because the import chain always goes through @myapp/store.
export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;

// In a TypeScript codebase, type these against the store:
//   import type { TypedUseSelectorHook } from 'react-redux';
//   import type { RootState, AppDispatch } from './store';
//   export const useAppDispatch: () => AppDispatch = useDispatch;
//   export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
packages/core/store/index.js
// packages/core/store/index.js — Public API
// Every export here is what remotes (React or Next.js) import.

// ── Store + factories ──────────────────────────────────────
export { store, makeStore, makeServerStore } from './src/store';

// ── Typed dispatch + selector hooks ────────────────────────
export { useAppDispatch, useAppSelector } from './src/hooks';

// ── User slice ─────────────────────────────────────────────
export {
  userSlice, setUser, setAt, setIsLoggedIn, setAddress, clearUser,
  selectUser, selectIsLoggedIn, selectAccessToken,
  selectUserName, selectEmail, selectAddress,
} from './src/slices/userSlice';

// ── Cart slice ─────────────────────────────────────────────
export {
  cartSlice, addToCart, removeFromCart, updateQuantity, clearCart,
  selectCartItems, selectTotalCartItems, selectAppliedCoupon,
} from './src/slices/cartSlice';

// ── Re-export react-redux primitives so remotes do NOT need
//    their own react-redux dependency at runtime ────────────
export { Provider, useDispatch, useSelector } from 'react-redux';

// Why re-export Provider here instead of letting remotes
// import 'react-redux' directly?
//   1. Some remotes ship without react-redux as a top-level dep.
//      Routing through @myapp/store guarantees they still pick
//      up the federated singleton.
//   2. The host can swap react-redux for a wrapper later without
//      touching every remote's import statements.

Routing every Redux primitive through @myapp/store guarantees that Module Federation's singleton negotiation handles the call. If a remote imports react-redux directly, webpack might bundle a separate copy at build time, and the bundled copy disagrees with the federated copy on internal state — leading to the silent production bugs covered in the gotchas section.

Step 4 — Webpack Shared Config (Local vs Production)

The shared block is where the singleton contract is signed. Five packages need the singleton flag, and the store itself needs strictVersion: true to fail loudly if any remote ships a different version. Local and production configs differ ONLY in URLs, mode, and chunking — the shared block is byte-identical so the singleton stays valid in both environments.

apps/Main/webpack.config.js (production)
// apps/Main/webpack.config.js — React HOST (production)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const dependencies = require('./package.json').dependencies;

module.exports = {
  mode: 'production',
  entry: path.resolve(__dirname, 'src', 'index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    publicPath: '/',
    chunkFilename: '[name].[contenthash].js',
    clean: true,
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json'],
    alias: {
      '@myapp/store': path.resolve(__dirname, '../../packages/core/store'),
      '@myapp/api':   path.resolve(__dirname, '../../packages/core/api'),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Main',
      filename: 'remoteEntry.js',
      remotes: {
        Auth:    'Auth@/auth/remoteEntry.js',
        Cart:    'Cart@/cart/remoteEntry.js',
        Orders:  'Orders@/orders/remoteEntry.js',
        Account: 'Account@/account/remoteEntry.js',
        Support: 'Support@/support/remoteEntry.js',
      },

      // ── THE SHARED BLOCK IS THE WHOLE CONTRACT ───────────────
      // Every key here MUST appear identically in every remote's
      // webpack.config.js. Module Federation matches by package
      // name string at runtime — any drift creates a duplicate
      // copy and the singleton contract is broken.
      shared: {
        react:              { singleton: true, requiredVersion: dependencies.react },
        'react-dom':        { singleton: true, requiredVersion: dependencies['react-dom'] },
        'react-router-dom': { singleton: true, requiredVersion: dependencies['react-router-dom'] },
        '@reduxjs/toolkit': { singleton: true, requiredVersion: '^2.6.0' },
        'react-redux':      { singleton: true, requiredVersion: '^9.2.0' },

        // The store itself — strictVersion forces a hard fail if
        // any remote ships a different version at deploy time.
        '@myapp/store': {
          singleton: true,
          strictVersion: true,
          requiredVersion: '1.0.0',
        },
      },
    }),
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
        },
      },
    },
  },
};

The differences between local and production are mechanical — same shared block, same singleton declarations, only the surrounding plumbing changes:

AspectLocal DevelopmentProduction / Server
modedevelopmentproduction
output.filename[name].bundle.js[name].[contenthash].js
Remote URLs'Auth@https://localhost:4001/remoteEntry.js''Auth@/auth/remoteEntry.js'
devServerHTTPS + CORS headers + port 4000Not present
optimization.splitChunksfalse (fast HMR)chunks: 'all' with vendor groups
optimization.moduleIdsDefault'deterministic' (cache-friendly hashes)
Shared blockIdenticalIdentical

Each remote uses the same shared block (matched byte-for-byte) so Module Federation can resolve every singleton at runtime.

apps/Cart/webpack.config.js — REMOTE shared block (matches host)
// apps/Cart/webpack.config.js — React REMOTE shared block (matches host)
const { ModuleFederationPlugin } = require('webpack').container;
const dependencies = require('./package.json').dependencies;

module.exports = {
  // ... entry, output, devServer omitted for brevity
  plugins: [
    new ModuleFederationPlugin({
      name: 'Cart',
      filename: 'remoteEntry.js',
      exposes: {
        './CartMFE':         './src/components/CartMFE.jsx',
        './MiniCartPreview': './src/components/MiniCartPreview.jsx',
      },

      // ── This block MUST be byte-identical to the host's shared block.
      // Module Federation matches by package name string. Any divergence
      // (different requiredVersion, missing key, different singleton flag)
      // creates a duplicate copy at runtime and breaks the singleton.
      shared: {
        react:              { singleton: true, requiredVersion: dependencies.react },
        'react-dom':        { singleton: true, requiredVersion: dependencies['react-dom'] },
        'react-router-dom': { singleton: true, requiredVersion: dependencies['react-router-dom'] },
        '@reduxjs/toolkit': { singleton: true, requiredVersion: '^2.6.0' },
        'react-redux':      { singleton: true, requiredVersion: '^9.2.0' },
        '@myapp/store': {
          singleton: true,
          strictVersion: true,
          requiredVersion: '1.0.0',
        },
      },
    }),
  ],
};

// What every key buys you:
//
//   react / react-dom         → Two React copies = two contexts = two stores.
//   react-redux               → Two react-redux copies = useSelector reads
//                                from one store, dispatch writes the other.
//   @reduxjs/toolkit          → Two RTK copies = two action-type registries.
//                                Auth's setIsLoggedIn fires action type
//                                "user/setIsLoggedIn@1" while Cart's reducer
//                                listens for "user/setIsLoggedIn@2".
//   @myapp/store              → The store itself. strictVersion turns a
//                                silent runtime drift into a loud build
//                                failure. Pick one — both options recover
//                                gracefully; nothing else does.
⚠️

Never let the shared block drift between host and remote. Module Federation matches shared dependencies by exact package name string. A remote that lists '@myapp/store' without singleton: true, or with a different requiredVersion, ships its own copy of the store at runtime. The two stores look identical to the developer but are different memory references — dispatch in one is invisible to useSelector in the other. The same discipline from shared dependencies in Module Federation applies to the store package.

Step 5 — Provider Setup in the Host

The host wires the <Provider> at the very top of the React tree so every federated remote becomes a descendant of the same store. The exact wiring differs between a React host and a Next.js host because of how each framework boots — but both end up with the SAME store singleton at the React root.

React host: bootstrap.js with <Provider> + <BrowserRouter>

apps/Main/src/index.js + apps/Main/src/bootstrap.js
// apps/Main/src/index.js — entry that lazily loads bootstrap.js
import('./bootstrap');

// Why a separate index.js → bootstrap.js indirection?
//
// Module Federation needs the shared scope (react, react-dom,
// @myapp/store, ...) to be resolved BEFORE any code that uses
// them runs. The dynamic import('./bootstrap') gives webpack a
// hook to fetch every remoteEntry.js + every shared singleton
// first. If you put the React boot code directly in index.js,
// the host renders before remotes have negotiated their
// singletons → "Shared module is not available for eager
// consumption" build failure.

// apps/Main/src/bootstrap.js — actual React boot
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider, store } from '@myapp/store';   // <- federated singleton
import App from './App.jsx';
import './index.css';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>
);

// What's important here:
//
//   1. Provider wraps the ENTIRE app, including the lazy-loaded
//      remotes. Every <Auth.Login />, <Cart.MiniCart />, and
//      every component a remote exposes ends up downstream of
//      THIS Provider in the React tree.
//
//   2. There is exactly ONE 'store' import. The store is created
//      inside @myapp/store/src/store.js and exported as a const.
//      Importing 'store' from anywhere returns the same reference.
//
//   3. No new <Provider> ever appears inside a remote's exposed
//      component. Wrapping a remote's component in <Provider>
//      again creates a SECOND react-redux context and useSelector
//      starts reading from whichever Provider is closer in the
//      tree. The singleton store is fine; the duplicated context
//      is the bug.

The index.jsbootstrap.js indirection is non-negotiable for any host that consumes federated remotes. Without it, webpack's eager-consumption check fires and the build crashes at runtime with Shared module is not available for eager consumption.

Next.js host: ClientReduxProvider with ssr: false

A Next.js host cannot wrap the Provider directly in _app.tsx because Module Federation containers attach to window, and window does not exist during the server build. The pattern is a thin client-only wrapper imported with next/dynamic.

apps/MainNext/components/ClientReduxProvider.tsx + _app.tsx
// apps/MainNext/components/ClientReduxProvider.tsx — Next.js host
'use client';
import { ReactNode } from 'react';
import { Provider, store } from '@myapp/store';

interface Props { children: ReactNode; }

export default function ClientReduxProvider({ children }: Props) {
  return <Provider store={store}>{children}</Provider>;
}

// apps/MainNext/pages/_app.tsx — Pages Router host
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';

// CRITICAL: ssr: false on the Redux Provider wrapper.
//
// Module Federation containers attach to window. The federated
// @myapp/store can only initialize in the browser — importing
// the Provider directly at the top of _app.tsx triggers a
// 'window is not defined' crash during 'next build'.
//
// next/dynamic with ssr: false ensures Next.js never tries to
// render <Provider store={store}> on the server.
const ClientReduxProvider = dynamic(
  () => import('../components/ClientReduxProvider'),
  { ssr: false, loading: () => null }
);

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ClientReduxProvider>
      <Component {...pageProps} />
    </ClientReduxProvider>
  );
}

// The Next.js host wires Redux DIFFERENTLY from the React host:
//   - React host:  bootstrap.js + <Provider> at the React root
//   - Next.js host: ClientReduxProvider + next/dynamic ssr:false
//
// Both end up with one <Provider store={store}> as the
// closest ancestor of every federated remote. The store
// reference is the same singleton in both cases.

The Next.js half is covered in detail in the shared Redux store in Next.js Module Federation guide — including the makeServerStore factory and the SSR/CSR trade-offs.

Step 6 — Read and Write the Store from Any Remote

Every remote — React or Next.js — uses the same imports. The hooks come from @myapp/store, the selectors come from @myapp/store, the action creators come from @myapp/store. There is no Provider import inside an exposed component because the host's Provider is already up the tree.

React remote: dispatch from Auth's OTPVerify

apps/Auth/src/components/OTPVerify.jsx
// apps/Auth/src/components/OTPVerify.jsx — React remote
import React, { useState } from 'react';
import { handleVerifyMobileOTP } from '@myapp/api/auth.js';
import {
  useAppDispatch,
  useAppSelector,
  selectUser,
  setAt,
  setIsLoggedIn,
  setUser,
} from '@myapp/store';                  // <- the federated package

const OTPVerify = ({ onNavigate }) => {
  const dispatch = useAppDispatch();
  const user     = useAppSelector(selectUser);

  const [otp, setOtp] = useState(['', '', '', '', '', '']);
  const mobile = sessionStorage.getItem('mobileNumber') || '';

  const verifyOTP = async () => {
    const code = otp.join('');
    const res  = await handleVerifyMobileOTP({ mobile, code });

    if (res?.token) {
      // Dispatching INTO THE HOST'S STORE from a React remote.
      // The dispatch is processed by the host's reducer chain.
      // Every other remote (Cart, Account, Header...) that has a
      // useSelector subscribed to the user slice re-renders within
      // a microtask. No event bus, no postMessage, no manual sync.
      dispatch(setAt({ at: res.token }));
      dispatch(setUser(res.user));
      dispatch(setIsLoggedIn({ isLoggedIn: true }));

      onNavigate?.('/');
    }
  };

  return (
    <div>
      {/* OTP input grid omitted */}
      <button onClick={verifyOTP}>Verify</button>
    </div>
  );
};

export default OTPVerify;

// What is NOT in this file:
//   - No createStore() / configureStore() call
//   - No new <Provider> wrapping the component
//   - No useSelector imported from 'react-redux' directly
//
// Every Redux primitive comes from @myapp/store. That single
// import chain is the entire reason this remote's dispatch
// reaches the host's store and propagates to every other remote.

Next.js remote: read + dispatch from Products' AddToCartButton

apps/Products/components/AddToCartButton.tsx
// apps/Products/components/AddToCartButton.tsx — Next.js remote
'use client';
import {
  useAppDispatch,
  useAppSelector,
  selectIsLoggedIn,
  selectAccessToken,
  addToCart,
} from '@myapp/store';

interface Props {
  id: string; name: string; price: number; image: string;
}

export default function AddToCartButton({ id, name, price, image }: Props) {
  const dispatch     = useAppDispatch();
  const isLoggedIn   = useAppSelector(selectIsLoggedIn);
  const accessToken  = useAppSelector(selectAccessToken);

  const handleClick = () => {
    if (!isLoggedIn) {
      // Bridge back to the Auth remote — host owns routing.
      window.location.href = '/login';
      return;
    }

    // accessToken is the SAME token the Auth remote dispatched
    // five minutes ago. No prop drilling, no localStorage
    // round-trip — the federated singleton makes it directly
    // readable from any remote in the tree.
    dispatch(addToCart({
      id, name, price, image, qty: 1, authToken: accessToken,
    }));
  };

  return <button onClick={handleClick}>Add to Cart</button>;
}

// Same dispatch + same selectors as the React Auth remote above.
// The proof the singleton is working: log in via Auth, open Redux
// DevTools attached to the host, navigate to /products — Products
// sees user.isLoggedIn = true and the same accessToken without
// any cross-remote messaging code.

The two files use the SAME imports, the SAME hooks, the SAME selectors. The only difference is 'use client' at the top of the Next.js file — required by the Next.js framework, not by the federated store.

Federated Redux store dispatch flow showing Auth remote dispatching setIsLoggedIn into the host singleton store and Products + Cart remotes re-rendering via useSelector

How Module Federation Negotiates the Store at Runtime

The runtime sequence is what makes the singleton binding hold. The shared scope is webpack's runtime registry of every shared package; every host and every remote contributes its declarations at boot, and the highest-resolution version wins (or, with strictVersion, the build fails).

Runtime negotiation — first dispatch from a remote to the host's store
# How Module Federation Negotiates the Store at Runtime
# ───────────────────────────────────────────────────────
#
# 1. Browser loads the host's main bundle
#    - webpack runtime parses the host's shared block
#    - Registers '@myapp/[email protected]' as ELIGIBLE in the shared scope
#    - Host's bootstrap.js imports { store } from '@myapp/store'
#    - Host LOADS the store package → store instance created in memory
#    - Shared scope now records '@myapp/[email protected]' as RESOLVED
#
# 2. User clicks /cart → host fetches Cart remote
#    - Browser GET /cart/remoteEntry.js
#    - Cart's container manifest declares: 'I share @myapp/[email protected]'
#    - webpack runtime checks shared scope:
#        Is @myapp/[email protected] resolved? → YES (the host already loaded it)
#    - Cart's import of '@myapp/store' is REWIRED to point at the
#      host's already-loaded module instance
#    - Cart's useSelector calls the same react-redux that subscribes
#      to the same store
#
# 3. dispatch(addToCart()) inside Cart
#    - addToCart action creator (loaded from host's @myapp/store) fires
#    - Reducer chain (loaded from host's @myapp/store) runs
#    - State updates inside the host's store object
#    - react-redux notifies every component subscribed via useSelector
#    - Header (host), MiniCart (Cart remote), AccountDropdown (Account
#      remote) — all re-render with the new totalCartItems
#
# Why strictVersion: true matters here:
#
#   If a deployed Cart remote shipped @myapp/[email protected] (a slice
#   shape change) and the host runs @myapp/[email protected]:
#
#     • Without strictVersion: Module Federation picks the higher
#       version, swaps in 1.1.0, breaks the host's reducers silently
#     • With strictVersion: federation refuses to load Cart, surfaces:
#         "Unsatisfied version 1.1.0 from Cart of shared singleton
#          module @myapp/store (required =1.0.0 from Main)"
#
#   The second behavior is the ONE you want — it converts a
#   silent production bug into a noisy deploy failure that the
#   release pipeline catches before users see anything.

The whole guarantee comes down to one store object in memory, accessed through one shared module reference. The webpack runtime is what enforces it. Without singleton: true, the runtime would freely load a second copy if a remote requested a different version. With singleton: true AND strictVersion: true, the runtime refuses any version mismatch and surfaces a loud error in the console — easy to catch in CI and impossible to miss in production.

Each Remote Keeps Its Own Standalone Provider

Every remote in this architecture also has its own bootstrap.js with <Provider> — even though the host's Provider already wraps it in production. This duplication exists for one reason: standalone development. A developer working on the Cart remote alone can run cd apps/Cart && npm start and visit https://localhost:4002 directly, where there is no host to provide a <Provider>. The remote's own bootstrap.js wraps itself with one so that useSelector and useDispatch still work in isolation.

apps/Cart/src/bootstrap.js — standalone wrapper for solo dev
// apps/Cart/src/index.js — entry for STANDALONE remote dev
import('./bootstrap');

// apps/Cart/src/bootstrap.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider, store } from '@myapp/store';   // <- LOCAL store
import CartMFE from './components/CartMFE.jsx';

const root = createRoot(document.getElementById('root'));

root.render(
  <BrowserRouter>
    <Provider store={store}>
      <CartMFE />
    </Provider>
  </BrowserRouter>
);

// WHY DOES EVERY REMOTE HAVE ITS OWN <Provider> WRAPPER FILE?
//
// Standalone development. Every remote can run in isolation:
//   cd apps/Cart && npm start     → opens https://localhost:4002
//
// In standalone mode there is NO host to provide a <Provider>.
// The remote's own bootstrap.js wraps itself with one so that
// useSelector + useDispatch still work during local component
// development. The store is the same federated singleton — it
// just happens to be the only consumer in the standalone scenario.
//
// CRITICAL CONVENTION:
//   - Wrap remote's standalone bootstrap with <Provider>
//   - Do NOT wrap exposed components with <Provider>
//
// Wrong:
//   // apps/Cart/src/components/CartMFE.jsx
//   export default function CartMFE() {
//     return (
//       <Provider store={store}>      // <- DUPLICATE Provider
//         <CartContents />
//       </Provider>
//     );
//   }
//
// Right:
//   export default function CartMFE() {
//     return <CartContents />;        // <- host's Provider is up the tree
//   }
//
// A duplicated Provider creates a NESTED react-redux context.
// Both contexts point to the same store (singleton), so state
// is consistent — but React emits a warning and any component
// that uses useSelector will read from whichever Provider is
// CLOSER in the tree. Subtle, hard-to-debug bugs follow.

The convention is strict: only the remote's standalone bootstrap.js wraps with <Provider>. Components exposed via Module Federation (the ones in the exposes block of webpack.config.js) MUST NOT include a <Provider> in their rendered tree. They rely on the host's provider being the closest ancestor when loaded inside the host.

Debugging the Federated Store

The single most useful debugging tool is the Redux DevTools browser extension (opens in a new tab) — combined with a one-line dev-only window helper.

packages/core/store/src/store.js — dev-only window helper
// packages/core/store/src/store.js — dev-only window helper
import { configureStore } from '@reduxjs/toolkit';
// ... slices ...

export const makeStore = () => configureStore({ /* ... */ });
export const store = makeStore();

if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
  // Expose the store on window for debugging from the console.
  // SAFE in development only — leaving the store on window in
  // production leaks user state to any third-party script.
  window.__myapp_store = store;
}

// In the browser DevTools:
//
//   __myapp_store.getState()
//     → dump full state, including user, cart, tickets, chat
//
//   __myapp_store.getState().user.isLoggedIn
//     → quick auth flag check
//
//   __myapp_store.dispatch({ type: 'user/clearUser' })
//     → log out from the console
//
// Combined with the Redux DevTools browser extension, this is
// the single most useful debug tool when verifying a federated
// store. Every remote (Auth, Cart, Products, Orders) shows up
// as actions in the SAME DevTools instance — proof the
// singleton is working.

In the browser DevTools console, __myapp_store.getState() dumps the full state at any moment. Every remote's dispatch (Auth, Cart, Products, Orders) shows up in the SAME Redux DevTools instance — proof the singleton is working. If you see two stores in the DevTools Stores panel, the singleton is broken; jump to the gotchas section.

Federated Redux store debugging diagram showing one Redux DevTools instance receiving dispatches from Auth, Cart, Orders, and Products remotes through the singleton store

Common Redux Store Micro Frontend Gotchas in Production

After running this pattern across multiple production MFE deployments, eight gotchas account for almost every "state is not propagating" or "store is not working" incident. Every one of them is either a singleton declaration that drifted, a Provider in the wrong place, or a React/Next.js subtlety around when the federated container can run.

Federated store gotchas — symptom, cause, fix
# Federated Redux Store — Production Gotchas
# ────────────────────────────────────────────
#
# 1. Two stores at runtime
#    SYMPTOM: Auth dispatches setIsLoggedIn(true). Cart's
#             useSelector still returns false.
#    CAUSE:   A remote either forgot to declare '@myapp/store' as
#             singleton, OR imported react-redux directly instead
#             of via @myapp/store. Either creates a fresh store.
#    FIX:     Audit every webpack.config.js shared block. Every
#             remote must list:
#               '@myapp/store':   { singleton: true, strictVersion: true, requiredVersion: '1.0.0' }
#               'react-redux':    { singleton: true, requiredVersion: '^9.2.0' }
#               '@reduxjs/toolkit': { singleton: true, requiredVersion: '^2.6.0' }
#
# 2. Strict version refusal at deploy
#    SYMPTOM: New Cart deploy fails to load. Console:
#               'Unsatisfied version 1.1.0 from Cart of shared
#                singleton module @myapp/store (required =1.0.0)'
#    CAUSE:   Cart bumped @myapp/store to 1.1.0 but the host and
#             other remotes still ship 1.0.0.
#    FIX:     Bump @myapp/store EVERYWHERE in one release —
#             host + every remote rebuild + redeploy together.
#             strictVersion does not allow drift.
#
# 3. 'Shared module is not available for eager consumption'
#    SYMPTOM: webpack build fails. The host's bootstrap import
#             of @myapp/store throws at runtime.
#    CAUSE:   The host imports React/Redux/store directly inside
#             index.js (the entry file). The shared scope hasn't
#             been resolved when the import runs.
#    FIX:     Use the index.js → bootstrap.js indirection pattern.
#             index.js does only:  import('./bootstrap');
#             bootstrap.js holds the actual React.render call.
#
# 4. Nested Provider warnings
#    SYMPTOM: React DevTools shows two Redux Providers in the tree.
#             Console: 'You may not render a Provider inside a
#             Provider'. Some useSelector calls return stale data.
#    CAUSE:   A remote's exposed component wraps its return value
#             in <Provider> on top of the host's <Provider>.
#    FIX:     Remove <Provider> from every exposed component.
#             ONLY the remote's standalone bootstrap.js wraps
#             with <Provider>. Federated components inherit the
#             host's Provider.
#
# 5. window is not defined during Next.js build
#    SYMPTOM: 'next build' fails inside packages/core/store at
#             module load time.
#    CAUSE:   A Next.js page imports from @myapp/store at the
#             module level, which transitively loads react-redux
#             code that touches window during SSR.
#    FIX:     Always import the federated store via a dynamic
#             import with ssr: false. NEVER import @myapp/store
#             at the top of a server-rendered page.
#
# 6. Hydration mismatch on user-aware UI
#    SYMPTOM: Next.js error #418 — 'Hydration failed because the
#             initial UI does not match the server'.
#    CAUSE:   Server rendered <LoginButton /> (no store on server).
#             Client mounted federated store, re-rendered as
#             <AccountMenu /> because user.isLoggedIn = true.
#    FIX:     Render a neutral skeleton on the server, switch to
#             user-specific UI only after hydration:
#               const [mounted, setMounted] = useState(false);
#               useEffect(() => setMounted(true), []);
#               if (!mounted) return <Skeleton />;
#
# 7. Direct 'react-redux' import in a remote
#    SYMPTOM: Works on localhost, breaks in production with
#             'undefined is not a function' from useSelector.
#    CAUSE:   The remote imported { useSelector } from 'react-redux'.
#             webpack bundled its OWN react-redux even though the
#             federated one exists. Two react-redux copies disagree
#             on internal state.
#    FIX:     ALWAYS:  import { useSelector } from '@myapp/store';
#             NEVER:   import { useSelector } from 'react-redux';
#
# 8. Persisted state across HMR reloads
#    SYMPTOM: Dev — log in → edit a slice → HMR refresh → still
#             logged in even though the store should reset.
#    CAUSE:   Module Federation HMR keeps the federated store
#             alive across reloads since it lives in the host's
#             module graph, not the remote's.
#    FIX:     Not a real bug. To force-reset for a test:
#               window.__myapp_store.dispatch({ type: 'RESET_ALL' })
#             after wiring a root reducer that handles RESET_ALL
#             by returning every slice's initialState.

If you remember one debugging step: count the stores in Redux DevTools. If there is more than one, the singleton is broken. The fix is always either adding a missing singleton: true flag in some remote's shared block, removing a duplicate <Provider> from an exposed component, or replacing an import { useSelector } from 'react-redux' with import { useSelector } from '@myapp/store'.

What's Next

You now have a complete federated Redux store — @myapp/store package with configureStore, slices, typed hooks, and re-exported react-redux primitives; webpack shared blocks (local + production) with singleton: true and strictVersion: true; a React host wired through bootstrap.js and <Provider>; a Next.js host wired through ClientReduxProvider + next/dynamic with ssr: false; and React + Next.js remotes that read and write the same store with no event bus, postMessage, or localStorage round-trip. The next article — Article 26: Redux Toolkit Slices for MFE State — drills into slice design at scale: how to split a growing store across multiple slices, when to extract a feature into its own slice file, the naming conventions that keep selectors collision-free across remotes, and the patterns for cross-slice derivations using createSelector. After that, Article 27 covers custom hooks that compose multiple selectors into a single reusable contract, and Article 28 wraps Section 4 with permission-based routing built on top of the same federated store.

← Back to Content Security Policy in Next.js Micro Frontend

Continue to Redux Toolkit Slices for MFE State →


Frequently Asked Questions

How do I share a Redux store across micro frontends?

Publish the store as a workspace package (for example @myapp/store) that exports a single configureStore instance, then federate it with Module Federation as a singleton with strictVersion: true and requiredVersion: '1.0.0'. The host imports { store } from @myapp/store inside its entry file (bootstrap.js for a React host, ClientReduxProvider wrapped in next/dynamic with ssr: false for a Next.js host) and wraps the entire app with <Provider store={store}>. Every remote declares the same shared block in its webpack.config.js or next.config.js. At runtime, the first import of @myapp/store loads the package and creates the store; every subsequent import inside any remote receives the same reference. dispatch() in a React Auth remote is visible to useSelector() in a Next.js Products remote within a microtask, with no event bus or postMessage code.

Why does each remote need to declare @myapp/store as singleton in its shared block?

Module Federation matches shared dependencies by package name string at runtime. If only the host declares @myapp/store as a singleton and a remote omits it (or declares it without singleton: true), the remote's webpack bundle includes its own copy of the store package. At runtime you end up with TWO physical store instances — one created by the host, one created by the remote. dispatch() in the remote mutates the remote's store; useSelector() in the host reads from the host's store; they never see each other's updates. The fix is identical declarations in EVERY workspace: 'react', 'react-dom', 'react-redux', '@reduxjs/toolkit', and '@myapp/store' all flagged singleton: true with the same requiredVersion. Adding strictVersion: true on the store turns a silent runtime drift into a deploy-time build failure if any remote ships a different version.

Why does the React host need a separate index.js -> bootstrap.js indirection?

Module Federation needs the shared scope to be resolved before any code that imports a shared dependency runs. If index.js synchronously imports React, react-dom, react-redux, and @myapp/store, webpack tries to evaluate those imports before negotiating the shared scope. That throws Shared module is not available for eager consumption at runtime. The fix is to keep index.js minimal — its only line is import('./bootstrap'). The dynamic import gives webpack a hook to fetch every remote's remoteEntry.js, resolve the shared scope, and only then evaluate bootstrap.js (which contains the React.render call and the imports of react / react-dom / @myapp/store). Both React hosts and React remotes use this two-file pattern; Next.js hosts skip it because Next.js handles the entry-point split internally.

Should an exposed remote component wrap its return in <Provider>?

No. The host already wraps the entire app in <Provider store={store}>, including every federated remote. Wrapping a remote's exposed component in another <Provider> creates a NESTED react-redux context. Both providers point to the same federated store singleton, so state values are consistent, but React emits a warning and useSelector inside that subtree starts reading from whichever Provider is closer in the tree. The convention is: wrap ONLY the remote's standalone bootstrap.js (the file that runs when the remote is opened directly on its own dev server) with <Provider>. Components exposed via Module Federation (CartMFE, MiniCartPreview, OTPVerify) MUST NOT include a <Provider> in their rendered tree — they rely on the host's provider being the closest ancestor when loaded inside the host.

What happens if a Next.js remote tries to read the federated store on the server?

It crashes during next build with ReferenceError: window is not defined. Module Federation containers attach to the window object — the federated @myapp/store package transitively touches window through react-redux internals. On the Next.js server runtime, window does not exist. Importing @myapp/store at the top of a server-rendered page throws on the very first build. The pattern that works is: keep federated state strictly client-side. Wrap the Provider in a ClientReduxProvider component and import it via next/dynamic with ssr: false. The first paint is a plain HTML shell with no user-specific state; the client takes over once the federated container loads. The same trade-off is covered in detail in the Next.js-specific guide on the shared Redux store in Next.js Module Federation. SSR continues to work for everything that does NOT depend on federated state — product titles, marketing copy, blog posts.

Why import useSelector from @myapp/store instead of react-redux directly?

The store package re-exports Provider, useSelector, useDispatch from react-redux, plus typed wrappers useAppSelector and useAppDispatch. Routing every Redux primitive through @myapp/store guarantees that Module Federation's singleton negotiation handles the call. If a remote imports react-redux directly, webpack might bundle a separate copy at build time. The bundled copy and the federated copy disagree on internal state — the bundled useSelector subscribes to whichever store the bundled Provider sees (which may be a fresh one), while the federated useSelector subscribes to the host's singleton. Symptoms include useSelector returning stale values, undefined errors after a deploy that worked locally, or Redux DevTools showing two stores in the Stores panel. Centralizing all Redux primitives behind @myapp/store also lets you upgrade react-redux major versions without touching every remote's import statements.

How do I debug when state is not propagating between micro frontends?

Three checks in order. First, install the Redux DevTools browser extension and open the Stores panel — exactly one store should appear. Multiple stores means singleton negotiation failed and you have a duplicate copy of @myapp/store, react-redux, or @reduxjs/toolkit somewhere. Second, expose the store on window in development (window.__myapp_store = store) and run __myapp_store.getState() from any page; both Auth and Cart should mutate the same object when their respective dispatches fire. Third, audit the shared block in every webpack.config.js and next.config.js — every remote MUST declare @myapp/store, react, react-dom, react-redux, and @reduxjs/toolkit as singletons with the exact same requiredVersion. The most common bug is one remote forgetting react-redux: { singleton: true } in its shared block, which silently bundles its own react-redux and creates a second store instance even though @myapp/store is correctly federated. Adding strictVersion: true to @myapp/store catches the version-drift category at deploy time instead of in production.