Skip to content

[Angular | nx.dev] Adding NGRX - State Management

Georg Höller
Georg Höller
6 min read
[Angular | nx.dev] Adding NGRX - State Management
Photo by Guillaume Bolduc / Unsplash

Recently I uploaded a post about creating a single project workspace structure using nx.dev and angular. Now it's time to add ngrx to our newly created project. I will continue with the project structure from the previous post.

[Angular | nx.dev] Single Project Workspace Structure
The concept of a monorepo is great but not always doable because of all sorts of circumstances. For me the critical point always is git - I want each project to have its own repository! But this doesn’t mean you can’t use a monorepo framework for a single project. We

Setup

Installing ngrx

npm install @ngrx/store
npm install @ngrx/component-store
npm install @ngrx/effects
npm install @ngrx/entity
npm install @ngrx/router-store

npm install @ngrx/schematics --save-dev
npm install @ngrx/store-devtools --save-dev
More information: https://ngrx.io/guide/store/install

Prepare Your Workspace

First, we have to add the root level store. This will grow with each feature, by adding feature level states.
In my case I will add the root level store to the node-note application and a feature level state to the data-access-notes library. After that, we will use the store in the feature-note-list to add and display notes.

Root Level Store

Start by creating the root level store with the following command. Make sure to add the correct path to your root module.

nx g ngrx app --module=apps/node-note/src/app/app.module.ts --root

You will see some changes in the import section of your app module. That is the basic initialization for ngrx store and effects. It should be good as it is for now.

Feature Level Store

Now add the feature level store to the data-access-notes library. It's common practice to place your state-relevant code in a folder called '+state'. With the plus as a prefix, it should be always on top of your file tree.

nx g ngrx notes --module=libs/nn-notes/data-access-notes/src/lib/data-access-notes.module.ts --directory +state/notes
-- Is this the root state of the application? (y/N) · false
-- Would you like to use a Facade with your NgRx state? (y/N) · true

This command will create all necessary files and add some basic functionality. In addition to that, it updates the library module with the initialization code we already know from the root level store. But instead of forRoot it calls forFeature.

The store preparations should be done. Let's see how the store works!

Using The Store

For this tutorial, our goal is to be able to create and read notes. In addition to this, we should get an error state if a note with the same id is already in the notes array.
We will use the note-overview component we created in the previous tutorial.

Add a note

Add some basic HTML to be able to create a new note.

<h1>Notes List</h1>
<div>
  <div>ID:</div>
  <input #noteId type="number" />
  <div>Text:</div>
  <input #noteText />
</div>
<button (click)="add(noteId.value, noteText.value)">Add</button>
note-overview.component.html
add(id: string, text: string) {
}
note-overview.component.ts

Now we have to add some logic to our state that we can add notes. In the feature store command, we checked the facade option. So we have to use the generated facade file to add the note because it should be the only file that gets referenced.

Actions:

Create a new action definition for adding notes. The string parameter defines the internal name, the second parameter defines input parameters.

export const addNote = createAction(
  '[Notes/API] Add Note',
  props<{ note: NotesEntity }>()
);
notes.actions.ts

Reducers:

Reducers are responsible for updating the state. Add the following code to add the new note to the state.

on(NotesActions.addNote, (state, { note }) =>
  notesAdapter.addOne(note, { ...state })
),
notes.reducer.ts

Facade:

Finally, dispatch the action with a new function in the facade.

addNode(note: NotesEntity) {
  this.store.dispatch(NotesActions.addNote({ note }));
}
notes.facade.ts

Now call the facade function inside your recently added add function.

constructor(private notesFacade: NotesFacade) {}

add(id: string, text: string) {
    this.notesFacade.addNode({
      id: id,
      text: text,
    });
  }
note-overview.component.ts

Display a Note

To display the added note we will use the auto-generated allNotes observable inside the facade. But first, we should take a look at what is happening in the selector.

Selector:

The code is quite simple:

export const getNotesState = createFeatureSelector<State>(NOTES_FEATURE_KEY);

const { selectAll, selectEntities } = notesAdapter.getSelectors();

export const getAllNotes = createSelector(getNotesState, (state: State) =>
  selectAll(state)
);

The predefined function selectAll does the job for us, returning the entire array of notes.

Switching back to our note list, let's add the following HTML to display the notes.

<table>
  <tr>
    <th>ID</th>
    <th>Text</th>
  </tr>
  <tr *ngFor="let node of notesList$ | async">
    <td>{{ node.id }}</td>
    <td>{{ node.text }}</td>
  </tr>
</table>
note-overview.component.html

Now we only have to add the local variable notesList$ and assign allNotes$.

notesList$: Observable<NotesEntity[]> = this.notesFacade.allNotes$;
note-overview.component.ts

Great, the added notes should now appear!

Error Handling

You may recognize that nothing happens when you add the same Id multiple times.

Effects:

Effects should call your service logic. When we add a note, this is the place to call the backend and, for example, add it to the database. We don't use a backend for this tutorial but we will use an effect to add error handling.

First, add two more actions:

export const addNoteSuccess = createAction(
  '[Notes/API] Add Note Success',
  props<{ note: NotesEntity }>()
);

export const addNoteFailure = createAction(
  '[Notes/API] Add Note Failure',
  props<{ error: Error }>()
);
notes.actions.ts

Add a new effect:

addNote$ = createEffect(() =>
  this.actions$.pipe(
    // define action
    ofType(NotesActions.addNote),
    // load previous notes
    withLatestFrom(this.store$.select(NotesSelectors.getAllNotes)),
    concatMap(([action, latestAllNotes]) => {
      // dummy observable to catch error
      return of(null).pipe(
        concatMap(() => {
          // validation
          if (latestAllNotes 
          	  && latestAllNotes.find((n) => n.id == action.note.id))
            return throwError(() => new Error('Duplicate ID'));

          return of(NotesActions.addNoteSuccess(action));
        }),
        catchError((error) => {
          return of(NotesActions.addNoteFailure({ error }));
        })
      );
    })
  )
);
notes.effects.ts

Basically, we use an effect for the validation and call different actions for each outcome.

💡
Do not use catchError for the first concatMap. This will break everything. Catch errors inside!

Now we have to make some modifications to the reducer. Change the action on your recently created reducer and add the error reducer:

on(NotesActions.addNoteSuccess, (state, { note }) =>
  notesAdapter.addOne(note, { ...state })
),
on(NotesActions.addNoteFailure, (state, { error }) => {
  return { ...state, error: error.message };
})
notes.reducer.ts

The selector for the error should already exist:

export const getNotesError = createSelector(
  getNotesState,
  (state: State) => state.error
);
notes.selectors.ts

Now we can finally display the error in our HTML:

<div *ngIf="notesError$ | async; let err" style="color: red">
  Error: {{ err }}
</div>
note-overview.component.html

Don't forget to define the observable variable in the typescript file:

notesError$: Observable<string | null | undefined> =
    this.notesFacade.notesError$;
note-overview.component.ts

Source Code:

GitHub - Georg632/gh-dev
Contribute to Georg632/gh-dev development by creating an account on GitHub.
AngularDeveloper

Comments


Related Posts

Members Public

[ngrok | Angular] External Access to your Local Test Environment

Sometimes you have to test your local hosted API or website with multiple different devices. They may be on the same network and you can open your local ports in some way to get it working but most of the time it's a messy solution. Tech Stack * Angular * ASP.NET

[ngrok | Angular] External Access to your Local Test Environment
Members Public

[Angular | Capacitor] Interact With Your Native Calendar

Recently I had do to implement a simple task: When the user presses the 'save appointment' button, the app should add this event to the native calendar. My first thought was saving an .ical file and open it, till i recognized that this isn't the most user friendly way. After

[Angular | Capacitor] Interact With Your Native Calendar
Members Public

[Angular] Native iOS and Android App - Capacitor

In this blog post we will go through the initial process of how to convert an existing angular project into a cross-platform project. Our focus is on iOS and Android. In addition to this, I show you how to speed up developing and especially testing the app. Setup * Code Editor

[Angular] Native iOS and Android App - Capacitor