NgRx Entities: Wrangling the reducers

Note: This will be a series of posts around how I moved from custom built reducers and state management with NgRx to use the NgRx Entity library. Previous posts include NgRx Entities: Namespaces with TypeScript and NgRx Entities: Updating the selectors for background.


After doing the base setup with the selectors and the state, I can finally move to updating the reducers for the application. This is where the magic and simplicity of the Entity library from NgRx comes to fruition.

A great reference book that goes deeper into this topic: Architecting Angular Applications with Redux, RxJS, and NgRx

Update the actions

I lied. Before I could get into the reducers, I needed to make a modification to the actions first specifically around any action that updates individual records. Since the entity library utilizes key stores from IDs to reference single records, any payload that modifies the record must utilize the type Update from the entity store. This would require the payload sent over to the action to look like:

1
const data = {
2
  id: 1,
3
  changes: {
4
    // object data that is changed
5
  }
6
};

Because of this, the action referencing any change to a record needs to be updated.

Before

1
// store/actions/book.actions.ts
2
...
3
4
export class UpdateBookSuccess implements Action {
5
  public readonly type = BooksActions.UpdateBookSuccess;
6
  constructor(public payload: Book) { }
7
}

After

1
// store/actions/book.actions.ts
2
...
3
4
export class UpdateBookSuccess implements Action {
5
  public readonly type = BooksActions.UpdateBookSuccess;
6
  constructor(public payload: Update<Book>) { }
7
}

On to the reducers!

The biggest change that comes with the addition of the entity adapter is that everything becomes a simple one liner and the complexity is wrapped up internally in the method. No longer do I have to handle the immutable state or select the correct item based on ID. Instead, I get to send in the whole payload and let the adapter sort it out.

Before

1
// store/reducers/books.reducers.ts
2
export const booksReducers = (
3
  state = initialBooksState,
4
  action: BooksActions,
5
): BooksState => {
6
  switch (action.type) {
7
    case BooksActions.GetBookSuccess: {
8
      return {
9
        ...state,
10
        selectedBook: action.payload,
11
      };
12
    }
13
14
    case BooksActions.GetBooksSuccess: {
15
      return {
16
        ...state,
17
        books: action.payload,
18
      };
19
    }
20
21
    case BooksActions.CreateBookSuccess: {
22
      return {
23
        ...state,
24
        books: [...state.books, action.payload]
25
      };
26
    }
27
28
    case BooksActions.UpdateBookSuccess: {
29
      return {
30
        ...state,
31
        books: state.books
32
          .map(
33
            book => book.id === action.payload.id
34
            ? action.payload
35
            : book
36
          ),
37
      };
38
    }
39
40
    case BooksActions.DeleteBookSuccess: {
41
      return {
42
        ...state,
43
        books: state.books.filter((book: Book)
44
          => book.id !== action.payload)
45
      };
46
    }
47
48
    default:
49
      return state;
50
  }
51
};

As you can see, lots of handling of immutable state and filtering based on data passed in through the payload. Let’s clean this up with entities and the new namespace setup!

1
// store/reducers/books.reducers.ts
2
export const booksReducers = (
3
  state = BooksStore.initialState,
4
  action: BooksActions,
5
): BooksStore.State => {
6
  const { adapter } = BooksStore;
7
8
  switch (action.type) {
9
    case BooksActions.GetBookSuccess: {
10
      return adapter.addOne(action.payload, state);
11
    }
12
13
    case BooksActions.GetBooksSuccess: {
14
      return adapter.addAll(action.payload, state);
15
    }
16
17
    case BooksActions.CreateBookSuccess: {
18
      return adapter.addOne(action.payload, state);
19
    }
20
21
    case BooksActions.UpdateBookSuccess: {
22
      return adapter.updateOne(action.payload, state);
23
    }
24
25
    case BooksActions.DeleteBookSuccess: {
26
      return adapter.removeOne(action.payload, state);
27
    }
28
29
    default:
30
      return state;
31
  }
32
};

Much cleaner as the complexity is now all handled by the adapter!

As a reminder, anyplace the payload is being used to update a record, the design must contain the form:

1
const data = {
2
  id: someId,
3
  changes: {
4
    // object data that is changed
5
  }
6
};

After all of these updates, I am pretty happy how less complex the reducers have turned out. Relying on the entity setup has helped move to a more streamlined use which makes my code look and feel cleaner.

Filed under: Code