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.
After working with NgRx for a while and writing custom reducers, I figured it was time to dive into adding the Entity library. My hesitancy at first was that I had already gone down the road of creating my own reducers and switching to use entities I felt would be a hill to climb — I was wrong.
A great reference book that goes deeper into this topic: Architecting Angular Applications with Redux, RxJS, and NgRx
Namespaces to the rescue
All of my store and states were based on a main interface state for the given part of the store. This was useful because my store only ever cared about the state as it got passed around. Now, with the introduction of entities, there turns out to be a lot of other goodies that the state of the store needed per item. To package all of these up, I ended up creating a namespace for each item as their own sub-store.
For example, let’s imagine we are storing books in our store.
Before
1 | // books.state.ts |
2 | |
3 | export interface BooksState { |
4 | books: Book[]; |
5 | selectedBook: Book; |
6 | } |
7 | |
8 | export const initialBooksState: BooksState = { |
9 | books: null, |
10 | selectedBook: null, |
11 | } |
After
Adding entities ends up blowing up the state file, but it provides a ton of functionality that makes things a lot easier later on.
1 | // books.state.ts |
2 | |
3 | export namespace BooksStore { |
4 | export interface State extends EntityState<Book> { |
5 | selectedBook: number | null; |
6 | } |
7 | |
8 | export const adapter: EntityAdapter<Book> |
9 | = createEntityAdapter<Book>(); |
10 | |
11 | export const initialState: State = adapter.getInitialState({ |
12 | selectedBook: null, |
13 | }); |
14 | |
15 | export const { |
16 | selectAll, |
17 | selectEntities, |
18 | selectIds, |
19 | selectTotal, |
20 | } = adapter.getSelectors(); |
21 | } |
A couple things to point out with the code above is that the new state interface extends the default entity state inferring the book interface. Hidden in here is the default collection state as that is the entity key array provided by the extension.
The adapter is also new, but that is where the magic of the entity library comes from. It allows me to set an initial state and also provides all the selectors needed to grab the items from the store. It also provides the methods for adding to the store in the reducer.
In use
Now with everything packaged up in the namespace of BookStore
it makes it very simple to know what item you are using not only in shared context files, but also provides a similar API for each store.
1 | // store/state/index.ts |
2 | export { BookStore } from './books.state.ts'; |
3 | export { PagesStore } from './pages.state.ts'; |
4 | ... |
1 | // store/state/app.state.ts |
2 | const initialAppState: AppState = { |
3 | books: BooksStore.initialState, |
4 | pages: PagesStore.initialState, |
5 | ... |
6 | } |
1 | // store/reducers/books.reducer.ts |
2 | const { adapter } = BooksStore; |
3 | |
4 | const booksReducers = ( |
5 | state = BooksStore.initialState, |
6 | action: BooksActions, |
7 | ): BooksStore.State => { |
8 | ... |
9 | } |
Using the namespace setup allows for easy segmentation on what each item exported should do. It also allows for a shared API amongst other sub-stores so that the overhead of remembering what the state was named is less.