Key Takeaways
- Don't add Redux or NgRx Store merely because components need to share data. Start with the smallest owner that fits the state's lifetime and update rules.
- Angular dependency injection controls which consumers share a service instance. It doesn't enforce predictable updates, immutability, or debugging conventions.
- Redux is framework-agnostic. NgRx Store brings a Redux-inspired architecture to Angular; SignalStore and ComponentStore offer different state-management contracts.
- A formal store earns its complexity when coordinated updates, cross-feature consistency, and reproducible state transitions become difficult to manage in services.
Angular applications don't need Redux by default. Component state and well-designed services can handle straightforward state without an action-and-reducer pipeline. The title “Why you should NEVER use Redux with Angular” is too absolute: the mistake is adopting a store without a problem that justifies it, not using Redux with Angular.
The original argument also needs correcting: Redux doesn't solve exclusively React-specific problems, and Angular's dependency injection doesn't make state-management libraries redundant. Choose based on ownership, update complexity, and debugging needs—not framework loyalty.
What Redux Actually Solves
Redux is a framework-agnostic state container. Its value is an explicit update model: an action describes an event, a reducer calculates the next state from the previous state and that action, and consumers read from the store. Redux Toolkit is the Redux team's recommended approach for writing Redux logic.
That structure can make shared state easier to trace and test. It also introduces concepts and indirection that a simple component or service might not need. Our Flux vs MVC explanation provides background on why explicit, one-way update flows can be useful.
Prop Drilling Isn't the Whole Problem
Prop drilling means passing data through intermediate components that don't use it themselves. React Context and component composition can address that problem without Redux. Angular inputs also pass data between components, while injected services offer another sharing mechanism; none of these choices alone determines how state should change.
The Case Against Adding a Store by Default
Sharing a service is useful, but sharing an object isn't the same as defining a state architecture. Dependency injection is one of Angular's practical framework advantages; your team still has to decide who owns each value, who may update it, and how to investigate incorrect changes.
Start with State Ownership and Lifetime
- Component-local state: keep a dialog's visibility or an editor's temporary selection in the component when nothing else needs to coordinate with it.
- Feature-shared state: consider a service or SignalStore for a search page's filters, results, and loading state.
- Application-wide state: share an account or tenant selection where necessary, but don't assume broad readership requires a reducer-based store.
Next, count the independent writers and the rules connecting their updates. A value read in many places but changed through one simple method is a different problem from several features coordinating optimistic edits, failures, and background refreshes.
A Working Angular Service with Reactive State
This example shares a user name between two consumers. In an existing standalone Angular browser workspace, replace src/main.ts with the following and keep <app-root></app-root> in src/index.html.
import {
ChangeDetectionStrategy,
Component,
Injectable,
inject,
signal,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
@Injectable({ providedIn: 'root' })
class UserState {
private readonly nameState = signal('Sam');
readonly userName = this.nameState.asReadonly();
rename(userName: string): void {
this.nameState.set(userName);
}
}
@Component({
selector: 'app-user-badge',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>Header: {{ state.userName() }}</p>`,
})
class UserBadge {
readonly state = inject(UserState);
}
@Component({
selector: 'app-profile-editor',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Profile: {{ state.userName() }}</p>
<button type="button" (click)="state.rename('Taylor')">
Rename to Taylor
</button>
`,
})
class ProfileEditor {
readonly state = inject(UserState);
}
@Component({
selector: 'app-root',
standalone: true,
imports: [UserBadge, ProfileEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-user-badge></app-user-badge>
<app-profile-editor></app-profile-editor>
`,
})
class App {}
bootstrapApplication(App).catch((error: unknown) => console.error(error));Both consumers resolve the root-provided service, with no overriding provider in this example. Clicking the button changes the private writable signal, and both templates read its updated value. There's no constructor assignment that copies the current string into an unrelated field.
Provider scope matters. Providing a service on a component creates an instance for that component instance and makes it available to descendants that resolve that provider. Providing it separately on both consumers would give them separate state rather than the shared state shown here.
A component can use signal for its own local state without introducing a service. Use computed for values derived from other signals instead of maintaining duplicate fields that must be synchronized. These are reactivity primitives, not a prescribed application-wide update policy.
RxJS and Reactive Programming Aren't Store Semantics
An Observable represents a stream of values. A BehaviorSubject retains a current value and emits it to new subscribers. Those capabilities don't require actions, reducers, immutable transitions, or an action history.
For a search feature, think of the flow as input, debouncing, request selection, state update, and display. RxJS supplies stream-composition tools; your service or store still needs rules for stale responses, loading, errors, and caching. If the stream model is unfamiliar, start with our Observable vs Promise comparison.
Service-based state:
UI event -> named method -> private state -> readonly signal/Observable -> views
NgRx Store:
UI event -> dispatch(action) -> reducer -> Store -> selector -> viewsAngular's toSignal bridges an Observable into signal-based consumers. It subscribes immediately and, by default, unsubscribes when its creating component or service is destroyed; reuse the returned signal rather than repeatedly converting the same stream. For manual subscriptions, takeUntilDestroyed can tie completion to the owner's destruction. These tools address reactive consumption and cleanup, not state ownership.
Redux vs NgRx for Angular: Compare the Actual Options
NgRx isn't another name for the Redux package. NgRx Store is an Angular-focused, RxJS-powered implementation inspired by Redux. SignalStore and ComponentStore are separate options within the NgRx ecosystem.
| Option | Good starting point | What you take responsibility for |
|---|---|---|
| Component state or signals | Isolated UI interactions | Keeping state local and deriving values clearly |
| Injected service with signals or observables | Shared state with a small, understandable update API | Ownership, update discipline, lifecycle, testing, and tracing |
| NgRx SignalStore | Structured signal-oriented feature or application state | Choosing store boundaries and composing state and behavior |
| NgRx ComponentStore | RxJS-oriented feature or widget state | Choosing scope and organizing updaters, selectors, and effects |
| NgRx Store | Coordinated shared state that benefits from explicit actions and reducers | Action design, feature boundaries, effects, and selectors |
| Redux Toolkit directly | A deliberately framework-neutral domain store | Angular consumption and lifecycle integration as well as store design |
SignalStore can be provided locally or globally. It also supports event-driven state transitions through the signals events package, so “signals versus actions” isn't a clean dividing line. Evaluate the update model and tooling you need, not just the reactive primitive.
For new signal-first features, I'd evaluate SignalStore before introducing ComponentStore. Existing observable-heavy ComponentStore features don't need a rewrite merely to follow that preference.
When Direct Redux Integration Makes Sense
A hybrid React/Angular application can deliberately share a framework-neutral Redux store. That is an architectural choice, not an automatic benefit of running both frameworks: define who dispatches each event, how Angular observes changes, and who cleans up subscriptions. Don't maintain competing copies of the same domain state.
When NgRx Store Earns Its Complexity
NgRx's own guidance on when to use Store emphasizes shared state, multiple data sources, and situations where services are no longer sufficient. Consider it when several of these needs appear together:
- Several features must respond consistently to the same business event.
- Many independent sources update related data.
- You need to reproduce bugs from explicit state transitions.
- Action inspection and time-travel debugging would materially improve investigations.
- Your service layer has accumulated its own informal dispatcher, reducer conventions, and cross-service coordination.
For example, a checkout workflow might coordinate cart edits, promotions, inventory responses, and payment status. Explicit events can make those relationships clearer than a chain of service methods. You still need deliberate rules for cancellation, stale responses, and backend validation.
NgRx Store DevTools provides Store instrumentation for time-travel debugging. Keep reducers pure so recorded transitions remain reproducible; inspecting an earlier state isn't a substitute for implementing business-level undo.
Boilerplate Concerns with Redux and NgRx
The boilerplate objection remains useful, but don't compare a modern service against obsolete handwritten action classes. Redux Toolkit reduces Redux setup and reducer code, while NgRx's creator APIs provide concise actions and reducers. The remaining cost is the architecture: events, transitions, selectors, registration, and sometimes effects.
Here's the same user-name transition expressed with NgRx. This is an alternative state-and-configuration fragment, not an addition to the service example. It assumes a compatible @ngrx/store package is installed; merge the provider into your existing application configuration.
// Alternative fragment: user-ngrx.ts
import type { ApplicationConfig } from '@angular/core';
import {
createAction,
createReducer,
on,
props,
provideStore,
} from '@ngrx/store';
export interface UserState {
userName: string;
}
const initialState: UserState = { userName: 'Sam' };
export const userRenamed = createAction(
'[Profile] User renamed',
props<{ userName: string }>(),
);
export const userReducer = createReducer(
initialState,
on(userRenamed, (state, { userName }): UserState => ({
...state,
userName,
})),
);
export const selectUserName = (state: { user: UserState }): string =>
state.user.userName;
export const appConfig: ApplicationConfig = {
providers: [provideStore({ user: userReducer })],
};With an injected Store, a consumer reads through store.selectSignal(selectUserName) and updates through store.dispatch(userRenamed({ userName: 'Taylor' })). NgRx Store can therefore expose signals without replacing its action-and-reducer architecture.
For one string, that structure buys little. For a business event affecting several features, it gives you named transitions and a consistent place to test them. Compare the maintenance cost of those conventions against the conventions you'd otherwise build inside services.
Common Mistakes
- Confusing a snapshot with reactive state. Assigning the result of
state.userName()once stores the current string. Keep reading the signal in a reactive context instead. - Making everything global. A temporary form draft or dialog flag doesn't need application-wide ownership merely because a store exists.
- Exposing unrestricted writes. A service with public mutable objects and writers everywhere can become harder to reason about than a formal store.
- Treating a store as a performance fix. Choose the architecture for state correctness and maintainability; investigate rendering cost separately.
Reading a signal in an OnPush template lets Angular track that dependency and mark the component when it changes. That behavior doesn't depend on adopting Redux. Use the Angular DevTools profiler to measure expensive change-detection work before changing state architecture to address a rendering problem.
Production Checklist
- Document each state's owner, lifetime, allowed writers, and reset behavior.
- Test transitions, failure paths, and whether consumers receive the intended shared or isolated instance.
- Define loading, stale-response, cache-invalidation, and logout-reset policies explicitly.
- Decide what may appear in action logs or persisted state; exclude credentials and unnecessary personal data.
- Check the selected NgRx release's installation requirements against your workspace before installing or upgrading.
Conclusion
Never add Redux to Angular by reflex. Start with local state or a service when the update model is simple, and adopt a formal store when its structure solves a concrete coordination or debugging problem. Angular makes lightweight state sharing convenient; it doesn't make disciplined state management unnecessary.
FAQ
Do Angular applications need a state management library?
No. Component state and services with signals or observables can be sufficient when ownership and updates are straightforward. Consider SignalStore, ComponentStore, or NgRx Store when their conventions solve a specific maintenance problem.
Can I use Redux with Angular?
Yes. Redux is framework-agnostic, but direct integration requires an Angular consumption and lifecycle strategy. NgRx Store offers an Angular-focused, Redux-inspired alternative.
Is NgRx the same as Redux?
No. NgRx is an Angular ecosystem of libraries; NgRx Store follows a Redux-inspired action-and-reducer architecture. SignalStore and ComponentStore provide different state-management approaches within that ecosystem.
When should I use NgRx Store instead of an Angular service?
Consider NgRx Store when many writers and features need coordinated updates, reproducible transitions, or action-based debugging. A service is a better starting point when it exposes a small, clear update API and you can readily trace its changes.
Does a state management library improve Angular performance?
Not automatically. Rendering performance depends on what changes, which consumers are notified, and how much work they perform. Profile the application before choosing a store as a performance intervention.
Further Reading
- Redux getting started — framework independence, Redux Toolkit, and adoption trade-offs.
- Angular signals — writable signals, read-only exposure, computed state, and template tracking.
- Angular dependency-provider scopes — application-wide and component-level service ownership.
- Angular signals and RxJS interoperability — conversion, subscriptions, and cleanup.
- NgRx Store guide — actions, reducers, selectors, and standalone registration.
- NgRx SignalStore guide — composable signal-based state and provider scope.
- ComponentStore API reference — updaters, selectors, effects, and lifecycle methods.
- React Context and alternatives — why prop drilling doesn't require Redux.
