You don't need NgRx, well almost.
While NgRx is a powerful state management library, I have seen countless projects misuse or overuse it, leading to unnecessary complexity, wasted time, and frustration.
The Overuse of NgRx
NgRx is often applied as a default state management solution, even when it’s not needed. Many teams blindly follow the pattern: action → effect → API call → action → reducer → selector → component, even for data that is simply fetched and displayed. This rigid application can be a huge waste of time and money.
Observations from the Angular Community
- Many teams enforce NgRx usage, even when simple services and RxJS would suffice.
- The boilerplate and verbosity make maintaining code harder, especially for small features.
- Selectors and actions bloat files unnecessarily, leading to frustration among developers.
- Enterprise projects often mandate NgRx, not because it’s needed, but because it’s “best practice.”
Case Studies & Industry Trends
- NgRx in Small and Medium Projects: Developers increasingly advise against NgRx unless absolutely necessary.
- State Management Trends: Simpler alternatives, such as Component Store and plain services with RxJS, are gaining popularity.
- Angular Signals Introduction (2024): With Signals, Angular is pushing for simpler state management, reducing the need for complex global stores.
Anecdotal Data from the Community
While no broad surveys exist, common developer feedback reveals:
- A large percentage of teams regret overusing NgRx and later refactor their code.
- Some developers abandon NgRx altogether after experiencing its unnecessary complexity.
- Overusing selectors and actions leads to unnecessary maintenance burdens.
Common NgRx Anti-Patterns
1. Fetch-and-Throw Anti-Pattern
NgRx is often misused for simple API requests where the data is not needed beyond the component lifecycle. Instead of using a simple service, teams follow a rigid pattern:
🚨 Bad Example: Overcomplicated Data Fetching
// Action
export const loadUsers = createAction("[Users] Load Users");
export const loadUsersSuccess = createAction(
"[Users] Load Users Success",
props<{ users: User[] }>()
);
// Effect
@Injectable()
export class UsersEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.http
.get<User[]>("/api/users")
.pipe(map((users) => loadUsersSuccess({ users })))
)
)
);
constructor(private actions$: Actions, private http: HttpClient) {}
}
// Reducer
export const usersReducer = createReducer(
initialState,
on(loadUsersSuccess, (state, { users }) => ({ ...state, users }))
);
// Selector
export const selectUsers = createSelector(
(state: AppState) => state.users,
(users) => users
);
This is overkill for fetching data that will be discarded after use.
✅ Better Approach: Simple Service with RxJS
@Injectable({ providedIn: "root" })
export class UsersService {
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>("/api/users");
}
}
// Component
this.usersService.getUsers().subscribe((users) => (this.users = users));
This is cleaner, faster, and easier to maintain.
2. Storing UI State in NgRx
NgRx should not be used for local UI state, such as modals, toggles, or form states. UI state does not need global management and should be handled within the component.
🚨 Bad Example: Storing a Modal’s Open State in NgRx
export const openModal = createAction("[UI] Open Modal");
export const closeModal = createAction("[UI] Close Modal");
export const uiReducer = createReducer(
{ modalOpen: false },
on(openModal, (state) => ({ ...state, modalOpen: true })),
on(closeModal, (state) => ({ ...state, modalOpen: false }))
);
this.store.dispatch(openModal());
this.store
.select(selectModalOpen)
.subscribe((isOpen) => (this.modalOpen = isOpen));
This is unnecessary. A simple component state would be better:
✅ Better Approach: Local Component State
export class MyComponent {
modalOpen = false;
toggleModal() {
this.modalOpen = !this.modalOpen;
}
}
3. Over-Normalization
While normalizing state is useful for large datasets, over-normalization can make retrieval overly complex.
🚨 Bad Example: Over-Normalized State
interface AppState {
users: { ids: string[]; entities: { [id: string]: User } };
}
To get user data, you have to map ids
to entities
, adding unnecessary complexity.
✅ Better Approach: Store Data in a Simple Array
interface AppState {
users: User[];
}
This is simpler and often sufficient unless managing deeply nested data.
So, When Do You Actually Need NgRx?
NgRx is great when:
- State is truly global and shared across multiple components or modules.
- Complex state interactions require structured actions (e.g., optimistic updates, undo/redo functionality).
- Debugging and time-travel debugging are priorities.
However, for most applications, simpler alternatives like RxJS services or Angular Signals are better choices.
Conclusion
NgRx is not evil, but its overuse leads to bloated, unreadable code. Many developers fall into the trap of using it by default instead of assessing whether a simpler approach would work better. Before reaching for NgRx, ask yourself: Do I really need global state management? Or am I just following a pattern blindly?