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
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
}
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());
Action:
export const loadUsers = createAction('[Users] Load Users');
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 }))
)
)
)
);
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[] }>()
);
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
}))
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
);
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);
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
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)
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?
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
signalStoreandselectSignal, 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.
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.
Thank you, Bhargav.
It helped me a lot. Hope to see your next article!