zgba Network

NgRx in Angular: A Practical Guide to State Management, Architecture, and Real-World Patterns

NgRx is more than a store. It is an architecture for managing complex application state in Angular using predictable state transitions, reactive programming, and unidirectional data flow. Modern Angular applications can start simple, but as the application grows, state management becomes increasingly difficult. You may eventually have: Components sharing the same data Multiple API requests updating the same state Complex loading and error states Data that must survive navigation Business logic spread across components Race conditions between asynchronous operations Difficult-to-debug state changes This is where NgRx becomes useful. In this article, we will build the concepts from the ground up and gradually move toward real-world patterns. Table of Contents What Is State Management? Why Do We Need NgRx? What Is NgRx? NgRx Architecture The Unidirectional Data Flow Store Actions Reducers Selectors Effects Dispatching Actions Reading State Complete CRUD Example Async Operations Loading and Error States Entity State NgRx Entity Feature-Based State ComponentStore vs Store Facades Immutability Common NgRx Patterns Common Mistakes Performance Testing When Should You Use NgRx? Final Architecture 1. What Is State Management? Before understanding NgRx, we need to understand state. State is simply the data that describes the current condition of your application. For example: interface User { id: number; name: string; email: string; } Your application might have: interface AppState { user: User | null; isLoading: boolean; error: string | null; } At a particular moment: { user: { id: 1, name: β€˜John’, email: β€˜john@example.com’ }, isLoading: false, error: null } This is application state. 2. Local State vs Global State Not every piece of state needs NgRx. Consider a button: isMenuOpen = false; This is usually local component state. You probably don’t need a global store for it. But consider: Authentication Products Shopping Cart User Profile Notifications Permissions Orders These may be shared by many parts of the application. For example: Navbar ↓ User Authentication State ↑ Profile Page ↑ Checkout ↑ Orders When many components depend on the same state, centralized state management becomes useful. 3. What Is NgRx? NgRx is a collection of Angular libraries inspired by Redux and built around reactive programming with RxJS. The core idea is: Component ↓ Action ↓ Reducer / Effect ↓ Store ↓ Selector ↓ Component Instead of allowing components to modify shared state directly, state changes happen through actions. This creates predictable state transitions. 4. NgRx Architecture The main pieces are: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Component β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ dispatch β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Action β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Reducer β”‚ β”‚ Effect β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β”‚ HTTP / API β”‚ β”‚ β”‚ β–Ό β”‚ Action β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Store β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ select β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Component β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ There are five concepts you should understand extremely well: Actions Reducers Store Selectors Effects 5. The Unidirectional Data Flow NgRx follows a unidirectional data flow. That means data moves in a predictable direction. UI ↓ Action ↓ State Transition ↓ Store ↓ Selector ↓ UI For example: this.store.dispatch( increment() ); The component does not directly change: count = count + 1; Instead, it describes what happened: increment() The reducer determines how the state should change. 6. The Store The Store is the centralized state container. Imagine: interface AppState { counter: number; user: User | null; products: Product[]; } Conceptually: Store β”œβ”€β”€ counter β”œβ”€β”€ user └── products The Store itself is observable. You read state reactively: counter = this.store.select(selectCounter); Then:

{{ counter | async }}

7. Actions An Action describes something that happened. Example: import { createAction } from β€˜@ngrx/store’; export const increment = createAction( β€˜[Counter] Increment’ ); Another: export const loadProducts = createAction( β€˜[Products Page] Load Products’ ); The string: [Products Page] Load Products is the action type. A good action describes an event. Prefer: [Login Page] Login Submitted over: [Auth] Set User Why? Because the first describes what happened, while the second describes an implementation detail. 8. Actions With Payloads Actions can carry data. export const addToCart = createAction( β€˜[Cart] Add Product’, props<{ productId: number }>() ); Dispatch: this.store.dispatch( addToCart({ productId: 10 }) ); Another example: export const login = createAction( β€˜[Login Page] Login Submitted’, props<{ email: string; password: string; }>() ); Then: this.store.dispatch( login({ email: β€˜john@example.com’, password: β€˜123456’ }) ); 9. Reducers Reducers determine how state changes in response to actions. Example: import { createReducer, on } from β€˜@ngrx/store’; export const initialState = 0; export const counterReducer = createReducer( initialState, on(increment, state => state + 1) ); The important concept is: Action + Current State ↓ New State For example: State = 5 Action = increment Reducer 5 + 1 New State = 6 10. Reducers Must Be Pure A reducer should be: Predictable Synchronous Pure Free from side effects Don’t do this inside a reducer: on(loadProducts, state => { http.get(β€˜/products’); return state; }); Reducers should not perform HTTP requests. They should only calculate the next state. 11. Immutable State NgRx relies heavily on immutability. Bad: state.user.name = β€˜John’; return state; Good: return { …state, user: { …state.user, name: β€˜John’ } }; The idea is to create a new state reference instead of mutating the existing one. 12. Selectors Selectors are used to read data from the Store. Suppose: interface CounterState { count: number; } Create a feature selector: export const selectCounterState = createFeatureSelector(β€˜counter’); Then: export const selectCount = createSelector( selectCounterState, state => state.count ); Component: count = this.store.select(selectCount); Template:

{{ count | async }}

13. Why Selectors Are Important You might ask: Why not simply get the entire state? Because selectors provide: Encapsulation Components don’t need to know how state is structured. Memoization Selectors can avoid unnecessary recalculations. Reusability Multiple components can use the same selector. Performance Components subscribe only to the data they need. 14. Derived State One of the most powerful selector features is derived state. Suppose: interface CartState { items: CartItem[]; } You can calculate: export const selectCartTotal = createSelector( selectCartItems, items => items.reduce( (total, item) => total + item.price * item.quantity, 0 ) ); The total does not need to be stored separately. Instead: Cart Items ↓ Selector ↓ Cart Total This avoids duplicated state. 15. Effects Effects handle side effects. Examples: HTTP Requests Local Storage Analytics Navigation WebSocket interactions External APIs Suppose: User clicks Load Products ↓ loadProducts action ↓ Effect ↓ HTTP request ↓ API response ↓ loadProductsSuccess ↓ Reducer ↓ Store This is the main purpose of NgRx Effects. 16. Example Effect @Injectable() export class ProductsEffects { loadProducts = createEffect(() => this.actions.pipe( ofType(loadProducts), switchMap(() => this.productsService.getProducts().pipe( map(products => loadProductsSuccess({ products }) ), catchError(error => of(loadProductsFailure({ error })) ) ) ) ) ); constructor( private actions: Actions, private productsService: ProductsService ) {} } The effect listens for: loadProducts Then performs: productsService.getProducts() And dispatches: loadProductsSuccess or: loadProductsFailure 17. The Complete Request Flow Let’s visualize it: User β”‚ β–Ό Products Component β”‚ β”‚ dispatch() β–Ό loadProducts β”‚ β–Ό Products Effect β”‚ β”‚ HTTP β–Ό Backend API β”‚ β–Ό Response β”‚ β–Ό loadProductsSuccess β”‚ β–Ό Products Reducer β”‚ β–Ό Store β”‚ β–Ό selectProducts β”‚ β–Ό Products Component This is one of the most important NgRx flows to understand. 18. Loading and Error State A realistic application needs more than data. For example: interface ProductsState { products: Product[]; loading: boolean; error: string | null; } Initial state: const initialState: ProductsState = { products: [], loading: false, error: null }; When loading starts: on(loadProducts, state => ({ …state, loading: true, error: null })) Success: on(loadProductsSuccess, (state, { products }) => ({ …state, products, loading: false })) Failure: on(loadProductsFailure, (state, { error }) => ({ …state, loading: false, error })) 19. Complete Products Example Actions export const loadProducts = createAction( β€˜[Products Page] Load Products’ ); export const loadProductsSuccess = createAction( β€˜[Products API] Load Products Success’, props<{ products: Product[] }>() ); export const loadProductsFailure = createAction( β€˜[Products API] Load Products Failure’, props<{ error: string }>() ); Reducer export interface ProductsState { products: Product[]; loading: boolean; error: string | null; } export const initialState: ProductsState = { products: [], loading: false, error: null }; export const productsReducer = createReducer( initialState, on(loadProducts, state => ({ …state, loading: true, error: null })), on(loadProductsSuccess, (state, { products }) => ({ …state, products, loading: false })), on(loadProductsFailure, (state, { error }) => ({ …state, loading: false, error })) ); Selectors export const selectProductsState = createFeatureSelector(β€˜products’); export const selectProducts = createSelector( selectProductsState, state => state.products ); export const selectLoading = createSelector( selectProductsState, state => state.loading ); export const selectError = createSelector( selectProductsState, state => state.error ); Component products = this.store.select(selectProducts); loading = this.store.select(selectLoading); error = this.store.select(selectError); loadProducts() { this.store.dispatch(loadProducts()); } Template <button (click)=β€œloadProducts()”> Load Products @if (loading | async) {

Loading…

} @if (error | async; as error) {

{{ error }}

} @for (product of products | async; track product.id) {

{{ product.name }}

{{ product.price }}

} 20. NgRx Entity Imagine you have: Product[] with thousands of products. You frequently need: Find product by ID Add product Remove product Update product Select all products Managing arrays manually becomes repetitive. NgRx Entity provides utilities for normalized collections. Conceptually: Entity State ids: [1, 2, 3] entities: { 1: Product, 2: Product, 3: Product } Instead of: products.find(p => p.id === id); the entity adapter can provide selectors and operations for you. 21. Entity Adapter Example: const adapter = createEntityAdapter(); Initial state: const initialState = adapter.getInitialState({ loading: false, error: null }); Add products: adapter.addMany(products, state); Update: adapter.updateOne( { id: product.id, changes: product }, state ); Remove: adapter.removeOne(productId, state); This becomes extremely useful for large collections. 22. Feature-Based Architecture A large Angular application should not have one giant Store file. Instead: src/app/ β”‚ β”œβ”€β”€ core/ β”‚ β”œβ”€β”€ shared/ β”‚ └── features/ β”‚ β”œβ”€β”€ auth/ β”‚ β”œβ”€β”€ store/ β”‚ β”‚ β”œβ”€β”€ auth.actions.ts β”‚ β”‚ β”œβ”€β”€ auth.reducer.ts β”‚ β”‚ β”œβ”€β”€ auth.effects.ts β”‚ β”‚ └── auth.selectors.ts β”‚ β”‚ β”‚ └── pages/ β”‚ β”œβ”€β”€ products/ β”‚ β”œβ”€β”€ store/ β”‚ β”‚ β”œβ”€β”€ products.actions.ts β”‚ β”‚ β”œβ”€β”€ products.reducer.ts β”‚ β”‚ β”œβ”€β”€ products.effects.ts β”‚ β”‚ └── products.selectors.ts β”‚ └── cart/ β”œβ”€β”€ store/ β”‚ β”œβ”€β”€ cart.actions.ts β”‚ β”œβ”€β”€ cart.reducer.ts β”‚ β”œβ”€β”€ cart.effects.ts β”‚ └── cart.selectors.ts This keeps features isolated. 23. NgRx Facade Pattern Some teams don’t want components to know about NgRx directly. Instead, introduce a facade. @Injectable({ providedIn: β€˜root’ }) export class ProductsFacade { products = this.store.select(selectProducts); loading = this.store.select(selectLoading); constructor( private store: Store ) {} loadProducts() { this.store.dispatch(loadProducts()); } } Component: constructor( private productsFacade: ProductsFacade ) {} products = this.productsFacade.products; loadProducts() { this.productsFacade.loadProducts(); } Now the component doesn’t know about: Actions Selectors Store It only knows: ProductsFacade This can be useful in large applications. 24. RxJS and NgRx NgRx relies heavily on RxJS. For example: this.store.select(selectProducts) returns an Observable. Effects also use RxJS operators: ofType() switchMap() mergeMap() concatMap() exhaustMap() map() catchError() Understanding RxJS is therefore extremely valuable when working with NgRx. 25. Choosing the Correct Flattening Operator This is a common interview topic. switchMap Cancels the previous request. Useful for: Search Autocomplete Example: actions.pipe( ofType(searchProducts), switchMap(…) ); concatMap Queues requests. Request 1 ↓ Request 2 ↓ Request 3 Useful when order matters. mergeMap Runs requests concurrently. Request 1 ────────┐ Request 2 ───────── Request 3 β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Useful when requests are independent. exhaustMap Ignores new requests while one is running. Useful for: Login Submit button Payment For example: actions.pipe( ofType(login), exhaustMap(…) ); If the user clicks Login multiple times quickly, subsequent clicks are ignored until the first request completes. 26. NgRx vs Services You don’t need NgRx for every Angular application. A simple service may be enough: @Injectable({ providedIn: β€˜root’ }) export class CartService { private cartSubject = new BehaviorSubject<CartItem[]>([]); cart = this.cartSubject.asObservable(); add(item: CartItem) { const current = this.cartSubject.value; this.cartSubject.next([ …current, item ]); } } This can work perfectly well for a small application. NgRx becomes more valuable when state transitions and interactions become complex. 27. NgRx vs ComponentStore NgRx Store is designed for centralized application state. ComponentStore is better suited for localized state. Example: Global Application State ↓ Store Feature-local state ↓ ComponentStore You don’t necessarily need to choose one exclusively. A large application can use both. 28. What Should NOT Go Into NgRx? A common mistake is putting everything into the Store. Don’t automatically store: isDropdownOpen hoveredItem temporary input values modal visibility simple UI flags unless there is a real reason. Ask: Does this state need to be shared, persisted, coordinated, or observed across multiple parts of the application? If not, local state may be better. 29. Avoid Duplicated State Suppose you have: products: Product[]; selectedProduct: Product; You may be duplicating information. Instead, consider: products: Product[]; selectedProductId: number | null; Then: export const selectSelectedProduct = createSelector( selectProducts, selectSelectedProductId, (products, id) => products.find(product => product.id === id) ?? null ); Now there is one source of truth. 30. Keep Business Logic Out of Components Bad: loadProducts() { this.loading = true; this.http.get(β€˜/products’) .subscribe(products => { this.products = products; this.loading = false; }); } This mixes: UI HTTP State Loading Error handling With NgRx: loadProducts() { this.store.dispatch(loadProducts()); } The component becomes much simpler. 31. Smart vs Presentational Components NgRx works well with the separation between container and presentational components. Container Responsible for state interaction. products = this.facade.products; Presentational Receives data: @Input() products: Product[] = []; and emits events: @Output() productSelected = new EventEmitter(); Architecture: Container β”‚ β”œβ”€β”€ Store β”‚ └── Presentational Component β”‚ └── UI 32. Router Store NgRx can also integrate router state. This can allow application state to react to route changes. Conceptually: URL ↓ Router ↓ Router Store ↓ Selectors ↓ Application This can be useful when route parameters influence application state. 33. DevTools One of the major advantages of NgRx is debugging. With Redux DevTools, you can inspect: Action ↓ Previous State ↓ Next State For example: [Cart] Add Product Previous: items: [] Next: items: [Product 1] This makes complex state transitions much easier to understand. 34. Time-Travel Debugging Because state changes are represented as actions, you can conceptually replay state transitions. Action 1 Action 2 Action 3 Action 4 Instead of debugging: β€œSomething changed somewhere.” you can investigate: β€œWhich action caused the state transition?” This is one of the architectural strengths of Redux-style state management. 35. Testing Reducers Reducers are easy to test because they are pure functions. Example: it(β€˜should increment counter’, () => { const state = counterReducer( 5, increment() ); expect(state).toBe(6); }); No HTTP request. No browser. No asynchronous operation. Just: Input ↓ Reducer ↓ Output 36. Testing Selectors Selectors can also be tested independently. For example: const state = { products: [ { id: 1, price: 100 }, { id: 2, price: 200 } ] }; Then verify: expect(selectProducts.projector( state.products )).toEqual(state.products); Selectors are another reason to keep business calculations outside components. 37. Testing Effects Effects require testing asynchronous behavior. Conceptually: Input Action ↓ Effect ↓ Mock API ↓ Output Action You can verify: loadProducts ↓ API called ↓ loadProductsSuccess or: loadProducts ↓ API error ↓ loadProductsFailure 38. Common NgRx Mistakes Mistake 1 β€” Putting everything in Store Not every variable belongs in global state. Mistake 2 β€” Mutating state Bad: state.products.push(product); return state; Good: return { …state, products: [ …state.products, product ] }; Mistake 3 β€” HTTP inside reducers Never. Use Effects or another appropriate side-effect mechanism. Mistake 4 β€” Huge selectors Selectors should remain focused and composable. Mistake 5 β€” Components containing business logic If your component becomes: 500+ lines and heavily interacts with Store, consider extracting logic. Mistake 6 β€” Overusing Effects Effects are for side effects. Don’t create an effect just to transform simple state synchronously when a reducer or selector is enough. 39. NgRx and Angular Signals Modern Angular applications increasingly use Signals. You can bridge Store Observables into Signals. For example: products = this.store.selectSignal(selectProducts); Then in the template: @for (product of products(); track product.id) {

{{ product.name }}

} This gives you a more signal-oriented component API while still using NgRx for centralized state management. 40. A Real-World E-Commerce Architecture Imagine an e-commerce application. We could structure the state as: Application Store β”‚ β”œβ”€β”€ Auth β”‚ β”œβ”€β”€ user β”‚ β”œβ”€β”€ token β”‚ └── loading β”‚ β”œβ”€β”€ Products β”‚ β”œβ”€β”€ entities β”‚ β”œβ”€β”€ filters β”‚ β”œβ”€β”€ loading β”‚ └── error β”‚ β”œβ”€β”€ Cart β”‚ β”œβ”€β”€ items β”‚ └── total β”‚ β”œβ”€β”€ Orders β”‚ β”œβ”€β”€ entities β”‚ β”œβ”€β”€ loading β”‚ └── error β”‚ └── UI β”œβ”€β”€ notifications └── theme The flow could be: User β”‚ β–Ό Angular Component β”‚ β–Ό Action β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό Reducer Effect β”‚ β”‚ β–Ό β–Ό Store API β”‚ β”‚ β”‚ β–Ό β”‚ Success Action β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό Selector β”‚ β–Ό Component This architecture scales significantly better than putting all application logic into components. 41. When Should You Use NgRx? NgRx is a good choice when your application has: Complex shared state Multiple features depending on the same data Many asynchronous workflows Complex state transitions Large teams Strong debugging requirements Event-driven architecture Need for predictable state changes Significant business logic Examples: E-commerce Banking dashboards Admin platforms CRM systems Large SaaS applications Enterprise applications Real-time applications 42. When Should You Avoid NgRx? For a small application: 5 components 2 API calls simple forms little shared state NgRx may introduce unnecessary complexity. A simple: Component ↓ Service ↓ HTTP architecture might be better. The goal is not: β€œUse NgRx because NgRx is powerful.” The goal is: β€œUse the simplest architecture that can reliably manage the application’s complexity.” 43. The Most Important Mental Model If you remember only one thing, remember this: Action = What happened? Reducer = How does state change? Effect = What side effect should happen? Selector = What state does the UI need? Store = Where is application state centralized? For example: User clicks β€œAdd to Cart” Action: [Product Page] Add To Cart ↓ Reducer: Add product to cart state ↓ Effect: Maybe synchronize cart with backend ↓ Store: Updated cart ↓ Selector: Calculate cart count ↓ UI: Cart: 3 items That is NgRx. 44. Final Architecture A mature Angular + NgRx application can look like: Angular UI β”‚ β–Ό Component / Facade β”‚ β–Ό Action β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β–Ό β–Ό Reducer Effect β”‚ β”‚ β”‚ HTTP/API β”‚ β”‚ β”‚ β–Ό β”‚ Success/Failure β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό Store β”‚ β–Ό Selectors β”‚ β–Ό Component / UI This architecture gives you: Predictable state transitions Centralized state Reactive data flow Testable business logic Clear separation of concerns Better debugging Scalable feature architecture Conclusion NgRx is not simply a library for storing variables. It is a state-management architecture built around a very clear philosophy: Events ↓ Actions ↓ State Transitions ↓ Store ↓ Selectors ↓ UI Effects extend this architecture by handling asynchronous operations and other side effects. The real power of NgRx appears when an application becomes complex enough that managing state manually starts becoming difficult. The key is not to memorize APIs. Understand the architecture first: Actions Reducers Selectors Effects Store Once those concepts are clear, the NgRx APIs become much easier to learn. And the most important practical rule is: Don’t introduce NgRx because your application is Angular. Introduce it because your application’s state management has become complex enough to justify it.

View original article