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.
Top comments (1)
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?