DEV Community

Bhargav Patel
Bhargav Patel

Posted on

Redux / NgRx State Management in Angular: Part 1

Managing state in Angular applications becomes difficult as applications grow larger and more complex. Redux and NgRx provide a structured and predictable way to handle shared application state.

This article explains:

  • What Redux/NgRx solves
  • Core concepts
  • When to avoid it
  • When it becomes useful
  • Why established libraries are preferred

Alt Text

Understanding State in Angular

State in Angular applications can be anything like:

  • Router state
  • Component-local state
  • Shared state between components

In small applications, services are often enough for sharing data. However, as applications scale, several problems begin to appear:

  • Complex component relationships
  • Difficult state synchronization
  • Duplicate state across components
  • Unpredictable updates

Redux-style architecture solves this problem by introducing a centralized global store.

The store becomes the single source of truth for application state.


Core Redux / NgRx Concepts

Redux/NgRx is built around a few important concepts:

  • Store
  • Actions
  • Reducers
  • Effects
  • Selectors

Store

The store contains the entire application state in one centralized location.

Benefits of using a store:

  • Single source of truth
  • Predictable data flow
  • Easier debugging
  • Better state sharing

Components subscribe only to the state they need instead of maintaining separate copies.


Actions

Actions describe events that happen inside the application.

Actions are plain JavaScript objects that usually contain:

  • type
  • optional payload data

Example:

{
  type: 'LOAD_USERS',
  payload: users
}
Enter fullscreen mode Exit fullscreen mode

Actions help create a clear and trackable update flow.


Reducers

Reducers are functions that receive:

  • Current state
  • Action

Reducers return a new updated state.

Important rules for reducers:

  • Must be pure functions
  • Must not contain side effects
  • Should not perform:
    • API calls
    • Local storage operations
    • Async tasks

Reducers should only focus on transforming state.


Effects (NgRx)

Reducers should only update state and remain pure functions. They should not perform API calls, async operations, or side effects.

NgRx uses Effects to handle side effects separately.

Common use cases for effects:

  • API requests
  • Authentication
  • Local storage updates
  • Async operations
  • Notifications

Effects help keep components clean and business logic centralized.


Selectors

Selectors are functions used to read data from the store.

Instead of directly accessing store data inside components, selectors provide a reusable and organized way to retrieve state.

Benefits of Selectors:

  • Cleaner components
  • Reusable queries
  • Better maintainability
  • Derived/computed state
  • Improved performance through memoization

Selectors make store access predictable, reusable, and easier to maintain in large applications.


Example Flow of Redux / NgRx

Understanding Redux/NgRx becomes easier by following a simple flow.

Example scenario:
A user opens a page and clicks Load Users.


Step 1 — Component Dispatches Action

The component does not directly call the API.

Instead, it dispatches an action.

this.store.dispatch(loadUsers());
Enter fullscreen mode Exit fullscreen mode

Action:

export const loadUsers = createAction('[Users] Load Users');
Enter fullscreen mode Exit fullscreen mode

Purpose:

  • Describe what happened
  • Start the state update process

Step 2 — Effect Handles API Call

The effect listens for the dispatched action.

loadUsers$ = createEffect(() =>
  this.actions$.pipe(
    ofType(loadUsers),
    switchMap(() =>
      this.userService.getUsers().pipe(
        map(users => loadUsersSuccess({ users }))
      )
    )
  )
);
Enter fullscreen mode Exit fullscreen mode

Purpose:

  • Perform async operations
  • Call APIs
  • Keep reducers pure

Step 3 — Success Action Is Dispatched

After data is received from the API, another action is dispatched.

export const loadUsersSuccess = createAction(
  '[Users] Load Users Success',
  props<{ users: User[] }>()
);
Enter fullscreen mode Exit fullscreen mode

Purpose:

  • Notify application that data was loaded successfully

Step 4 — Reducer Updates Store

Reducer receives:

  • Current state
  • Action

Then returns updated state.

on(loadUsersSuccess, (state, { users }) => ({
  ...state,
  users
}))
Enter fullscreen mode Exit fullscreen mode

Purpose:

  • Update application state
  • Keep state immutable and predictable

Step 5 — Selector Reads Data

Selectors provide clean access to store data.

export const selectUsers = createSelector(
  selectUserState,
  state => state.users
);
Enter fullscreen mode Exit fullscreen mode

Purpose:

  • Read state
  • Reuse state queries
  • Keep components clean

Step 6 — Component Receives Updated Data

Component subscribes to selector data.

users$ = this.store.select(selectUsers);
Enter fullscreen mode Exit fullscreen mode

Purpose:

  • Automatically receive updated state
  • Keep UI reactive

Complete Redux / NgRx Flow

Component
   ↓
Dispatch Action
   ↓
Effect Handles API Call
   ↓
Success Action
   ↓
Reducer Updates Store
   ↓
Selector Reads State
   ↓
Component Updates UI
Enter fullscreen mode Exit fullscreen mode

This predictable flow is one of the biggest advantages of Redux/NgRx in large Angular applications.


When NOT to Use Redux / NgRx

Redux/NgRx is not always the right solution.

In many projects, it can introduce unnecessary complexity and slow development.


1. Small Projects or Prototypes

Avoid Redux/NgRx when:

  • Applications are small
  • Requirements change frequently
  • Features are experimental

Reasons:

  • Too much boilerplate
  • Slower development
  • Architecture overhead is unnecessary

2. Shared UI or Component Libraries

Avoid embedding Redux inside reusable component libraries.

Reasons:

  • Forces every consuming project to use Redux
  • Different projects may not require global state management
  • Reduces flexibility

3. Teams Without Redux Experience

Avoid Redux/NgRx in important projects if the team lacks experience.

Possible issues:

  • Difficult debugging
  • Poor architecture decisions
  • Hard-to-maintain code
  • Incorrect implementation patterns

State management libraries require strong architectural understanding.


4. Applications Already Using Apollo Client

Apollo Client already provides state management for GraphQL applications.

Using Redux together with Apollo may create:

  • Multiple sources of truth
  • Synchronization issues
  • Extra complexity

In many GraphQL applications, Apollo alone is enough.


Common Drawbacks of Redux / NgRx

Even small features may require:

  • Actions
  • Reducers
  • Effects
  • Selectors
  • Store updates

This increases:

  • Boilerplate
  • Development time
  • Learning curve

For simple applications, this overhead may not be worth it.


When Redux / NgRx Makes Sense

Redux/NgRx becomes valuable in large and state-heavy applications.


1. Large Applications

Good fit for applications with:

  • Hundreds of components
  • Deep component trees
  • Complex shared state

Benefits:

  • Centralized architecture
  • Predictable updates
  • Easier debugging
  • Better maintainability

2. Undo / Redo Functionality

Redux works well for applications requiring:

  • Undo functionality
  • Redo functionality
  • Reverting changes
  • Optimistic UI updates

Reason:

  • State history can be tracked and restored easily

3. State Persistence

Redux is useful when applications need to:

  • Save application state
  • Restore sessions
  • Synchronize state across clients
  • Store state in local storage

Redux naturally supports state serialization.


4. Large Legacy System Migrations

Redux can help when:

  • Migrating large systems
  • Scaling existing applications
  • Replacing fragile custom state patterns

Using a structured architecture early helps reduce long-term complexity.


Signs That Redux May Be Needed

Redux/NgRx becomes worth considering when:

  • Multiple services start coordinating state manually
  • State synchronization becomes difficult
  • Custom state patterns begin appearing
  • Debugging shared data becomes painful

These are common indicators that application complexity is increasing.


Why Established Libraries Are Better

Using mature libraries like Redux/NgRx provides:

  • Community support
  • Better documentation
  • Standardized architecture
  • Easier onboarding
  • Long-term maintainability

Custom in-house state solutions often become:

  • Poorly documented
  • Difficult to scale
  • Hard for new developers to understand

Established libraries reduce long-term architectural risk.


Final Thoughts

Redux/NgRx should be treated as an architectural decision rather than a default choice.

Avoid Redux/NgRx When

  • Applications are small
  • Rapid development is important
  • Requirements change frequently
  • Apollo Client already manages state

Consider Redux/NgRx When

  • Applications are large and complex
  • Shared state becomes difficult to manage
  • Predictability is important
  • Undo/restore features are required
  • Long-term scalability matters

The additional complexity of Redux/NgRx is justified only when application scale and state management needs become significant.

Edit: POV based on signal

Top comments (3)

Collapse
 
gimi5555 profile image
Gilder Miller

Interesting. I wonder where Signals fit into this. With the shift toward signal-based reactivity in Angular, the need for a rigid Redux-style store for simple shared state seems to be diminishing. It feels like we're moving toward a hybrid model where we use a global store only for the 'source of truth' data and Signals for everything else. Do you see Signals eventually making the Redux pattern obsolete for most use cases?

Collapse
 
newavtar profile image
Bhargav Patel

Good observation :)

Earlier in Angular, even simple shared state often ended up going through the full NgRx flow: actions, reducers, selectors, and then components subscribing to observables. That made sense for complex applications, but it also meant that a lot of simple state management was being forced into a very structured Redux-style pipeline just because that was the standard reactive toolset.

The old flow looked like this:
Component dispatches an action → Action is processed by reducer or effect → Store is updated → Selectors derive the required state → Component subscribes and updates the UI.

With Signal-based Angular and newer NgRx APIs like signalStore and selectSignal, the model has become much more direct. For simple or feature-level state, you no longer need to think in terms of dispatching actions and writing reducers. Instead, state can be stored and updated directly using signals, and components react to those changes automatically. In that case, the flow becomes much simpler: the component calls a store method, the signal updates, and the UI reacts instantly. The heavy action–reducer–selector chain disappears for these cases.

The new flow looks like this:
Component calls a store method → Signal state is updated → Computed signals recalculate automatically → UI updates instantly.

However, this does not remove the Redux pattern itself. It only reduces how often it needs to be used. When state becomes more complex, especially in large applications where multiple features interact, side effects need coordination, or there is a need for clear traceability of state changes, the traditional NgRx (Redux-style) approach is still important. Effects, structured event flows, and predictable state transitions remain essential in those scenarios, and Signals alone do not provide those architectural guarantees.

So what is actually happening is not a replacement, but a shift in responsibility.

Signals handle the reactive layer of state in a simpler and more natural way, while NgRx is evolving to integrate Signals internally so both approaches work together.

This makes NgRx lighter and less boilerplate-heavy for simple cases, while still keeping its full architectural strength for complex systems.

In practice, this results in a hybrid model. Signals are used for most local and feature-level state because they are simpler and more direct. NgRx-style architecture is reserved for truly complex global state where structure, debugging, and orchestration actually matter.

So Signals are not replacing Redux patterns. They are simply removing the unnecessary use of Redux-style complexity in places where it was never really required.

Collapse
 
gimi5555 profile image
Gilder Miller

Thank you, Bhargav.
It helped me a lot. Hope to see your next article!