Published: May 14, 2026 · 13 min read
Custom Redux Hooks for Micro Frontends Complete Guide
The first hint a federated Redux setup needs custom hooks micro frontend react patterns is a grep result. Run git grep -n "selectIsLoggedIn" in a five-remote workspace and the count comes back somewhere around 80 — every header, every protected route, every checkout step calls useSelector(selectIsLoggedIn) directly. Now imagine renaming isLoggedIn to isAuthenticated in the user slice. Eighty edits across five remotes, each owned by a different team, all needing to ship in one coordinated release. The slices and selectors from the Redux Toolkit slices article already minimized the surface, but raw useSelector calls still leak the slice names into every consumer. Custom hooks fix the leak. One hook per concern — useCurrentUser, useCart, useHeaderViewModel — and every remote imports the hook instead of the raw selectors and actions. When selectIsLoggedIn renames, only the hook's implementation updates; the eighty consumer files stay untouched.
This guide walks the custom-hook patterns that hold up at scale: the foundation hooks (useAppDispatch/useAppSelector), the three legitimate hook shapes (read-only, read+write, view-model), the folder layout inside the federated store package, the two common performance traps that ship to production, and unit-testing the hooks with renderHook.
In this guide, you will:
- See why raw
useSelectorcalls leak the slice contract into every remote and how custom hooks fix it - Build the foundation hooks (
useAppDispatch/useAppSelector) that every other hook composes on top of - Implement a read+write hook (
useCurrentUser) that wraps seven selectors and four actions into one API - Implement a view-model hook (
useHeaderViewModel) that composes three slices withcreateSelectormemoization - Decide between read-only, read+write, and view-model hook shapes per concern with a clear rule
- Avoid the two re-render traps (selector defined inside the hook, returning a new object literal) that look correct at code review
- Lay out the hook folder inside
packages/core/store/src/hooks/so every remote imports from one entry point - Unit test custom hooks with
@testing-library/react'srenderHookand a freshmakeStoreper test

Why Raw useSelector Leaks the Contract
In a single SPA, importing useSelector from react-redux and reading three slice fields inline is fine — the slice file and the component live in the same repo and refactor together. In a federated MFE, the slice file lives in a workspace package consumed by every remote, and every remote is a separately deployed bundle. A useSelector(selectUserName) call site is a permanent dependency on the name selectUserName in the user slice's public surface.
The cost is two-sided. Day-to-day, every junior dev re-discovers the same selector imports, often missing createSelector and wiring a non-memoized filter that re-renders on every dispatch. During refactors, a single slice rename becomes a coordinated PR across the entire workspace.
Custom hooks turn the slice surface into an internal implementation detail. Remotes import useCurrentUser and trust that everything they need — isLoggedIn, name, login, logout — comes back. A slice rename becomes a one-file change inside the hook; the consumers stay untouched. The federated singleton mechanics covered in Module Federation shared dependencies make this safe at runtime, but the discipline that makes it sustainable is the hook abstraction itself.
The Foundation Hooks — useAppDispatch and useAppSelector
Every custom hook in the federated store builds on top of two thin aliases. The aliases exist for the same reason the package re-exports Provider and useSelector from react-redux: they keep every remote on the singleton import path.
The pattern is simple. useAppDispatch is () => useDispatch() and useAppSelector is a re-export of useSelector. In TypeScript the value is bigger — useAppDispatch returns AppDispatch and useAppSelector is typed against RootState, so consumers get autocomplete without passing generics — but even in JavaScript the indirection pays for itself the first time the Redux internals need to change. Migrate from react-redux to a custom middleware stack, switch to useSyncExternalStore directly, add a Redux DevTools wrapper — none of it touches consumer code.
Every remote in the workspace MUST import from @myapp/store, never from react-redux directly. A misconfigured Module Federation shared block can silently load a second copy of react-redux into a remote, and useSelector from the second copy reads from a different React context — the hook returns stale state with no error. The federated package is the singleton boundary; imports outside it are how the singleton breaks.
Pattern One — Read+Write Hook (useCurrentUser)
The most common hook shape is one feature's full surface — every selector that reads the feature plus every action that writes to it, returned as one object. useCurrentUser is the canonical example because authentication state is read by every remote and written by exactly one (Auth).
Three details are load-bearing. First, every read calls useAppSelector with a module-scoped selector reference — never an arrow function defined inside the hook body. React-Redux's referential equality bailout depends on the selector returning the same value reference; an inline arrow recreates the function each render and breaks the bailout. Second, every write is wrapped in useCallback with [dispatch] as the dependency. dispatch is stable across renders by react-redux's guarantee, so the callback memoization is permanent — the returned login, logout, and updateAddress keep the same reference for the lifetime of the component. Third, the returned object has a fixed shape that every remote depends on. Adding a field is a MINOR semver bump on @myapp/store; renaming or removing one is MAJOR.
The consumer side compresses by an order of magnitude.
Five imports become one. Three useSelector calls become zero. The component no longer knows clearUser exists, no longer reads from state.user.*, and no longer breaks when the user slice renames any of its fields.
useCart follows the same shape — one hook wrapping every cart selector and every cart action plus two computed booleans (isEmpty, hasCoupon) memoized with useMemo.
The useMemo wrap around isEmpty and hasCoupon is intentional. The values themselves are primitives that React would diff cheaply anyway, but consumers that pass them into useEffect dependency arrays benefit from referential stability across renders.
Pattern Two — View-Model Hook (useHeaderViewModel)
When one remote needs a precise projection of multiple slices, a view-model hook is the right shape. The Header remote reads login state from the user slice, the cart count from the cart slice, and the open-ticket count from the tickets slice — three subscriptions with no shared lifecycle. A view-model hook collapses them into one memoized selector.
selectHeaderViewModel is the createSelector from the slices article's cross-slice selector section — it lives in selectors/header.js and composes user, cart, and ticket inputs into the shape the Header needs. The hook is one line because all the work is already done in the selector; the hook just exposes it through the package's public API.
The benefit is decoupling. The Header remote no longer depends on userSlice, cartSlice, and ticketSlice individually. It depends on the view-model shape. Refactoring any of the three slices does not touch the Header as long as the view-model output stays compatible. View-model hooks are read-only by design — writes belong in the per-feature hooks above. Mixing reads from three slices with writes for three slices in one hook creates a god-object that re-renders on every dispatch anywhere in the store.

Pattern Three — Read-Only Hook (useIsLoggedIn)
The third shape is the smallest — a hook that returns a single primitive value. Wrapping a one-line selector in a hook is only justified when the field is read in many places across the workspace; for a field read in two components, the indirection is cost without payoff.
The justification is concrete: isLoggedIn is read by every protected route, every header variant, every checkout guard, and several remote-level redirects. That is enough places that a future rename — isLoggedIn → isAuthenticated → loggedInAt (a timestamp instead of a boolean) — would otherwise touch every one of them.
Picking the Right Hook Shape
The three shapes cover every legitimate use case. The decision is mechanical, not stylistic.
| Hook shape | Returns | When to use |
|---|---|---|
Read-only (useIsLoggedIn) | A primitive value | One field read in 10+ places across the workspace |
Read+write (useCurrentUser, useCart) | { ...data, ...actions } | One feature with both reads and writes that travel together |
View-model (useHeaderViewModel) | { ...projected } | One remote needs a precise projection of multiple slices |
| Don't use a hook | — | A field read in 1–2 places — inline useAppSelector is fine |
The anti-patterns matter as much as the patterns. Hooks that fetch network data belong in RTK Query (opens in a new tab) or a dedicated useXxxQuery hook — slice hooks read from and write to the store, nothing else. A mega useStore hook that returns the whole state tree re-renders the consumer on every dispatch and defeats every memoization in the store package. useState wrappers that re-derive Redux state into local component state create a second source of truth that drifts.
Folder Layout in the Federated Store Package
Custom hooks share the same folder discipline as slices and selectors — one file per hook, no barrels inside hooks/, all hooks re-exported through the package's single index.js.
Three folder rules make the convention enforceable. One hook file per hook — useCart.js holds only useCart and its private helpers, never useCart plus useCartItem plus useCheckout. Hooks never import from other hook files — composition belongs inside the consuming component, not inside the store package, because hook-to-hook composition makes the dependency graph cyclic and breaks test isolation. View-model hooks live next to read+write hooks — a flat folder is grep-friendly when the workspace grows to 30+ hooks across the federated store package.
The Public Surface in index.js
Every remote in the workspace imports from one path: @myapp/store. The package's index.js re-exports every hook, every action, every selector, and the store itself. The hooks section is the preferred consumer API; the raw slice surfaces stay exported for advanced consumers (tests, dev tools) but application code defaults to the hooks.
CI should diff this file against the previous release on every PR and fail the build on any deletion. Adding a new hook is a backwards-compatible MINOR semver bump. Removing one is a breaking change that requires a MAJOR bump and a coordinated rebuild of every remote — the same rule that applies to slices in the previous article.
Two Performance Traps That Ship to Production
Custom hooks make the consumer API cleaner, but the hooks themselves still have to play by react-redux's referential-equality rules. Two specific mistakes have shipped to production in real federated stores and both look correct at code review.
The first trap is a selector defined inline with an arrow function inside the hook body. The arrow is recreated on every render, returns a new array reference on every call, and breaks useAppSelector's bailout — every consumer re-renders on every dispatch. The fix is to define selectors at module scope (for cheap reads) or wrap them in createSelector (for any selector that filters, maps, or reduces).
The second trap is more subtle. A hook that returns { data, actions } returns a new object literal on every render. A consumer doing const cart = useCart(); useEffect(..., [cart]) sees cart change on every render and the effect re-fires. The fix is conventional — encourage destructuring at the consumer (const { items, addItem } = useCart()) so the dependency array contains primitive references that stay stable. For hooks that must return one object that's used in useEffect deps, wrap the return in useMemo of the data fields only.
| Trap | Symptom | Fix |
|---|---|---|
| Selector defined inside the hook | Consumer re-renders on every dispatch | Move selector to module scope or use createSelector |
| Hook returns new object literal | Consumer's useEffect([cart]) re-fires on every render | Destructure at consumer or wrap return in useMemo |
Missing useCallback on actions | Child re-renders when callback is passed as prop | Wrap every dispatch callback in useCallback with [dispatch] |
useSelector inside useEffect | Stale closure reads | Read with useAppSelector at hook scope, not inside effects |
Before / After — One Component, Two Worlds
The cleanest way to show the payoff is the same component written both ways. The view-model hook flattens five imports and four hook calls into two imports and two hook calls — and the second hook is only there for the logout action.

Beyond the line count, the bigger win is decoupling. The Header after the refactor depends on useHeaderViewModel and useCurrentUser. It does not import from userSlice, cartSlice, or ticketSlice. A slice rename — selectUserName → selectName, selectTotalCartItems → selectCartCount — touches the hook implementation and nothing else. The Header's unit tests mock two hooks instead of mocking a full <Provider> tree with a fake store.
Unit Testing Custom Hooks
Hooks in the federated store package are the most leveraged code in the workspace — a subtle bug in useCurrentUser breaks every remote silently. They need unit tests. @testing-library/react's renderHook is the standard tool; the only MFE-specific detail is wrapping with a fresh makeStore per test so test cases stay isolated.
The pattern that catches the most bugs: a stability test asserting result.current.login stays the same reference across renders. Forgetting to add [dispatch] to a useCallback is the most common silent regression — the hook still works, but every consumer that passes login to a child re-renders the child on every parent render. The stability test catches the bug at PR time instead of as a perf regression three months later.
Run the test suite in CI before publishing a new version of the @myapp/store package. Any change to a hook that breaks the public contract (renames a returned field, removes an action, changes a callback signature) is a MAJOR semver bump under the Module Federation strictVersion rules (opens in a new tab) that the slices article covered — the tests are what catches the contract drift before it becomes a deploy-failure surprise.
What's Next
You now have the custom-hook patterns that hold up at scale: the foundation hooks (useAppDispatch/useAppSelector) that every other hook builds on, the three legitimate hook shapes (read-only, read+write, view-model), the flat folder layout in packages/core/store/src/hooks/, the single index.js entry point that every remote imports from, the two performance traps that look correct at code review, and the renderHook testing pattern that catches contract drift before it ships. The next article — Article 28: Permission-Based Routing in Micro Frontends — applies these hook patterns to authorization: a usePermissions hook reads the user's role array, a <SellerPermissionRoute> component gates whole MFEs, and a permission-aware menu config hides routes the current user cannot access. The federated store, the slices, and the custom hooks together provide the foundation; permissions ride on top.
← Back to Redux Toolkit Slices for Micro Frontend
Continue to Permission-Based Routing in Micro Frontends →
Frequently Asked Questions
What are custom Redux hooks in a micro frontend?
Custom Redux hooks are React hooks defined inside the shared federated store package (for example @myapp/store) that wrap useSelector and useDispatch into a feature-specific API. Instead of every remote importing five selectors plus four action creators and stitching them together with raw useSelector/useDispatch, the remote imports one hook — useCurrentUser, useCart, useHeaderViewModel — and destructures the data and actions it needs. The hook is the public contract: slice renames change the hook's implementation, every consumer remote stays untouched. In a micro frontend, this matters more than in a single SPA because one slice rename without the abstraction layer means a coordinated PR across five or six remotes.
Where should custom Redux hooks live in a micro frontend workspace?
Custom Redux hooks belong inside the federated store package (packages/core/store/src/hooks/) — never inside a single remote. Putting useCurrentUser inside apps/Cart/ would force Products, Orders, and Account to either copy the file or import across remote boundaries, both of which break federation. The folder layout uses one file per hook (useCurrentUser.js, useCart.js, useHeaderViewModel.js), a flat hooks/ folder next to slices/ and selectors/, and re-exports every hook through the package's single index.js. Every remote imports from '@myapp/store' only — never from a slice file or hook file directly. This keeps the contract surface in one place and makes semver versioning enforceable.
What are the three custom hook shapes for a micro frontend?
Three shapes cover every legitimate use case. Read-only hooks (useIsLoggedIn) return a single primitive value and are justified when the field is read in 10+ places across the workspace. Read+write hooks (useCurrentUser, useCart) return an object with both data fields and action callbacks for one feature; this is the most common shape. View-model hooks (useHeaderViewModel, useCheckoutViewModel) wrap a createSelector that composes two or more slices into the exact shape a single remote needs and return read-only data; writes belong in the per-slice hooks above. Anti-patterns to avoid include mega-hooks that return the whole state tree, hooks that re-derive Redux state into local useState, hooks that fetch network data (use RTK Query), and hooks that return new object literals every render without useMemo.
How do I prevent unnecessary re-renders from custom Redux hooks?
Three rules eliminate most re-render problems. First, define selectors at module scope or wrap them in createSelector — never as arrow functions inside the hook body, because the arrow is recreated each render and breaks useSelector's referential equality bailout. Second, wrap action callbacks in useCallback with [dispatch] in the dependency array so the returned functions keep stable references and consumers that pass them as props do not re-render their children unnecessarily. Third, encourage destructuring at the consumer (const { items, totals } = useCart()) instead of passing the whole hook return object into useEffect dependencies — primitive references stay stable, object literals returned by the hook do not without useMemo. Together these three rules make custom hooks no more expensive than raw useSelector/useDispatch calls.
Should custom hooks return data and actions in the same object?
Yes for read+write hooks like useCurrentUser and useCart, because the data and actions travel together — a component that reads cart items almost always needs addItem, removeItem, and updateQuantity. Returning one object means the consumer writes const { items, addItem } = useCart() instead of importing two hooks. For pure read-only hooks (useIsLoggedIn) return the value directly, not an object — wrapping a boolean in { isLoggedIn } adds destructuring noise with no benefit. For view-model hooks like useHeaderViewModel the return is data-only because writes belong in the underlying per-feature hooks; mixing reads from three slices and writes for three slices into one hook creates a god-object that re-renders the consumer on every dispatch anywhere in the store.
How do I test custom Redux hooks for a micro frontend?
Use @testing-library/react's renderHook with a Provider wrapper that supplies a fresh makeStore() per test. Each test creates a new store, renders the hook through the Provider, and asserts on result.current — the hook's returned object. For read assertions, check the initial values. For write assertions, call result.current.login(...) inside act() and re-check the same fields after the dispatch settles. Always include a stability test: rerender the hook and confirm result.current.login (or any action) keeps the SAME reference across renders — this verifies useCallback is wired correctly and catches the common bug where missing dependency arrays make every render produce new functions. Custom hooks in the federated package MUST be unit tested because a subtle bug breaks every remote in the workspace silently.
What is the difference between useAppSelector and useSelector in a micro frontend?
Functionally they are identical — useAppSelector is a thin alias for useSelector exported from the federated store package. The reason every remote should import useAppSelector from @myapp/store instead of useSelector from react-redux is twofold. First, the alias gives the workspace one place to add typing (in TypeScript useAppSelector is typed against RootState; the plain useSelector returns unknown without a generic argument), and one place to change Redux internals later — a future migration to a different store library would only touch hooks.js. Second, importing through @myapp/store guarantees the singleton path: a remote that imports useSelector directly from react-redux risks pulling its own bundled copy if the Module Federation shared block is misconfigured, which causes the hook to read from a different React context and return stale state across remotes.