Ngrx Entities - Simplify Your Ngrx State
Ngrx Entities - Simplify Your Ngrx State

In this post you will learn what is NgRx entity and how you can reduce the amount of boilerplate when you are using NgRx.

So this post is for you if you are already familiar with NgRx and you understand concepts like reducers, selectors, effects and now you are curious what is ngrx-entity and how you can use it to simplify your code.

NgRX website

Here is the library which is called ngrx/entity and this is an entity state adapter for managing record collections. This line for me doesn't bring a lot of clarity. But for you to understand it correctly, this is not some new stuff inside NgRx, these are just some helper methods that allow us to work with collections more efficiently. It won't change your patterns of actions, effects and reducers. It just simplifies them a bit.

Installation

So our first step here is to install this package.

npm install @ngrx/entity

Additionally I already prepared for us a project with standard stuff like ngrx/store and ngrx/effects.

Initial project

This is how our initial project looks like. Here we have a list of posts that we fetch from the API. We can also create new posts. They will stay there even after page reload because we make an API call to save them on the backend.

Also inside Redux Devtools you can see our actions to get and create posts.

State improvement

By now our project is build with plain NgRx. We have a reducer, 2 effects for getting a list and creating a post and actions to make these changes.

Now we want to apply to our code ngrx-entity library. Our first step is to update our PostsStateInterface.

// before
export interface PostsStateInterface {
  posts: PostInterface[];
  isLoading: boolean;
  error: string | null;
}

// after
export interface PostsStateInterface extends EntityState<PostInterface> {
  isLoading: boolean;
  error: string | null;
}

We removed storing of our posts completely as now EntityState will store them and it uses such format.

{
  "ids": ["1", "2"],
  "entities": {"1": {"name": "User 1"}, "2": {"name": "User 2"}}
}

So it stores a list of ids and an object with entities.

Now inside our reducer we want to create an adapter and use methods to simplify our code.

// before
export const initialState: PostsStateInterface = {
  posts: [],
  isLoading: false,
  error: null,
};

export const reducers = createReducer(
  initialState,
  on(PostsActions.getPosts, (state) => ({ ...state, isLoading: true })),
  on(PostsActions.getPostsSuccess, (state, action) => {
    return {...state, isLoading: false, posts: action.posts}
  }),
  on(PostsActions.getPostsFailure, (state, action) => ({
    ...state,
    isLoading: false,
    error: action.error,
  })),
  on(PostsActions.createPostSuccess, (state, action) => {
    return {...state, isLoading: false, posts: [...state.posts, action.post]}
  })
);

// after
export const adapter: EntityAdapter<PostInterface> = createEntityAdapter<PostInterface>();

export const initialState: PostsStateInterface = adapter.getInitialState({
  isLoading: false,
  error: null,
});

export const reducers = createReducer(
  initialState,
  on(PostsActions.getPosts, (state) => ({ ...state, isLoading: true })),
  on(PostsActions.getPostsSuccess, (state, action) => {
    return adapter.addMany(action.posts, { ...state, isLoading: false });
  }),
  on(PostsActions.getPostsFailure, (state, action) => ({
    ...state,
    isLoading: false,
    error: action.error,
  })),
  on(PostsActions.createPostSuccess, (state, action) => {
    return adapter.addOne(action.post, { ...state, isLoading: false });
  })
);

Here we did several things. First of all we created an adapter by using createEntityAdapter function. Then we created initialState by using adapter.getInitialState. After this we updated getPostsSuccess and createPostSuccess to add new elements to the list of posts. For this we used adapter.addMany and adapter.addOne methods.

The last thing what we need to change is our selector to get a list of posts.

// before
export const postsSelector = createSelector(
  selectFeature,
  state => state.posts
)

// after
export const postsSelector = createSelector(
  selectFeature,
  adapter.getSelectors().selectAll
)

Here we used adapter.getSelectors which allows us to select either all elements as an array or a list of ids if we need it.

As you can see after all our changes the projects works the same but we used ngrx-entity here.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done