NGRX Best Practices

In my last article we glossed over things that one should probably avoid doing when building an Angular application with NgRx; today, we are going to discuss things that are good to do when facing that very same challenge.

NGRX Best Practices

In my last article we glossed over things that one should probably avoid doing when building an Angular application with NgRx; today, we are going to discuss things that are good to do when facing that very same challenge. The ideas presented in this article are not really that required, but will make the whole experience working on NgRx codebase way simpler and manageable. So, let's get started on the things you probably want to do:

Neat folder structure

First and foremost, the most important advice in this article. NgRx changes how we write, structure and maintain our Angular app, especially if we adopt it thoroughly. So, one of our first steps towards a better codebase is structuring our application in a concise, consistent manner. There are several things we can do about it.

Use Feature stores

A more or less complex Angular app will have several lazy loaded modules, and usually those modules have access to some state that is only specific to components inside it. So, dumping the entire initial state into the root store is not exactly the greatest idea, so it is better to also add feature states only when the respective modules get lazy loaded.

@NgModule({
  StoreModule.forRoot({root: rootReducer}),
  EffectsModule.forRoot([RootEffects]),
})
export class AppModule {}

And in a lazy loaded module:

@NgModule({
  StoreModule.forFeature({user: userReducer}),
  EffectsModule.forFeature([UserEffects]),
})
export class UserModule {}

Structure every feature state in a consistent manner

My suggestion for this is to have a state folder in every lazy loaded module (that has its own state), which will contain five separate files:

  1. state.ts (contains the type definition of the store and initialState)
  2. reducers.ts (contains the reducers)
  3. actions.ts (contains the actions)
  4. selectors.ts (contains named selectors);
  5. effects.ts (contains the effects class)

Also, if you use @ngrx/entity, you can have a file named "adapter.ts" to contain the adapters.

Better file naming

While in the previous section  I used names like "state.ts" or "selectors.ts", but I don't really suggest you use generic names like this. It is better to prefix these filenames with the name of the feature they belong to: "articles.reducers.ts" or "bookmarks.effects.ts" and so on. This way, you will not end up having dozens of files with the same name in your app, and those files can then be easily found by file name search (if not prefixed, you might search for "actions.ts", get 7 file results, and then start looking at the folder names to understand which is the one you actually need)

Keep a string enum of your feature names

You might have several feature states, each with its own unique name. I suggest you put those in an enum to be able to easily change those names if it becomes needed at some point. Here is an example of such an enum:

export enum Features {
  User = 'user',
  Article = 'article',
  Bookmark = 'bookmark'
}

Then, in, for example, user.module.ts you will have this:

@NgModule({
  StoreModule.forFeature({[Feature.User]: userReducer}),
  EffectsModule.forFeature([UserEffects]),
})
export class UserModule {}

And in bookmarks.selector.ts you can easily take the feature state by:

const bookmarks = createFeatureSelector(Features.Bookmark);

Use named selectors

This is probably the simplest advice. Take a look at this code:

export class MyComponent {
    noUsers$ = this.store.select(state => state.user.userList.items.length === 0);

    constructor(
        private readonly store: Store,
    ) {}

}

Now this code is not exactly bad, but we can face some minor, but still irritating problems win the future. First of all, looking at the selector does not make it readily apparent as to what it does. We have to read the entire selector to understand, and if the logic is complex enough, that can be a bit tedious. Also, it is not really abstracted away. Of course, this is a very minor issue, and most apps will do well having code like this. But a more frustrating problem will arise if we suddenly need to combine this selector with another one using createSelector. If we need to do that, we will have to move this elector to become a named one. So why not start with it right away?

// selectors.ts
const noUsers = createSelector(userState, state => state.userList.items.length === 0);

// in component.ts
export class MyComponent {
    noUsers$ = this.store.select(noUsers);

    constructor(
        private readonly store: Store,
    ) {}

}

We now also get some nice upsides like having all our selectors in one place, and Memoization.

Write helper functions and custom operators

Sometimes, we need to perform custom operations/checks inside our effect pipelines. For example, we might check if a payload satisfies a certain condition, or maybe something in the store already exists or not, and so on. Sometimes, it is useful to create small custom operators/helper functions like this one mentioned by Serkan Sipahi in his tweet. Here is one example in code:

const truthy = <T extends unknown>(param = true) => filter<T>(value => !!value === param);

Another thing I like to do is create helper functions that help create multiple actions (of some similar category) with a single command. For example, if we use NgRx effects to do HTTP calls, we might create three actions to load data: loadData, loadDataSuccess and loadDataError to handle all scenarios. Here is how we do it:

const loadData = createAction(
  '[Some Component] Load Data',
  props<{payload: Params}>,
);

const loadDataSuccess = createAction(
  '[Some Component] Load Data Success',
  props<{payload: ResponseData}>,
);

const loadDataError= createAction(
  '[Some Component] Load Data Error',
  props<{payload: ResponseError}>,
);

Now we have created three actions. the only difference between which is their name's suffix (first has no suffix, second has  suffix "Success" and last has suffix "Error") and the type of its payload. We can write a function that can return a tuple (a triplet, to be precise) of such actions:

export function createHTTPActions<RequestPayload = void, ResponsePayload = void, ErrorPayload = ResponseError>(
  actionType: string,
): [
  ActionCreator<string, (props?: RequestPayload) => {
      payload: RequestPayload;
  } & TypedAction<string>>,
  ActionCreator<string, (props?: ResponsePayload) => {
      payload: ResponsePayload;
  } & TypedAction<string>>,
  ActionCreator<string, (props?: ErrorPayload) => {
      payload: ErrorPayload;
  } & TypedAction<string>>,
] {
  return [
    createAction(actionType, (payload: RequestPayload) => ({payload})),
    createAction(
      `${actionType} Success`,
      (payload?: ResponsePayload) => ({payload})),
    createAction(`${actionType} Error`, (payload: ErrorPayload) => ({payload})),
  ];
}

Now, on the surface this functions seems a bit complicated, but it can be used in a very neat manner to instantly create three actions for a single HTTP call:

const [ loadData, loadDataSucces, loadDataError ] = createHTTPActions<Parameters, ResponseData, ResponseError>('[Some Component] Load Data');

Now we instantly have three actions in just one line of code:

this.store.dispatch(loadData({/* the actual request data */}));

We can also give more thought to our actions and determine other cases where functions like this can help us reduce boilerplate

Take a look and @ngrx/entity and @ngrx/component-store

Reducers are probably the place where we have the most "boilerplatish" code in our apps. Take a look at this code:

// complex transformations of data in lists, like adding, deleting
// updating and filtering is done manually, in a tedious way
const articleReducer = createReducer(
  {articles: [], // some other state too maybe},
  on(fromArticles.addArticle, (state, {payload}) => ({
    ...state,
    articles: [...state.articles, payload],
  })),
  on(fromArticles.removeArticle, (state, {payload}) => ({
    ...state,
    articles: state.articles.filter(article => article.id !== payload.id),
  })),
  on(fromArticles.updateArticle, (state, {payload}) => {
    const article = state.articles.find(a => a.id === payload.id);
    // unpleasant logic to update and insert 
    // the updated article back into the array of articles
  }),
  on(fromArticles.addManyArticles, (state, {payload}) => ({
    ...state,
    articles: [...state.articles, ...payload],
  })),
  // probably more similar code to set, setMany, updateMany and so on
);

Now again, this is not exactly bad code, but if we have many lists of entities, we will keep writing such repeating code a lot. And we might also have other problems to solve, like sorting, performance, memoization, and so on. To fix issues like this, we can utilize @ngrx/entity, a tool by NgRx meant to make working with lists of data easy and even fun. Same reducer written using @ngrx/entity:

// adapter helper functions will be used instead of performing 
// data transforming logic manually
const articleAdapter = createAdapter<Article>();

const articleReducer = createReducer(
  {articles: articleAdapter.genIntiialState(), // some other state too maybe},
  on(fromArticles.addArticle, (state, {payload}) => {
    ...state,
    articles: articleAdapter.addOne(payload, state.articles),
  }),
  on(fromArticles.removeArticle, (state, {payload}) => {
    ...state,
    // removing an object from a list is now just a one-liner
    articles: articleAdapter.removeOne(payload, state.articles),
  }),
  on(fromArticles.updateArticle, (state, {payload}) => {
    ...state,
    // updating is also very easy
    articles: articleAdapter.update(payload, state.articles),
  }),
  on(fromArticles.addManyArticles, (state, {payload}) => {
    ...state,
    articles: articleAdapter.addMany(payload, state.articles),
  }),
  // more  simple one-liners for the entire logic
);

Now this does, indeed, make our code better. On top of that, instead of storing lists as Arrays, @ngrx/entity uses structures similar to JavaScript Maps, which makes accessing and updating entities way more optimized. It also provides us with ready memoized selectors.

Now, not every state in a form of a list needs @ngrx/entity.  If you only ever store a list of some data entities and select it to show in a UI, you probably don't need @ngrx/entity. But if you update the list, filter it, sort it and so on, you will definitely benefit from using it. And anyway, knowing @ngrx/entity will be very beneficial for anyone building an NgRx app. You can read more about @ngrx/entity in this and this articles.

Another nice recent addition to NgRx is the @ngrx/component-store, a simple class we can use to have reactive state management inside our components. It can also be used to share state between components, and can be also used instead of a large NgRx store in applications that are not so very large. You can read more about @ngrx/component-store in this and this articles.

Use a linter + rules

As I mentioned, in my last article we discussed different bad practices that one should avoid. It is in no way a comprehensive list, and, on the other hand, it would be tedious to try to enforce all those rules by hand. Thankfully, there are tools online that contain linting rules for NgRx, a rather useful one is eslint-plugin-ngrx, created by Tim Deschryver and maintained by the community. It contains some very useful rules that help us avoid combining selectors with combineLatest, dispatching multiple actions sequentially, dispatching from effects, and many more.

(I have contributed to eslint-plugin-ngrx by adding rule descriptions, but those are still incomplete; some rules miss descriptions. You can help by contributing to this effort!)

Conclusion

This best practices list is of course not everything we can do to make our NgRx apps better, but avoiding bad practices and using advice from this article, we will be able to build more easily scalable, maintainable, and simply nicer to work with applications.