Let's breakdown the diagram into logical sections, each focusing on one concept or step.
- What is NgRx?
- Why Use NgRx with Angular 16?
- Key Features of NgRx
- Setup and Installation
- Implementing Store, Actions, Reducers, and Effects
- Code Walkthrough
- Testing Your NgRx Setup
- Best Practices and Common Pitfalls
1. What is NgRx?
NgRx is a state management library for Angular applications that implements the Redux pattern using RxJS. It provides a predictable state container, making it easier to manage complex application states.
For example:
- In a shopping cart application, the items in the cart, user details, and order status can be managed centrally using NgRx.
- It eliminates the need to pass data between components manually.
Key Benefits of NgRx:
- Centralized state management.
- Immutability for easier debugging and testing.
- Integration with Angular's Dependency Injection.
- Supports time-travel debugging with tools like Redux DevTools.
2. Why Use NgRx with Angular 16?
Angular 16 introduced improvements such as standalone components, enhanced dependency injection, and better performance. These features align well with NgRx's modular architecture:
Standalone Components: Simplify the integration of NgRx by removing the need for NgModules.
RxJS Integration: NgRx leverages RxJS, which is integral to Angular's reactive programming model.
Tree Shakable Code: Angular 16's optimizations make the NgRx bundle smaller and more efficient.
3. Key NgRx Concepts
You can explain the main building blocks of NgRx with examples:
- Actions: Represent events in the application. For example:
export const addItem = createAction('[Cart] Add Item', props<{ item: Item }>());
Explanation: This action notifies the state that a new item should be added to the cart.
- Reducers: Define how the state changes in response to actions.
const cartReducer = createReducer(
initialState,
on(addItem, (state, { item }) => ({ ...state, items: [...state.items, item] }))
);
Explanation: This reducer updates the cart's state by adding a new item.
- Selectors: Extract specific pieces of state.
export const selectCartItems = createSelector(
selectCartState,
(state) => state.items
);
Explanation: Use selectors to avoid accessing the state object directly in components.
- Effects: Handle side effects such as API calls.
@Injectable()
export class CartEffects {
loadCart$ = createEffect(() =>
this.actions$.pipe(
ofType(loadCart),
switchMap(() => this.cartService.getCart().pipe(
map(cart => loadCartSuccess({ cart })),
catchError(error => of(loadCartFailure({ error })))
))
)
);
}
Explanation: Effects let you keep side effects like HTTP requests out of reducers.
4. Setting Up NgRx in an Angular 16 Project
Let's walk through the setup step-by-step:
1.Install NgRx Packages:
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
2.Initialize the Store: Import the StoreModule in your AppModule or standalone root component:
import { StoreModule } from '@ngrx/store';
import { cartReducer } from './reducers/cart.reducer';
bootstrapApplication(AppComponent, {
providers: [
provideStore({ cart: cartReducer }),
]
});
3.Set Up Effects:
import { provideEffects } from '@ngrx/effects';
import { CartEffects } from './effects/cart.effects';
bootstrapApplication(AppComponent, {
providers: [
provideStore({ cart: cartReducer }),
provideEffects([CartEffects]),
]
});
5. Debugging and Tools
- Install the Redux DevTools browser extension.
- Enable it in your app:
import { provideStoreDevtools } from '@ngrx/store-devtools';
bootstrapApplication(AppComponent, {
providers: [
provideStore({ cart: cartReducer }),
provideStoreDevtools(),
]
});
-This allows you to monitor dispatched actions, inspect state changes, and even time-travel through state history.
6. Common Pitfalls and Best Practices
- Avoid Mutating the State: Always return a new state object in reducers. Use ...spread syntax for immutability.
- Keep Reducers Simple: Move complex logic to services or effects.
- Use Strong Typing: Define interfaces for state and actions to prevent runtime errors.
7. Real-World Use Case: Managing a Shopping Cart
Include a hands-on example:
- Actions: Add, remove, and clear cart items.
- Reducer: Update the cart state based on actions.
- Effect: Fetch cart data from an API when the app initializes.
- Selectors: Get the total item count or cart total.
- Component dispatches action to indicate something is happened.
- Reducers → How actions modify state are the reducers, so, they detected all of the actions being dispatched in the app and determine how state should be modified. We have reducers for each feature or entity in the app like todo’s, auth, articles and so...
- Reducers are pure functions, take some input and they always will produce same output of same type.
- Reducers detect action, take current state and store new state in the store.
- Store which is often global is where all of the state for your application lives, it is one big object full of data and when a component wants to use some of that state from store(like currently todo’s) it can use Selector to pull in the state that it needs from the store, now that is almost whole story.
3.Effects -> Up to now, we can’t make asynchronous calls to go load in data from a server, this model works well enough, we are dispatching actions like add todo or remove todo because we immediately send the data we need along with that action.
But if we want to load data into an application and add it to the store, we might do this with an async operation that’s going to call a service, that’s going to load in some data from API somewhere and that’s going to take some time to complete, so this is where effects come into play like reducer. An effect can also listen to all of the actions being dispatched in the app but unlike a reducer that has to be a pure function intended just to update the state an effect can run whatever side effects it like in the case of loading data we would first dispatch an action like load todo this will be immediately handled by the reducers but we don’t have data we need yet because we need make a call to the service to load the data so all the reducers will do in response to that load to do’s action is do some like set a flag in the store changing status to do state to loading or something like that.
However our effects will also listen for that load todo action and where it detects this, it will go off and fetch the todo from the service and once data has finished loading it will dispatch a new action either load todo success or failure.
now reducer can handle this load to do a successful action that was just dispatched from effect and this action will already have all of the data available to it so the reducer can set that status flag to success and it can update the todo’s property in the store with the data as we just loaded.
Let's dig into this with actually working on the project:
Clone this project from github and you will have directory like this:
run these commands in terminal:
ng install
or yarn install
run the project with command
ng start
After running the project you will see output like this:
Let's see steps wise implementation of the project:
- Show all the list of grocery on webpage - using Reducer
- Add action and dispatched from increment button in grocery component - using action, reducer and selector.
- Show the added grocery item in bucket component - using selector
- Add action for remove the item from grocery component and remove from bucket component - using action, reducer and selector
- Filter the list of grocery component by type selected - using Selector
- Fetch the grocery list from API - using Effects
1. Show all the list of grocery on webpage - using Reducer
- Add
store
folder - In store folder add
reducers
folder - Add file
grocery.reducer.ts
and add below code
import { createReducer, on } from "@ngrx/store"
import { Grocery } from "src/models/grocery.model"
const initialState:Grocery[] = [
{ "id": 1, "name": "Milk", "type": "fruit" },
{ "id": 2, "name": "Banana", "type": "fruit" },
{ "id": 3, "name": "lays chips", "type": "snacks" },
{ "id": 4, "name": "doritos", "type": "snacks" }
]
export const groceryReducer = createReducer(initialState);
- Go to
app.module.ts
and add above reducer here
@NgModule({
declarations: [
//...existing components
],
imports: [
//...existing properties
**StoreModule.forRoot({groceries: groceryReducer}),**
//...existing properties
})
],
providers: [ ],
bootstrap: [AppComponent]
})
export class AppModule { }
- Now, go to the
http://localhost:4200/
and inspect the page and open redux devtool - You will see he initialState of groceries object present under state in redux devtool
2. Add actions and dispatched them from incrementand decrement button in grocery component - using action, reducer and selector.
- Dispatching action -> You have dispatched action from increment button in
grocery.component.ts
file. so, whenever you push increment button on UI, you will see action displayed in redux devtool.
- You will see in redux devtool under action tab there is action dispatched.
- Better synax of creating action
-Add
actions
folder instore
folder -Add file bucket.action.ts -Add below code there
import { createAction} from "@ngrx/store";
import { Bucket } from "src/models/bucket.model";
export const addToBucket = createAction(
'[Bucket] Add to Bucket',
props<{ payload: Bucket }>()
);
and this is how you can dispatch action in grocery.component.ts
now you will see type and payload dipatched here
- Bind Dispatched action with reducer and update the state.
Action will dispatched from grocery component → bucket.reducer → we will modify the state
i)Create another file bucket.reducer.ts
in store/reducers
folder.
Here it takes action and we telling what to do with data- so this addToBucket has access of 2 things - state and action - so in state we are sending the state as it is and new payload as action which we are dipatching the action from component.
import { createReducer, on } from "@ngrx/store";
import { Bucket } from "src/models/bucket.model";
import { addToBucket, removeFromBucket } from "../actions/bucket.action";
const initialState: Bucket[] = [];
export const bucketReducer = createReducer(initialState,
on(addToBucket, (state, action) => {
return [
...state,
action.payload
]
}),
);
Now you can see in redux devtool, when we are pushing increment button it dispatch the action with payload which we are collecting in reducer and adding new state.
What if we are adding same item multiple times then only quantity will increment.
import { createReducer, on } from "@ngrx/store";
import { Bucket } from "src/models/bucket.model";
import { addToBucket, removeFromBucket } from "../actions/bucket.action";
const initialState: Bucket[] = [];
export const bucketReducer = createReducer(initialState,
on(addToBucket, (state, action) => {
const bucketItem = state.find(item => item.id === action.payload.id);
if (bucketItem) {
return state.map(item => {
return item.id === action.payload.id ? { ...item, quantity: item.quantity + action.payload.quantity } : item
});
}else{
return [
...state,
action.payload
]
}
}),
);
Now you will see the item is added multiple times
- Remove bucket item from reducer state Create action remove from bucket
Dispatched from decrement button in grocery.component.ts
file
Listen this dispatched action in bucket reducer.
import { createReducer, on } from "@ngrx/store";
import { Bucket } from "src/models/bucket.model";
import { addToBucket, removeFromBucket } from "../actions/bucket.action";
const initialState: Bucket[] = [];
export const bucketReducer = createReducer(initialState,
on(addToBucket, (state, action) => {
const bucketItem = state.find(item => item.id === action.payload.id);
if (bucketItem) {
return state.map(item => {
return item.id === action.payload.id ? { ...item, quantity: item.quantity + action.payload.quantity } : item
});
}else{
return [
...state,
action.payload
]
}
}),
on(removeFromBucket, (state, action) => {
const bucketItem = state.find(item => item.id === action.payload.id);
if (bucketItem && bucketItem.quantity > 1) {
return state.map(item => {
return item.id === action.payload.id ? { ...item, quantity: item.quantity - 1 } : item
});
}else{
return state.filter(item => item.id !== action.payload.id);
}
})
);
Now, you see after decrement button pushed the quantity of item will reduced by 1.
3.Show the added grocery item in bucket component - using selector
We are selecting the state in bucket.component.ts
file
And displaying in bucket.component.html
file
And this is how it looks - added milk 2 times - so the action is dispatched and selected in bucket component.
4.Filter the list of grocery component by type selected - using Selector
Selectors - transform data before selecting and showing data
- Create
selectors
folder under thestore
folder and add filegrocery.selectors.ts
.
- Use in
grocery.component.ts
file like this
- Another way to create selector
5.Add Selector which select only type of grocery
- Create selector selectGroceryByType
- Use in
grocery.component.ts
file like this
- this is how it select only fruit type
6. Select with dropdown
Make changes in component so that it can show item for selected type in grocery.component.ts
file
modify the selector for choosing type only
and modify grocery.component.html
file
Now, when you select from dropdown you will see the items for selected type only.
- Fetch the grocery list from API - using Effects
- Until now we have action as synchronous - To make this action Async
- to perform side effects such as calling API and fetching data
- We have
db.json
file where there is list of the items, now we will use that and will fetch the items from here using effects. - Add this line in package.json file
- now run the command
npm run apiServer
in terminal, so that our db.json file will run. 1)now we will call data from api for that we will keep initialState as null
2) Create grocery.effects.ts
file in store\effects
folder
3) Add this effect in app.module.ts
file.
4) Create actions
5) Dispatch action from app.component.ts
6) Add reducer for the actions
7)If Api will fail to call api
8) This is how api will call and shows the data, see the networktab in the right side
You will get whole code in the github.
Happy learning!
Top comments (0)