DEV Community

Michael Gokey
Michael Gokey

Posted on

NgRx State Management Across Angular Modules

Part 2: Structuring Shared State in Large Applications

In Part 1, we explored the basics of NgRx and why a centralized state can help tame complexity in modern Angular applications.

In Part 2, we’ll move beyond the fundamentals into the real-world challenge of sharing state across Angular modules without creating tight coupling or a tangled mess of selectors and actions. (⏱️ Estimated reading time: 4.5 minutes)


Understanding the Problem

Imagine you're building a house with several rooms. Each room serves a different purpose, but everyone in the house needs to know if it’s day or night, hot or cold, or if someone’s at the door.

In Angular, each feature module is like a room, self-contained, but part of the same living space.

Now imagine that instead of a shared thermostat or central lighting control, every room makes its own guess about the time of day or the temperature. That’s what happens in Angular apps when modules try to manage state locally, in isolation; you get duplicate logic, inconsistent behavior, and poor user experience.

This is where NgRx shines. It gives your Angular app a centralized information desk (aka the Store) that every module can subscribe to and contribute to, in a clean, consistent way.

But here’s the catch:
While NgRx makes centralized state management possible, you still need intentional structure to make it scalable across multiple modules.


Review: NgRx Core Concepts

Before we dive into cross-module concerns, let’s do a quick refresher on the building blocks:

  • Store – the single source of truth, holding your application’s state tree.
  • Actions – plain objects that describe state changes.
  • Reducers – pure functions that respond to actions and return new state.
  • Selectors – reusable queries for retrieving slices of state.
  • Effects – handling side effects like API calls or navigation.

These concepts stay the same, but how you organize them matters a lot as your app scales.


The Challenge: Sharing State Without Tangling Modules

A common mistake in large Angular apps is allowing feature modules to directly reach into other modules' states.
This creates coupling and a nasty tangle when teams need to refactor or isolate functionality.

The goal is shared understanding, not shared chaos.

Here’s what we want instead:

  • Encapsulated feature modules, each managing its feature logic.
  • A core shared state slice, defined in a neutral place (like app.state.ts or a shared library).
  • Well-named action contracts and selectors that modules can subscribe to without needing to know who triggered the change.

Strategies for Cross-Module State

Here are a few battle-tested approaches to organizing state across modules:

1. Use Feature States Wisely

Each Angular module should define its own NgRx feature slice via StoreModule.forFeature.
Avoid trying to cram everything into a global root reducer.

This keeps logic scoped, testable, and maintainable.

But what if two modules need to interact?

2. Create a Shared or Global Slice for Cross-Cutting Concerns

Sometimes, multiple modules care about the same data, like user authentication, settings, or permissions.

Put these into a shared state module (e.g., @app/state/core) and use StoreModule.forRoot() to initialize it.

Then expose clear selectors like:

export const selectCurrentUser = createSelector(
  selectAuthState,
  (state) => state.user
);
Enter fullscreen mode Exit fullscreen mode

Other modules can import this shared selector without knowing how the data is stored or updated.

3. Use Facade Services to Hide Store Details

You don’t want every component wiring up selectors and dispatching actions.
Create a facade service per module or domain, something like UserFacade or SettingsFacade to act as the single entry point.

This service:

  • Wraps select() calls into clean, reactive streams (like currentUser$)
  • Dispatches actions behind readable method names (loadUser(), updatePreferences())
  • Decouples components from NgRx boilerplate

Now your UI is easier to test and reason about.

4. Minimize Cross-Domain Dispatches

Avoid letting one module dispatch actions that mutate another module’s state. Instead, use shared actions and let each reducer respond as needed; this keeps boundaries clear.


Case Study: AdminPortal State Structure

In our AdminPortal app, we split our store into three key areas:

  1. Core state
  • Authentication
  • User profile
  • App-level settings
  1. Feature state
  • Each module (e.g., users, dashboard, reports) owns its slice
  • Isolated selectors and reducers
  1. Shared utility state
  • UI loading flags
  • Toaster notifications
  • API error logs

Each module imports only the pieces it needs and uses facade services to access or update them.

This keeps our modules independent, but still able to collaborate.


Wrapping Up: Shared State Without the Pain

Sharing state across Angular modules doesn’t have to feel like a plumbing nightmare.
With the right NgRx structure, and some thoughtful use of shared slices, facades, and selectors, you can build apps that scale without getting brittle.

NgRx isn’t just about centralizing data. It’s about creating clear boundaries, predictable flows, and confident teams who know where things live and how they change.


Coming in Part 3:

Optimizing NgRx for Performance and Developer Experience
We’ll explore:

  • Memoized selectors and why they matter
  • Lazy-loading state modules the right way
  • Developer tooling, best practices, and debugging tips that make life easier

Stay tuned.

Top comments (0)