People working on laptops

Angular + Redux Style Guide. Part 2.

by Nikita Verkhoshintcev

This post will be a continuation of a large-scale Angular application style guide. Previously, I was talking about the best practices of structuring a project for better maintainability.

Now, I would like to reveal the practices for structuring the Redux store. The Redux is very important in large projects with a very complex state. It helps to have a single and predictable source of truth in your app which is easier to maintain.

In this article, I would like to show two approaches for Redux folder structure. Official NGRX Store documentation suggests one, and I’m using a slightly different way.

Noteworthy, this is a post about style guide, not the technology itself. I won’t be touching representational components, feature state, lazy loading, etc.

Let’s get started!

Single Source of Truth

As you, probably, already know, we use Redux to have a single source of the application state. It means, also referring to other style guides, that we want to control everything related to our store in one place. I suggest using Store directory in the root of your project.

We will keep all our action creators, reducers, and selectors inside this directory.

Before we start with the creation of a state, let’s take a look at actions and reducers.

First of all, since we are using TypeScript for the Angular, I highly recommend using strictly typed actions as well.

How are we going to structure them?

There are two favorite ways of doing it. You can keep all the actions and reducers grouped into the corresponding folders. This approach will be consistent with the rest of the app because we have all our containers, components, services, pipes, directives and guards grouped in such way.

I’m also a fan of using the Index file for exporting to keep my imports clean. I already mentioned that in the previous article.

So, here is a sample

Personally, I prefer another approach.

I group everything in directories based on a state property. So, each of the parts of the application state has its folder with the corresponding actions and reducers.

I find this way more logical and easier to maintain. We can work on any feature in isolation which is very handy. Here is an example of this structure.

Action Creators and Reducers

Let’s start with the actions and reducers.

You can dispatch an action by passing a hardcoded object with a type and optional payload. It would work. You can even export a constant string as a type so that you could control it in one place.

But is it handy? Can you manage the payload type check anyhow?

There is a way. I recommend using the action creators with strict types.

Let’s create sample companies actions.

import { Action } from '@ngrx/store'
//  The good practice is to keep your interfaces inside the models directory.
//  import { Company } from '../../models/company.model'
export interface Company {
  id: number
  name: string
}
 
export const ADD_COMPANY = '[Companies] Add Company'
export const REMOVE_COMPANY = '[Companies] Remove Company'
 
export class AddCompany implements Action {
  readonly type = ADD_COMPANY
 
  constructor(public payload: Company) {}
}
 
export class RemoveCompany implements Action {
  readonly type = REMOVE_COMPANY
 
  constructor(public payload: Company) {}
}
 
export type All =
  AddCompany |
  RemoveCompany

We have just created two actions which accept only objects of a Company type as a payload.

Notice, it’s essential to keep the right structure of action types. The best practice is to have a prefix first and then the verb with the following noun.

Let’s check how can you use it in the reducer function.

Another note, to keep the import statement clear, instead of importing all the constants and classes separately, we want to send them from the actions files as a single object.

There are two recommended options. In NGRX documentation they are using “from” word in the name of the object. So, in our case, it would be “fromCompanies.” If you would like to import anything as an object from the store directory, you should use “fromRoot” with this naming convention.

I, personally, like another approach. I’m using the more descriptive name such as “companiesActions”.

So, first of all, we need to import our actions and then use them in the reducer function.

import * as companiesActions from './companies.actions' 
import { Company } from '../../models/company.model'
 
export function companiesReducer(state: Company[], action: companiesActions.All) {
  switch(action.type) {
    case companiesActions.ADD_COMPANY:
      return [...state, action.payload];
    case companiesActions.REMOVE_COMPANY:
      return state.filter(company => company.id !== action.payload.id)
    default:
      return state;
  }
}

Once the actions and reducers are ready, we can export them in the Index file.

export * from './companies.actions';
export * from './companies.reducers';

App State and Selectors

Let’s get back to the application state itself and talk about action reducer map and selectors.

We will add everything related to the state in the store root folder in Index file. We have to do three things, define an app state interface, map the action reducers, and create selectors.

import { Company, User } from '../models';
import { ActionReducerMap } from '@ngrx/store';
import { companiesReducer } from './companies';
import { userReducer } from './user';
 
export interface AppState {
  companies: Company[]
  user: User
}
 
export const reducers: ActionReducerMap = {
  companies: companiesReducer,
  user: userReducer
}
 
export const getCompanies = (state: AppState) => state.companies
export const getUser = (state: AppState) => state.user

Here is a remarkable thing concerning selectors. Though you can just use a string value inside the select method, e.g., this.store.select(‘companies’) to return an Observable, I recommend using pre-defined selectors.

It is especially important if you are dealing with the store in features module.

For instance, here is an example of the official documentation.

export const selectFeature = createFeatureSelector('feature');
export const selectFeatureCount = createSelector(selectFeature, (state: FeatureState) => state.counter);

And finally, here is an example of how to use an action reducer map inside the module and how to use selectors inside the containers.

// app.module.ts
...
import { StoreModule } from '@ngrx/store';
import { reducers } from './store';
 
@NgModule({
  ...
  imports: [
    ...
    StoreModule.forRoot(reducers),
  ]
})

// companies.container.ts
...
import { Store } from '@ngrx/store';
import { AppState, getCompanies } from '../store';
import { Observable } from 'rxjs/Observable';
import { Company } from '../models';

@Component(...)
export class CompaniesContainer {
  companies$: Observable<Company[]>;

  constructor(private store: Store) {
    this.companies$ = store.select(getCompanies);
  }
}

To differentiate observables from the standard variables, I suggest marking them with the dollar sign in the end. This way you would instantly know that you’re dealing with the observable and it will prevent you from forgetting an async pipe in the template.

Conclusion

In this article, I shared the best practices for structuring the Redux store project which I use myself.

Along with the recommendations from the first article, you should be able to structure the large-scale Angular application for the best development performance. Eventually, it will help you save time on the code maintenance and prevent you from unnecessary mistakes.

Hope that this style guide would help you to implement a system within your team so that everybody would be on the same track.

Learn and implement best practices, follow best style guides, plan out the project beforehand for the better scalability to become the best developer! Happy coding!

Nikita Verkhoshintcev photo

Nikita Verkhoshintcev

Salesforce Consultant

Senior Salesforce and full-stack web developer. I design and build modern Salesforce, React, and Angular applications for enterprises. Usually, companies hire me when complex implementation, custom development and UI is required.

Let's work together!

Contact us today and take your digital end-user experience to the next level.

Contact us