How I’ve Set Up NgRx in Angular 16 with Standalone Components

A guide on how to implement NgRx in Angular 16

Kamil Konopka
Bits and Pieces

--

Photo by Temple Cerulean on Unsplash

NgRx is one of the most popular state management systems within the entire Angular ecosystem. But what is actually a state management system and why do I need one within my application?

Let’s start with a quick explanation:

  • State — set of data, which is being used within our application
  • Management system— set of centralized tools to simplify / organize / structure data used by our application

By definition, state management system data is globally scoped, which means, it can be used / accessed from anywhere within our application. Unlike plain Angular Service, it cannot be scoped to specific part / feature of the application.

NgRx additionally offers @ngrx/component-store library, specifically in order to provide scoping functionality. Component Store can be lazily loaded and it is being tied to component lifecycle (initialized with component creation and destroyed alongside) — will be covered with another article.

But we’re here to focus more on a global-scoped approach. NgRx library provides Redux implementation of Flux Architecture, which means unidirectional flow and predictability.

Not every data set needs state management system usage! You don’t have to put everything into NgRx store! If your data needs to be shared / reused within different application features / unrelated components or contains dictionaries it makes sense to use NgRx! Otherwise you might not need it!

Flux architecture provides a clear separation of concerns, which makes our code clean, but there are also trade-offs like the necessity of some boilerplate code to implement. It takes a bit more to set everything up and running, but it’s worth every second of your time. And the longer you will be using it, you’ll be leveraging its features with easiness.

  • State — represents JSON plain object which is a single source of truth of globally scoped application data.
  • Selectors —memoized functions allow one to take a specific chunk of the state and listen/subscribe to its changes.
  • Reducers — the only place where directly the state object is being updated in an immutable way. Guarantees predictable updates. You could use an adapter from @ngrx/entity library to ensure some of the most generic immutable operations out of the box.
  • Effects — classes (now even functions!) to handle asynchronous operations, like data fetch. This is the place where we can listen to specific action dispatching. In most cases, effects are returning an action, but it is not essential.
  • Actions — these are specific operations/commands that are being dispatched, like data updates (CRUD operations). This is the only way to update the NgRx application state.

Additionally, specifically in Angular applications, it makes sense to apply the Facade Services technique — a class which implements the facade design pattern is being used as the only possibility to access data/ trigger actions from the store.

Also, check out these Angular dev tools to simplify Angular development in 2023.

Usage example: when the data from the store needs to be accessed, we need to inject via Dependency Injection, Facade Service into the component that needs to use the data/methods/attributes from there. No direct store dependency injection into the component is allowed. This is crucial in case of providing a single implementation, that can be reused elsewhere (different areas of the application).

To start just execute the command below:

ng add @ngrx/store@latest

NgRx store will be initially added to your package.json file, also additional eslint package will get installed, if you’re using eslint within your application. If you navigate to your main.ts file, you will notice, that provideStore() was added to your providers array automatically.

Next, we need to install some additional NgRx libraries with the following command:

npm install @ngrx/{effects,entity,store-devtools} --save

We are planning to set up:

  • @ngrx/store-devtools, in order to connect our application with browser dev tools and have this nice experience of observing our state changes/actions triggering etc.
  • @ngrx/effects library is needed, as will be doing asynchronous operations.
  • @ngrx/entity library will help us manage our data in a scalable manner, using a dictionary-like approach, where our entities are being stored within Record<id, entity> — where the key is our entity's unique identifier and the value is the entity itself. This way, even if we handle hundred of thousands of entities, our storing mechanism will be as performant as with just a couple of (big O algorithm) those. Cheeky right?

Now, we have to create our actions, reducers and effects, but before we do that let’s focus on the actual structure of our store. I prefer a feature-based approach, where each entity type is handled separately. This technique will ensure nice separation, maintainability and atomic approach.

Our @ngrx/store-devtools library has been successfully fetched, therefore we can hook it up to our standalone application like below:

import { enableProdMode, isDevMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { environment } from './environments/environment';
import { AppComponent } from './app/app.component';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';

if (environment.production) {
enableProdMode();
}

bootstrapApplication(AppComponent, {
providers: [
provideStore(),
provideStoreDevtools({
maxAge: 25, // Retains last 25 states
logOnly: !isDevMode(), // Restrict extension to log-only mode
autoPause: true, // Pauses recording actions and state changes when the extension window is not open
trace: false, // If set to true, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code
traceLimit: 75, // maximum stack trace frames to be stored (in case trace option was provided as true)
}),
],
}).catch(err => console.error(err));

In order to be able to fully utilize Store Devtools, I need to install the browser plugin. As I use Chrome Browser, it will be Redux Devtools which is accessible under the following link: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en

I prefer to create a dedicated folder to keep the entire store implementation in one place with the following structure:

- store
|-feature-store-1
|-feature-store-2
|-feature-store-2
| |-feature-store-1.actions.ts
| |-feature-store-1.effects.ts
| |-feature-store-1.facade.ts
| |-feature-store-1.reducers.ts
| |-feature-store-1.selectors.ts
| |-feature-store-1.state.ts
| |-... // do not forget about unit tests files with .spec.ts! :)

In my case, I’ve created messages feature folder within the store and my structure looks like the below:

- store
|-messages
| |-messages.actions.ts
| |-messages.effects.ts
| |-messages.facade.ts
| |-messages.reducers.ts
| |-messages.selectors.ts
| |-messages.state.ts

Let’s now fill the files with some basic configuration.

Here’s how my messages.state.ts file looks:

import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { Message } from '../../messenger';

export interface MessagesState extends EntityState<Message> {
loading: [];
}

export const selectId = ({ id }: Message) => id;

export const sortComparer = (a: Message, b: Message): number =>
a.publishDate.toString().localeCompare(b.publishDate.toString());

export const adapter: EntityAdapter<Message> = createEntityAdapter(
{ selectId, sortComparer }
);

export const initialState: MessagesState = adapter.getInitialState(
{ loading: [] }
);

I am using the @ngrx/entity library to conveniently manage my data. Within my feature state definition I extend EntityState generic provided the library with my own data interface Message. Then I’m declaring:

  • selectId function to tell the adapter, how to create a unique identifier for my entity object value
  • sortComparer function definition (comparer is not required, you do not have to specify it).

Finally, I create my adapter instance and initialState, which will be passed to the reducer as starting point. Thanks to the adapter my initialState will look like this within the browser dev tools:

{ 
messages: {
ids: [],
entities: {},
loading: [],
},
}

We haven’t added ids and entities attributes, this is the adapter’s magic :-). So, when we add a new entity by dispatching an action, our state will look as follows:

{ 
messages: {
ids: ['1'],
entities: {
'1': {
... // my Message entity attributes
}
},
loading: [],
},
}

Let us add now actions to the messages.actions.ts file:

import { createAction, props } from '@ngrx/store';
import { Message } from '../../messenger';

export const messagesKey = '[Messages]';

export const addMessage = createAction(
`${messagesKey} Add Message`,
props<{ message: Message }>()
);

export const deleteMessage = createAction(
`${messagesKey} Delete Message`,
props<{ id: string }>()
);

NgRx library allows us to create ActionGroups, but I will cover this topic in separate article!

Next, on our agenda is the messages.reducers.ts file. This is how it should look:

import { ActionReducer, createReducer, on } from '@ngrx/store';
import { adapter, initialState, MessagesState } from './messages.state';
import { addMessage, deleteMessage } from './messages.actions';

export const messagesReducers: ActionReducer<MessagesState> = createReducer(
initialState,
on(addMessage, (state: MessagesState, { message }) =>
adapter.addOne(message, state)),
on(deleteMessage, (state: MessagesState, { id }) =>
adapter.removeOne(id, state))
);

We create messagesReducer with the createReducer function from @ngrx/store library. We pass initialState declared in messages.state.ts and we use an already created adapter on every case.

NgRx library is offering Feature creation with very similar experience to React Redux Toolkit or Vuex! Here’s the link to the article related to Feature Creators!

As an aside, organize your NgRx code based on Feature modules. Each Feature module should encapsulate related actions, reducers, effects, selectors, and state models. You can then independently package, auto-document, share, and reuse them using Bit. This approach keeps your codebase organized, and easier to understand and maintain.

Learn more:

All we need to do now is to hook up our messages feature store into the store provider like the below:

import { enableProdMode, isDevMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { environment } from './environments/environment';
import { AppComponent } from './app/app.component';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { messagesReducers } from './app/store/messages/messages.reducers';

if (environment.production) {
enableProdMode();
}

bootstrapApplication(AppComponent, {
providers: [
provideStore({ messages: messagesReducers }), // <-- this is the place! :-)
provideStoreDevtools({
maxAge: 25, // Retains last 25 states
logOnly: !isDevMode(), // Restrict extension to log-only mode
autoPause: true, // Pauses recording actions and state changes when the extension window is not open
trace: false, // If set to true, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code
traceLimit: 75, // maximum stack trace frames to be stored (in case trace option was provided as true)
}),
],
}).catch(err => console.error(err));

At this point, if we want to test our achievement, we are perfectly fine to do some sanity check, by injecting the store to our random component and dispatching an action like so:

import { Component, inject, OnInit } from '@angular/core';
import { MessagesService } from '../../services';
import { Message } from '../../models';
import { Store } from '@ngrx/store';
import { addMessage } from '../../../store/messages/messages.actions';

@Component({
selector: 'app-messenger',
templateUrl: './messenger.component.html',
styleUrls: ['./messenger.component.scss'],
standalone: true,
})
export class MessengerComponent implements OnInit {
private readonly store: Store = inject(Store);

addMessage(): void {
const message: Message = { /* message object with id attribute */ };
this.store.dispatch(addMessage({ message: { content } }));
}

ngOnInit(): void {
this.addMessage();
}

Within your Chrome (or another browser of your choice) Redux dev tools, you will be able to see the dispatched action / its payload / diff / most recent state, just like below:

We can also add the main application state interface, which can be called AppState, the ideal place for it seems to be within the index.ts file located directly within the store folder:

// file location: store/index.ts
import { MessagesState } from './messages';

export interface AppState {
messages?: MessagesState;
}

Now, once we have actual data within our store, we can create selectors within our messages.selectors.ts file:

import { AppState } from '../index';
import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store';
import { MessagesState } from './messages.state';
import { Message } from '../../messenger';

export const selectMessagesFeature: MemoizedSelector<AppState, MessagesState> =
createFeatureSelector<MessagesState>('messages');

export const selectMessages: MemoizedSelector<AppState, Message[]> =
createSelector(
selectMessagesFeature,
({ entities }: MessagesState): Message[] =>
Object.values(entities) as Message[]
);

We’re almost there, but we want to be pro and follow DRY programming principles. We do not want to inject store whenever we need to access our state data. We will introduce Facade specific to our MessagesState!

import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Message } from '../../messenger';
import { addMessage } from './messages.actions';
import { selectMessages } from './messages.selectors';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MessagesFacade {
private readonly store: Store = inject(Store);

readonly messages$: Observable<Message[]> = this.store.select(selectMessages);

addMessage(message: Message): void {
this.store.dispatch(addMessage({ message }));
}
}

The next step is to replace our test usage with actual MessagesFacade:

import { Component, inject, OnInit } from '@angular/core';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { MessagesService } from '../../services';
import { Message } from '../../models';
import { MessagesFacade } from '../../../store/messages.facade';

@Component({
selector: 'app-messenger',
templateUrl: './messenger.component.html',
styleUrls: ['./messenger.component.scss'],
standalone: true,
})
export class MessengerComponent implements OnInit {
private readonly messagesFacade: MessagesFacade = inject(MessagesFacade);

addMessage(): void {
const message: Message = { /* message object with id attribute */ };
this.messagesFacade.addMessage(message));
}

ngOnInit(): void {
this.addMessage();
}

As a result, there is no reference to store directly, everything related to MessagesState is being navigated via MessagesFacade. Now you’re perfectly capable of adding new messages to our store from anywhere in our application!

So far, we are handling our synchronous operations with our NgRx store configuration, there’s one bit left which is related to asynchronous operations!

If we want to persist our messages somewhere within the DB, we need to call the respective endpoint and pass it to our Back End implementation. As this operation takes time, it is being handled asynchronously (we do not know if the response will ever arrive and if it does, we do not know when!). This is where the @ngrx/effects library comes into play! Let us set up the effects within our standalone component-based state management implementation!

We need to add two additional actions to our messages.actions.ts file. This is how the file should look like:

import { createAction, props } from '@ngrx/store';
import { Message } from '../../messenger';

export const messagesKey = '[Messages]';

export const addMessage = createAction(
`${messagesKey} Add Message`,
props<{ message: Message }>()
);

export const deleteMessage = createAction(
`${messagesKey} Delete Message`,
props<{ id: string }>()
);

export const deleteMessageSuccess = createAction(
`${messagesKey} Delete Message Success`
);

export const deleteMessageError = createAction(
`${messagesKey} Delete Message Error`
);

Also, some ApiService is needed in order to keep the API communication layer abstracted (../../shared/services/messages-api.service.ts):

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MessagesApiService {
private readonly http: HttpClient = inject(HttpClient);

deleteMessage(id: string): Observable<void> {
return this.http.delete<void>(`/${id}`);
}
}

There are two options to declare effects, you can either use a class or functional approach. As the functional approach is relatively new, we will use it in our example (messages.effects.ts file):

import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { deleteMessage, deleteMessageError, deleteMessageSuccess } from './messages.actions';
import { MessagesApiService } from '../../shared/services/messages-api.service';
import { catchError, map, mergeMap } from 'rxjs';

export const deleteMessage$ = createEffect(
(actions$: Actions = inject(Actions), messagesApiService: MessagesApiService = inject(MessagesApiService)) => {
return actions$.pipe(
ofType(deleteMessage),
mergeMap(({ id }) =>
messagesApiService.deleteMessage(id).pipe(
map(() => deleteMessageSuccess()),
catchError(() => [deleteMessageError()])
)
)
);
},
{ functional: true }
);

For starters, we use the createEffect function from the @ngrx/effects library and we pass two arguments, one is an actual effect we want to create and the second one is a configuration object. As we are following a functional approach, our configuration will contain a functional flag set to true.

We need to inject actions$ and our ApiService as arguments to our effect, exactly as it is within the following example. This is preferred way as such effect is definitely easier to test!

We listen to the ApiService call and react either on success or on failure with specific actions.

Just one more action dispatch to add within our MessagesFacade and we are ready to test our effect:

  deleteOne(id: string): void {
this.store.dispatch(deleteMessage({ id }));
}

Now, you’re ready to call deleteOne message from anywhere in your application!

Voila! We’re done here! NgRx implementation has never looked simpler!

Build Angular Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

JavaScript/Typescript experience with biggest corporates and scalable software, with focus on reactive / performance programming approach / high code quality.