Complete NgRx Setup in Angular (Standalone)
With Effects, Selectors, DevTools & State Persistence (Meta-Reducers)
Complete NgRx Setup in Angular (Standalone)

With Effects, Selectors, DevTools & State Persistence (Meta-Reducers)
This is a step-by-step production-ready guide using:
- Angular (Standalone API)
- NgRx Store
- NgRx Effects
- NgRx DevTools
- Meta-Reducer for LocalStorage Hydration
- DummyJSON API (
https://dummyjson.com/products)
By the end, you will understand:
- NgRx lifecycle
- How to structure feature state
- How to avoid duplicate API calls after refresh
- How to persist state
What is NgRx?
NgRx is a reactive state management library for Angular based on the Redux pattern.
It follows unidirectional data flow:
Component
↓
Action
↓
Reducer
↓
Store Updated
↓
Selector
↓
Component UI Updated
Async work happens in Effects.
Working Example Repository: The complete source code for this tutorial is available on GitHub:
https://github.com/siva-geddada/ngrx
Step 1 — Install NgRx
ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/store-devtools@latest
Step 2 — Project Structure
products/
├── state/
│ ├── product.actions.ts
│ ├── product.reducer.ts
│ ├── product.effects.ts
│ ├── product.selectors.ts
│ ├── product.model.ts
│
└── product.component.ts
Step 3 — Define State
product.model.ts
export interface Product {
id: number;
title: string;
price: number;
thumbnail: string;
}
export interface ProductState {
loading: boolean;
products: Product[];
error: string | null;
}
export const initialProductState: ProductState = {
loading: false,
products: [],
error: null
};
Step 4—Create Actions
product.actions.ts
import { createAction, props } from '@ngrx/store';
import { Product } from './product.model';
export const LoadProductAction =
createAction('[Product Page] Load Products');
export const LoadProductActionSuccess =
createAction(
'[Product API] Load Products Success',
props<{ products: Product[] }>()
);
export const LoadProductActionError =
createAction(
'[Product API] Load Products Failure',
props<{ error: string }>()
);
Action describes what happened, not how.
Step 5—Create Reducer
product.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as ProductActions from './product.actions';
import { initialProductState } from './product.model';
export const PRODUCT_FEATURE_KEY = 'products';
export const productReducer = createReducer(
initialProductState,
on(ProductActions.LoadProductAction, state => ({
...state,
loading: true,
error: null
})),
on(ProductActions.LoadProductActionSuccess, (state, { products }) => ({
...state,
loading: false,
products
})),
on(ProductActions.LoadProductActionError, (state, { error }) => ({
...state,
loading: false,
error
}))
);
Reducer must be pure (no HTTP, no side effects).
Step 6—Create Effect
product.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { HttpClient } from '@angular/common/http';
import { switchMap, map, catchError, of } from 'rxjs';
import * as ProductActions from './product.actions';
@Injectable()
export class ProductEffects {
http = inject(HttpClient);
actions$ = inject(Actions);
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.LoadProductAction),
switchMap(() =>
this.http.get<any>('https://dummyjson.com/products').pipe(
map(res =>
ProductActions.LoadProductActionSuccess({
products: res.products
})
),
catchError(err =>
of(ProductActions.LoadProductActionError({
error: err.message
}))
)
)
)
)
);
}
Effects handle asynchronous operations.
Step 7 — Create Selectors
product.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductState } from './product.model';
import { PRODUCT_FEATURE_KEY } from './product.reducer';
export const selectProductState =
createFeatureSelector<ProductState>(PRODUCT_FEATURE_KEY);
export const selectProductLoading =
createSelector(selectProductState, state => state.loading);
export const selectProductLoadingError =
createSelector(selectProductState, state => state.error);
export const selectProductLoadingSuccess =
createSelector(selectProductState, state => state.products);
Selector reads data from store efficiently.
Step 8—Register Everything (Standalone)
main.ts
import { provideStore } from '@ngrx/store';
import { provideState } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { provideHttpClient } from '@angular/common/http';
import { isDevMode } from '@angular/core';
providers: [
provideStore(),
provideState(PRODUCT_FEATURE_KEY, productReducer),
provideEffects([ProductEffects]),
provideHttpClient(),
provideStoreDevtools({
maxAge: 25,
logOnly: !isDevMode()
})
]
Step 9—Component Usage
product.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import * as ProductSelectors from './state/product.selectors';
import * as ProductActions from './state/product.actions';
@Component({
selector: 'app-product',
templateUrl: './product.component.html'
})
export class ProductComponent implements OnInit {
private store = inject(Store);
loading$ = this.store.select(ProductSelectors.selectProductLoading);
error$ = this.store.select(ProductSelectors.selectProductLoadingError);
products$ = this.store.select(ProductSelectors.selectProductLoadingSuccess);
ngOnInit(): void {
this.products$.pipe(take(1)).subscribe((products) => {
if (!products || products.length === 0) {
this.store.dispatch(ProductActions.LoadProductAction());
}
});
}
onReload() {
this.store.dispatch(ProductActions.LoadProductAction());
}
}
Why API Calls Again on Refresh?
Because the NgRx store is in-memory.
Refresh = App restarts Store = initial state → API called again
To prevent that, use Meta-Reducers (Hydration).
Step 10 — Meta-Reducer (State Persistence)
hydration.metareducer.ts
import { ActionReducer, INIT, UPDATE } from '@ngrx/store';
export function hydrationMetaReducer(reducer: ActionReducer<any>) {
return (state, action) => {
if (action.type === INIT || action.type === UPDATE) {
const storedState = localStorage.getItem('appState');
if (storedState) {
return JSON.parse(storedState);
}
}
const nextState = reducer(state, action);
localStorage.setItem('appState', JSON.stringify(nextState));
return nextState;
};
}
Register Meta Reducer
provideStore(
{},
{ metaReducers: [hydrationMetaReducer] }
),
Final Lifecycle
Component dispatches action
↓
Reducer sets loading true
↓
Effect calls API
↓
Success/Failure action dispatched
↓
Reducer updates state
↓
Selector emits new data
↓
UI updates
Conclusion
You now have:
- Feature state
- Proper reducer
- Effects with async handling
- Selectors
- DevTools integration
- Persistent store using meta-reducer
- Production-ready structure
메타데이터
- post_id
- 907b7b76ff25
- slug
- complete-ngrx-setup-in-angular-standalone-907b7b76ff25
- url
- https://medium.com/@siva-cs579/complete-ngrx-setup-in-angular-standalone-907b7b76ff25
- canonical_url
- https://medium.com/@siva-cs579/complete-ngrx-setup-in-angular-standalone-907b7b76ff25
- author_url
- https://medium.com/@siva-cs579
- status
- ok
- fetched_at
- 2026-07-08 21:20:17