DEV Community

Renuka Patil
Renuka Patil

Posted on • Edited on

NGRX with Angular 16

Image description


Let's breakdown the diagram into logical sections, each focusing on one concept or step.

  1. What is NgRx?
  2. Why Use NgRx with Angular 16?
  3. Key Features of NgRx
  4. Setup and Installation
  5. Implementing Store, Actions, Reducers, and Effects
  6. Code Walkthrough
  7. Testing Your NgRx Setup
  8. 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 }>());

Enter fullscreen mode Exit fullscreen mode

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] }))
);
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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 })))
      ))
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }),
  ]
});
Enter fullscreen mode Exit fullscreen mode

3.Set Up Effects:

import { provideEffects } from '@ngrx/effects';
import { CartEffects } from './effects/cart.effects';

bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ cart: cartReducer }),
    provideEffects([CartEffects]),
  ]
});
Enter fullscreen mode Exit fullscreen mode

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(),
  ]
});

Enter fullscreen mode Exit fullscreen mode

-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:

  1. Actions: Add, remove, and clear cart items.
  2. Reducer: Update the cart state based on actions.
  3. Effect: Fetch cart data from an API when the app initializes.
  4. Selectors: Get the total item count or cart total.

  1. Component dispatches action to indicate something is happened. Image description
  2. 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. Image description

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.

Image description


Let's dig into this with actually working on the project:

Clone this project from github and you will have directory like this:

Image description

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:

Image description

Let's see steps wise implementation of the project:

  1. Show all the list of grocery on webpage - using Reducer
  2. Add action and dispatched from increment button in grocery component - using action, reducer and selector.
  3. Show the added grocery item in bucket component - using selector
  4. Add action for remove the item from grocery component and remove from bucket component - using action, reducer and selector
  5. Filter the list of grocery component by type selected - using Selector
  6. 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);
Enter fullscreen mode Exit fullscreen mode
  • 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 { }

Enter fullscreen mode Exit fullscreen mode
  • 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

Image description

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.

Image description

  • You will see in redux devtool under action tab there is action dispatched.

Image description

  • Better synax of creating action -Add actions folder in store 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 }>()
  );
Enter fullscreen mode Exit fullscreen mode

and this is how you can dispatch action in grocery.component.ts

Image description

now you will see type and payload dipatched here

Image description

  • 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
            ]

    }),
);
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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
            ]
        }
    }),
);
Enter fullscreen mode Exit fullscreen mode

Now you will see the item is added multiple times

Image description

  • Remove bucket item from reducer state Create action remove from bucket

Image description

Dispatched from decrement button in grocery.component.ts file

Image description

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);
        }


    })
);
Enter fullscreen mode Exit fullscreen mode

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

Image description

And displaying in bucket.component.html file

Image description

And this is how it looks - added milk 2 times - so the action is dispatched and selected in bucket component.

Image description

4.Filter the list of grocery component by type selected - using Selector
Selectors - transform data before selecting and showing data

  • Create selectorsfolder under the store folder and add file grocery.selectors.ts.

Image description

  • Use in grocery.component.ts file like this

Image description

  • Another way to create selector

Image description

5.Add Selector which select only type of grocery

  • Create selector selectGroceryByType

Image description

  • Use in grocery.component.ts file like this

Image description

  • this is how it select only fruit type

Image description

6. Select with dropdown
Make changes in component so that it can show item for selected type in grocery.component.ts file

Image description

modify the selector for choosing type only

Image description

and modify grocery.component.html file

Image description

Now, when you select from dropdown you will see the items for selected type only.

Image description

  1. Fetch the grocery list from API - using Effects
  2. Until now we have action as synchronous - To make this action Async
  3. to perform side effects such as calling API and fetching data

Image description

  • 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

Image description

  • 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

Image description

2) Create grocery.effects.ts file in store\effects folder

Image description

3) Add this effect in app.module.ts file.

Image description

4) Create actions

Image description

5) Dispatch action from app.component.ts

Image description

6) Add reducer for the actions

Image description

7)If Api will fail to call api

Image description

8) This is how api will call and shows the data, see the networktab in the right side

Image description

You will get whole code in the github.

Happy learning!

Top comments (0)