Published: May 13, 2026 · 12 min read
Redux Toolkit Slices for Micro Frontend Architecture
A growing micro frontend hits a wall around slice number five. The cartSlice started with three fields and now carries items, totals, wishlist, savedForLater, deliveryAddress, deliveryWindow, and paymentMethod. The Wishlist remote re-renders on every cart total change because selectWishlist runs through the entire cart state. A new developer adds a setUser action to the auth slice, names it updateUserProfile instead, and breaks every remote that still imports setUser from @myapp/store. These are not Redux problems — they are redux toolkit micro frontend slice-design problems. Slices in an MFE are the contract between independent remotes; every reducer name, every selector name, and every initial-state field is a public string that every remote depends on. In the previous article on sharing a Redux store across micro frontends, you wired a singleton federated store with configureStore, slices, and <Provider>. This article zooms into the slices themselves — how to design them so they survive growth, splits, and coordinated deploys without breaking remotes.
This guide walks the slice patterns that hold up at scale: the five-section file layout, naming conventions for collision-free remotes, the rule for when one slice should become two, createSelector for cross-slice derivations, and the semver versioning strategy that ties slice changes to coordinated releases of the federated @myapp/store package.
In this guide, you will:
- Understand why slice design matters more in an MFE than in a single SPA — every name is a public contract
- Learn the five-section slice file layout (imports, initialState, createSlice, selectors, exports)
- Apply naming conventions that prevent silent collisions across remotes (
selectXxx,setXxx,addXxx, slice name = reducer key) - Decide when to split a slice into two with the "two dispatch surfaces + half-state selector" rule
- Use
createSelectorfor memoized cross-slice derivations that keep referential stability across dispatches - Re-export every slice through the single
@myapp/storeentry point that every remote imports - Apply a semver versioning strategy to slice changes — PATCH adds a field, MAJOR renames one
- Lock the contract with ESLint rules so contract drift fails CI instead of failing in production

Why Slice Design Is Different in a Micro Frontend
In a single-SPA Redux project, slices are an internal organization concern — rename setUser to updateUser and the only people affected are the developers in the same repo. In a federated MFE, the same rename is a breaking change for every remote that ships independently. The slice file is the contract between teams that may not coordinate deploys.
The contract surface is bigger than it looks. Every reducer becomes an action creator with the type 'sliceName/reducerName'. Every selector is a function imported by name. Every initial-state field is read by every selector. A rename in any of these breaks every downstream remote that depends on the previous name — and because remotes deploy independently, you can ship the new contract from the host while old remote bundles in the CDN still reference the old names.
The slice rules below are stricter than what redux-toolkit.js.org recommends for a single SPA. That is intentional — single-SPA conventions optimize for refactor speed. MFE conventions optimize for cross-remote contract stability. Both are correct; they apply to different contexts. The federated singleton mechanics that make these contracts load-bearing are covered in Module Federation shared dependencies.
The Five-Section Slice File Layout
Every slice file in the federated store follows the same five-section layout. Consistency matters more than cleverness — a developer opening any slice file in any remote should know exactly where to find the initial state, the reducers, the selectors, and the exports without scrolling.
The five sections in order: imports, initialState (the public schema), createSlice (the reducer logic), selectors (the public read API), exports (actions + reducer). Cart, ticket, and chat slices follow exactly the same five-section template — open any of them and the structure is identical.
The cart slice is owned by both the Cart and Products remotes (Products dispatches addToCart, Cart dispatches updateQuantity). The ticket slice is owned by the Support remote but read by Account and Header for badge counts. The chat slice is intentionally tiny — once chat grows attachments, typing indicators, and unread counts, it will split (the next section explains the trigger).
Naming Conventions for Collision-Free Remotes
Every name in a slice file becomes a string that every remote imports. Pick the convention once, write it down, and enforce it through ESLint. Drift between remotes is how the silent "selector returns undefined" bugs ship to production.
The single most important rule is slice name = reducer key. The slice's name: 'cart' field, the reducer key cart: cartSlice.reducer in configureStore, and every selector's state.cart.* access path must all use the same string. Mixing 'Cart' and 'cart' (or 'auth' and 'user') creates two parallel paths through the state tree where one of them silently returns undefined.

Selectors get the strictest rule because they are imported by name from across the workspace. Every selector starts with select. Booleans use selectIsXxx or selectHasXxx. Lists use the plural form (selectCartItems). Derived values describe the result (selectTotalCartItems, selectFreeShippingItems). When you see import { setUser, selectUser } from '@myapp/store' in a remote, the prefix tells you immediately which one is the action and which one is the read.
Wiring Slices Into the Federated Store
The configureStore call registers every slice's reducer under a key that must equal the slice's name string. Mismatches between the two break every selector silently — state.user is undefined because the reducer is registered as state.User.
The pattern that matters: one folder for slices, one file for the store. The store file imports every slice by name and registers each reducer under a key matching the slice's name field. Future slices follow the same line: import → register under the matching key → re-export from index.js.
The single public entry point — index.js — is the only file remotes import from. Every named export here becomes part of the @myapp/store contract that survives across deploys.
CI should diff this file against the previous release on every PR and fail the build on any deletion. Adding a new export is backwards-compatible (a MINOR semver bump). Removing one is a breaking change that requires a MAJOR bump and coordinated rebuild of every remote — covered in the versioning section below.
When Should One Slice Become Two?
Slices grow. The first version of cartSlice had items and totalItems. Six months later it has wishlist data, delivery preferences, and payment methods. The split rule is concrete: split when (a) two distinct features dispatch to the slice AND (b) at least one selector touches only half of the slice's state.
The symptoms come first: selectWishlist re-runs on every cart total change because both live in the same slice; addToWishlist and addToCart grow separate validation rules but share an action namespace; the Wishlist remote re-renders when cart totals update. The fix is splitting into three single-purpose slices, each owned by one remote.
| Aspect | Before — one giant slice | After — three single-purpose slices |
|---|---|---|
| Owner remotes | Cart + Wishlist + Checkout (shared) | Cart owns cart, Wishlist owns wishlist, Checkout owns checkout |
useSelector(selectWishlist) recomputes when... | Cart totals change | Only when wishlist items change |
useSelector(selectCart) recomputes when... | Wishlist items change | Only when cart items change |
| Breaking change blast radius | Touches 3 teams' deploys | Touches 1 team's deploy |
index.d.ts size | One large CartState type | Three focused types |
| Onboarding cost | Read a 400-line slice file | Read three 100-line slice files |
Splitting is not free — every consumer that imports selectAddress from the old cartSlice needs to update to importing from checkoutSlice. That is a MAJOR semver bump on @myapp/store and a coordinated rebuild of every remote (covered below). The migration cost is the price of getting an isolated, single-purpose slice that one team owns end-to-end.
Do not split prematurely. Three fields in one slice with one consumer remote is fine — it stays one slice. The trigger is real production friction (re-renders, conflicting dispatches, growing breakage radius), not theoretical purity. Premature splits create more files than necessary and make the cross-slice selector folder grow without payoff.
Memoize Derived Reads with createSelector
A naive derived selector re-runs and returns a brand-new reference on every dispatch — even if the input data did not change. In a single SPA that wastes a few microseconds. In an MFE where every federated remote subscribes to the same store, a non-memoized selector runs once per subscribed remote on every state change.
createSelector is re-exported from @reduxjs/toolkit (it wraps the underlying reselect (opens in a new tab) library). The signature is createSelector([inputSelectors], outputCalculator). The output calculator runs only when at least one input selector returns a different reference than last time. Every subsequent call with unchanged inputs returns the cached output — same array reference, no useSelector re-render in any subscribed remote.
The pattern shines for cross-slice composition: a Header view-model that needs login state, cart count, and ticket data should be one memoized selector, not three separate useSelector calls.
selectHeaderViewModel lives in selectors/, not in any single slice file. The rule is: single-slice selectors live in the slice file; cross-slice selectors live in selectors/. Putting selectHeaderViewModel inside cartSlice.js would make cartSlice import from userSlice and ticketSlice, breaking the rule that slice files only depend on @reduxjs/toolkit.

Slice Folder Layout for a Real MFE
The folder layout makes the rules visible. Every slice file is a sibling under slices/. Every cross-slice selector is a sibling under selectors/. There is no barrel index.js inside slices/ — barrels confuse Module Federation tree-shaking (opens in a new tab) and force the entire slices folder into every remote's bundle.
The three folder rules: one slice file per slice, single-slice selectors live in the slice file, cross-slice selectors live in selectors/. Every action creator is the only way state mutates; every selector is the only way state is read. Business logic does not live anywhere else.
Slice Versioning — Semver Bound to the Federated Package
Slice changes are package changes. Every modification to a slice file translates to a semver bump on @myapp/store, and Module Federation enforces the bump through the shared block's strictVersion: true flag.
The pattern that keeps the workflow sustainable:
| Change | Semver | Rebuild every remote? | Why |
|---|---|---|---|
Add a field to initialState | PATCH 1.0.1 | No | Old remotes ignore the new field |
| Add a new reducer / action | MINOR 1.1.0 | No | Old remotes never call it |
| Add a new selector | MINOR 1.1.0 | No | Old remotes never import it |
| Add a new slice | MINOR 1.1.0 | No | Old remotes do not register it |
| Rename a field | MAJOR 2.0.0 | Yes | Selectors that read by name return undefined |
| Rename a reducer / action | MAJOR 2.0.0 | Yes | Old action creator becomes "is not a function" |
| Rename a selector | MAJOR 2.0.0 | Yes | Imports of the old name fail at build time |
Rename a slice (its name) | MAJOR 2.0.0 | Yes | state[oldName] disappears, every selector breaks |
| Remove a field / action / selector | MAJOR 2.0.0 | Yes | Old consumers throw or return undefined |
PATCH and MINOR are the common case — they ship without coordination. MAJOR bumps belong in scheduled release windows because every remote in the workspace has to be rebuilt and redeployed in the same window. Module Federation's strictVersion: true catches half-deployed states by refusing to load any remote that ships a different requiredVersion — the deploy fails loudly instead of silently shipping stale selectors.
Lock the Contract with ESLint Rules
The contract is enforceable. A handful of ESLint rules in the store package catch the common drift before it ships. The rules below are written as project-specific rules; equivalent published rules exist for some of them, but the workspace owns the slice convention so it owns the linter that enforces it.
The most valuable rule is sd/reducer-key-matches-slice-name — it catches the silent class of bugs where a developer copies a slice file, renames the import, and forgets to rename the name string. Without the rule, the second slice silently overwrites the first one's reducer entry; with the rule, CI fails before the PR can merge. Pair the rule set with Redux's own style guide (opens in a new tab) for the rules that apply to any Redux project (immutable updates, action shape, selector colocation).
What's Next
You now have the slice patterns that hold up at scale: the five-section file layout, naming conventions enforced through ESLint, the rule for when one slice becomes two, createSelector for memoized cross-slice composition, the single-entry-point export pattern, the semver versioning strategy bound to strictVersion: true, and the folder layout that makes the conventions visible. The next article — Article 27: Custom Redux Hooks for Micro Frontends — wraps multiple selectors into a single reusable hook contract (useCurrentUser, useCart, useHeaderViewModel) so remotes consume one hook per concern instead of stitching together useSelector and useDispatch calls. After that, Article 28 closes Section 4 with permission-based routing built on top of the same federated store and slice patterns.
← Back to Sharing Redux Store Across Micro Frontends
Continue to Custom Redux Hooks for Micro Frontends →
Frequently Asked Questions
What is a Redux Toolkit slice in a micro frontend?
A Redux Toolkit slice is the file that owns one feature's state inside a federated store. In a micro frontend setup, the slice lives inside the shared workspace package (for example @myapp/store) so every remote — React or Next.js — imports the SAME action creators, reducers, and selectors. createSlice(name, initialState, reducers) generates the reducer plus action creators in one call. The slice's name string ('user', 'cart', 'tickets') becomes a key in the global state tree and a prefix on every action type, which is why naming conventions are stricter in an MFE than in a single SPA — every name is a public string that every remote depends on.
How do I split a Redux store into slices for multiple micro frontends?
Map slices to features, not to remotes. Each slice owns one cohesive feature (user, cart, tickets, chat) and lives in packages/core/store/src/slices/. The Auth remote dispatches into userSlice but does not own it; Cart and Products remotes both dispatch into cartSlice. Splitting by feature instead of by remote keeps the contract stable when a remote's responsibility shifts — moving the cart UI from one remote to another does not require migrating the slice. The store file (configureStore) registers every slice under a key matching its name string. The package's index.js re-exports actions, selectors, and the reducer so every remote imports from a single entry point. Add a new slice when a feature has its own dispatch surface and at least one selector that does not touch any other slice.
What naming convention should slice selectors follow in a micro frontend?
Every selector in a federated slice MUST start with the prefix select — selectUser, selectCart, selectIsLoggedIn, selectTotalCartItems. Boolean selectors use the form selectIsXxx or selectHasXxx. List selectors use the plural form (selectCartItems). Derived/computed selectors describe the result (selectTotalCartItems, selectFreeShippingItems). The select prefix makes every read instantly distinguishable from a write at the import statement: import { selectUser, setUser } from '@myapp/store' tells you setUser is an action creator and selectUser is a read function. Enforce the convention via an ESLint rule because slice files live in a workspace package consumed by every remote — manual review across many repos is unreliable.
When should a slice be split into two separate slices?
Split when (a) two distinct features dispatch into the slice AND (b) at least one selector touches only half of the slice's state. A real example: a cartSlice that grew to include items, totals, wishlist, savedForLater, deliveryAddress, deliveryWindow, and paymentMethod is doing three jobs. Symptoms of an over-grown slice include selectWishlist re-running on every cart total change, addToWishlist and addToCart developing separate dispatch APIs, and the WishlistMFE re-rendering when cart totals update. The fix is to split into cartSlice (items, totals, coupon), wishlistSlice (items, savedForLater), and checkoutSlice (address, deliveryWindow, payment). Each new slice gets its own owner remote, its own action surface, and its own selectors that no longer recompute on unrelated state changes.
Why use createSelector for derived state in a micro frontend store?
createSelector memoizes the result so the derived value keeps the same reference until its inputs actually change. In an MFE this matters MORE than in a single SPA because every federated remote subscribes to the same store — a non-memoized derived selector that recomputes on every state change runs once per subscribed remote. A naive selector returning items.filter(...) returns a brand-new array reference every call, which makes useSelector trigger a re-render in every remote on every dispatch. createSelector wraps the inputSelectors and the outputCalculator: the calculator only runs when an input reference changes, so the derived array stays referentially stable across dispatches that did not touch the cart items. Use createSelector for any selector that runs .filter, .map, .reduce, .sort, or returns a new object literal.
How do I version slices when the schema needs to change?
Treat slice changes as semver changes on the @myapp/store package. PATCH (1.0.0 → 1.0.1) covers adding a new field to initialState — old remotes simply ignore it. MINOR (1.0.0 → 1.1.0) covers adding a new reducer, action, selector, or whole new slice — old remotes still work. MAJOR (1.0.0 → 2.0.0) covers any rename or removal: rename a field, rename a reducer, rename a selector, change a slice's name string, change initialState shape, or delete anything. Because every remote's webpack shared block declares '@myapp/store': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' }, bumping to 2.0.0 forces every remote to be rebuilt and redeployed in the same release — Module Federation will refuse to load a 2.0.0 host next to 1.0.0 remotes, surfacing the mismatch as a deploy failure instead of a silent runtime undefined. PATCH and MINOR bumps are the common case; MAJOR bumps belong in coordinated release windows.
Should cross-slice selectors live inside a single slice file?
No. A selector that reads from two or more slices does not belong to any one feature, so it lives in packages/core/store/src/selectors/ alongside other cross-slice selectors. Examples: selectHeaderViewModel composes user + cart + tickets data into one object the Header remote consumes; selectCheckoutViewModel composes cart + user.address. Keeping cross-slice composition in a separate folder serves three purposes. First, it keeps each slice file focused on one feature, so a developer can read userSlice.js without scrolling past unrelated cart logic. Second, it prevents accidental coupling — putting a cross-slice selector inside cartSlice.js makes cartSlice depend on the userSlice import, which fails as soon as someone tries to test cartSlice in isolation. Third, it keeps the createSelector memoization graph readable: every selector in selectors/ is intentionally cross-cutting, every selector in slices/ is intentionally local.