DEV Community

Cover image for State Management with NGRX - Introduction
Giancarlo Buomprisco
Giancarlo Buomprisco

Posted on • Originally published at frontend.consulting

State Management with NGRX - Introduction

State Management with NGRX — Introduction

In this article, I want to introduce you to the concepts that make up the NGRX platform and all its pieces to fully understand how it helps us create better applications.

NGRX, for those who don’t know yet, is a Redux library for Angular. It helps us with state management, which is arguably the hardest part to manage for modern, large scale, client-side applications.

Compared to Redux, which only provides the layer for the store, NGRX comes with quite a few powerful features.

Notice: the snippets in this article are not updated with the latest changes in NGRX 8.


This is the first part of a 4-part articles series that will cover:

  • Pillars of NGRX
  • How to architect our Store using reducers and entities
  • Managing and testing side effects
  • Connecting our UI to a Facade Service, with pros and cos

Why NGRX, or any state management solution

NGRX, and other state management libraries for Angular (NGXS, Akita) have become important pieces in the architecture of complex web apps.

An unpopular opinion of mine is that every Angular application should use some sort of state management, be it RX-powered services, MobX, or different Redux implementations.

I found several pitfalls with large (and even small) projects using local state within components, such as:

  • difficulty in passing data between routes
  • difficulty in caching data already fetched
  • repeated logic and requests
  • no conventions

This list could be much longer, but this is enough to convince me that some sort of state management is essential for not refactoring a new application any time soon.

NGRX Pillars

Let’s see how the platform around NGRX is structured:

  • store — which a central repository from which we can select the state from every component and service within the Angular DI
  • effects — which as the name suggests are side effects that happen when an action is dispatched
  • entity — an entity framework to help reduce the usual boilerplate

Let’s now take a more detailed look at all the concepts we’re going to explore in the next steps.

Store

A store is a central repository where we store our data.

It’s our application database and the only source of truth in our client. Technically speaking, it’s simply a nested object that we use to select and store data.

As the Store service is accessible via the Angular DI, the data in our store is accessible by all components and services from everywhere in our application.

Any information regarding state, unless isolated to one single part of the application (ex. forms, popups, transient state), should probably be stored in the store.

Actions

In NGRX-speak — actions are classes that hold information that gets passed to reducers or that trigger side effects.

Actions have two parameters:

  • a unique identifier we name type (make sure you mark it as readonly)
  • an optional payload property that represents the data being passed to the action
export enum LoginActionTypes {  
    LoginButtonClicked = '[Login Button] LOGIN_BUTTON_CLICKED',  
    LoginRequestStarted = '[Login API] LOGIN_REQUEST_STARTED'  
}

export class LoginButtonClicked {  
   public readonly type = LoginActionTypes.LoginButtonClicked;

   constructor(public payload: LoginRequestPayload) {}  
}

export class LoginRequestStarted {  
   public readonly type = LoginActionTypes.LoginRequestStarted;

   constructor(public payload: LoginRequestPayload) {}  
}

export type UserActions = LoginButtonClicked | LoginRequestStarted;
Enter fullscreen mode Exit fullscreen mode

Conventions for naming the type parameter:

  • you will usually see the type being written using the format [prefix] NAME
  • the prefix is useful to declare where the request is originated from, as recommended by the NGRX team

💡Pro Tip: write many granular actions and always write what originated them. It doesn’t really matter if you rewrite some action that does the same thing. 

Reducers

Reducers are simply pure functions responsible for updating the state object in an immutable manner. 

A reducer is a function that receives two parameters: the current state object and an action class, and returns the new state as output. 

The new state is always a newly built object, and we never mutate the state.

export function loginReducer(  
    state: UserState = {},  
    action: UserActions  
): UserState {  
    switch (action.type) {  
        case UserActionTypes.LoginSuccess:  
            return action.payload;  
        default:  
            return state;  
}
Enter fullscreen mode Exit fullscreen mode

This is a dead-simple reducer that simply returns the current state if no action is matched, or returns the action’s payload as the next state. In a real application, your reducers will end up being much bigger than the example. 

There are loads of libraries around to simplify using reducers, but to me, they are rarely worth using.

For more complex reducers, I’d recommend to create functions and keep the reducer functions simple and small.

In fact, we can refactor the switch statement by simply using an object and match the action type with the object’s keys.

Let’s rewrite that:

interface LoginReducerActions {   
    [key: UserActionTypes]: (  
        state: UserState,   
        action: UserActions  
    ): UserState;  
};

const loginReducerActions: LoginReducerActions = {   
    [UserActionTypes.LoginSuccess]: (  
       state: UserState,   
       action: LoginSuccess  
    ) => action.payload  
};

export function loginReducer(  
    state: UserState = {},  
    action: UserActions  
): UserState {  
    if (loginReducerActions.hasOwnProperty(action.type)) {  
       return loginReducerActions[action.type](state, action);  
    }

    return state;  
}
Enter fullscreen mode Exit fullscreen mode

Selectors

Selectors are simply functions we define to select information from the store’s object. 

Before we can introduce selectors, let’s see how we would normally select data from the store within a service:

 

interface DashboardState {  
   widgets: Widget[];  
}

export class DashboardRepository {  
    widgets$ = this.store.select((state: DashboardState) => {  
        return state.widgets;  
    });

    constructor(private store: Store<DashboardState>) {}  
}
Enter fullscreen mode Exit fullscreen mode

Why is this approach not ideal?

  • it’s not DRY
  • if the store’s structure will change (and believe me, it will), we need to change the selection everywhere
  • the service itself knows about the structure of the store
  • No caching 

Let’s introduce the utility provided by @ngrx/store called createSelector which was inspired by the React library reselect

For simplicity, I will keep the snippets unified, but you should assume that selectors are created in a separate file and they get exported.

// selectors  
import { createSelector, createFeatureSelector } from '@ngrx/store';

const selectDashboardState = createFeatureSelector('dashboard');

export const selectAllWidgets = createSelector(  
    selectDashboardState,   
    (state: DashboardState) => state.widgets  
);

// service  
export class DashboardRepository {  
    widgets$ = this.store.select(selectAllWidgets);

    constructor(private store: Store<DashboardState>) {}  
}
Enter fullscreen mode Exit fullscreen mode

💡Pro Tip: selectors are super useful, always write granular selectors and try to encapsulate logic within selectors rather than in your services or components

Entities

Entities are added by the package @ngrx/entity .

As you may have seen if you ever used Redux, the boilerplate for common CRUD operations is time-consuming and redundant.

NGRX Entity helps us by providing out of the box a set of common operations and selectors that help to reduce the size of our reducers.

What does our state look like by using this Entity framework?

interface EntityState<V> {    
  ids: string[] | number[];     
  entities: {   
      [id: string | id: number]: V   
  };
}
Enter fullscreen mode Exit fullscreen mode

I normally start by creating an adapter, in a separate file, so we can import it from different files such as the reducer and the selectors’ files.

export const adapter: EntityAdapter = createEntityAdapter();

Let’s use the adapter in our reducer. How does the adapter interact with it?

  • it creates an initial state (see the interface EntityState above)
  • it gives us a series of CRUD operations methods to write reducers on the fly
const initialState: DashboardState = adapter.getInitialState();

export const dashboardReducer(  
    state = initialState,  
    action: DashboardActions  
): DashboardState {  
   switch (action.type) {  
       case DashbordActionTypes.AddWidget:  
          const widget: Widget = action.payload;    
          return adapter.addOne(action.payload, state);  
   }

   // more ...   
   }
}
Enter fullscreen mode Exit fullscreen mode

💡 Pro Tip: Take a look at all the methods available in NGRX Entity

The entity adapter also allows us to kickstart a collection of selectors to query the store.

Here’s an example for selecting all the widgets in our dashboard’s state:

const { selectAll } = adapter.getSelectors();

export const selectDashboardState = createFeatureSelector<DashboardState>('dashboard');

export const selectAllWidgets = createSelector(  
  selectDashboardState,  
  selectAll  
);
Enter fullscreen mode Exit fullscreen mode

💡 Pro Tip: Take a look at all the selectors available in NGRX Entity

Effects

Finally, my number one favorite feature in NGRX: Effects.

As the name suggests, we use effects to manage side-effects in our application. NGRX implements effects as streams emitted by actions, that in most cases return new actions.

Let’s consider the diagram below:

  • an action gets dispatched from somewhere in the application (ex: UI, WebSocket, Timers, etc.)
  • the effects intercept the action, for which a side-effect has been defined. The side-effect gets executed
  • The side effect, with exceptions, returns a new action
  • The action goes through a reducer and updates the store

As I mentioned, not all side effects will return a new action. We can configure an effect not to dispatch any action if it is not needed, but it’s important that you understand that in most cases we do want to dispatch new actions as a result. 

The most practical use-case for effects in NGRX is making HTTP requests:

export class WidgetsEffects {  
    constructor(  
        private actions$: Actions,  
        private api: WidgetApiService  
    ) {}

    @Effect()  
    createWidget$: Observable<AddWidgetAction> =   
        this.actions$.pipe(  
            ofType(WidgetsActionTypes.CreateWidgetRequestStarted),  
            mergeMap((action: CreateWidgetAction) => {  
                return this.api.createWidget(action.payload);   
            }),  
            map((widget: Widget) => new AddWidgetAction(widget))  
         );

    @Effect({ dispatch: false })  
    exportWidgets$: Observable<void> =   
        this.actions$.pipe(  
            ofType(WidgetsActionTypes.ExportWidgets),  
            tap((action: ExportWidgets) => {  
                return this.api.exportWidgets();   
            }),  
         );  
}
Enter fullscreen mode Exit fullscreen mode

Let’s break the above snippet down.

  • we created a class called WidgetsEffects
  • we import two providers: Actions and WidgetsApiService
  • Actions is a stream of actions. We use the operator ofType that helps us filter the actions to only the one we want to listen to
  • We create a property on the class and decorate it with Effect
  • This effect is called when an action called CREATE_WIDGET_REQUEST is dispatched
  • We get the payload from the action and execute a call with our API service
  • Once that has successfully been executed, we map it the action AddWidgetAction which can be picked up by the reducer and update our store
  • In the second effect called exportWidgets$ , we receive an action ExportWidgets , we use the tap operator to execute a side-effect, and then… well, nothing! As we passed the configuration { dispatch: false } we don’t have to return any action

Takeaways

  • A state-management solution, be it a library or your own implementation, should always be preferred to local state 
  • A single source of truth such as the store helps us managing the state of our application, with a few exceptions such as when the state is transient
  • we briefly explored the concept of store, actions, reducers, entities, selectors, and effects, but in the next steps we will go into details into each of these with some more advanced examples

In the next article, we're going to the store of the application and the state’s entities.


If you need any clarifications, or if you think something is unclear or wrong, do please leave a comment!

I hope you enjoyed this article! If you did, follow me on *Medium* or *Twitter for more articles about the FrontEnd, Angular, RxJS, Typescript and more!*

Top comments (4)

Collapse
 
ky1e_s profile image
Kyle Stephens

Nice tutorial. Much more detailed than many NgRx tutorials I have come across.

One point to note:

A state-management solution, be it a library or your own implementation, should always be preferred to local state.

I would hesitate to be so prescriptive.

Dan Abramov, creator of Redux, notes that you might not need Redux for every application - You Might Not Need Redux

Quote:

Redux offers a tradeoff. It asks you to:

  • Describe application state as plain objects and arrays.
  • Describe changes in the system as plain objects.
  • Describe the logic for handling changes as pure functions.

None of these limitations are required to build an app, with or without React. In fact these are pretty strong constraints, and you should think carefully before adopting them even in parts of your app.

Collapse
 
gc_psk profile image
Giancarlo Buomprisco

Hi, thanks a lot for your comment!

I totally agree with you, my statement should clarify better what I meant.

There are some parts where I found global state to be way overkill for what I was trying to do, for example: transient state and forms.

Notice also: I don't think Redux is the answer to everything, nor the sentence fully states such a thing. I just think that every application should try to adhere to a form of state management and stick to it.

If Redux is taken out of the equation, I think using services powered by Rx with Subjects would be a very good idea.

Collapse
 
cadams profile image
Chad Adams

Imo, Ngrx is overly complicated and has too much boilerplate. Both github.com/ngxs/store and github.com/datorama/akita do a much better job.

Collapse
 
gc_psk profile image
Giancarlo Buomprisco • Edited

I haven't had the chance to use them yet, but I think they're all quite valid options. I agree NGRX has some boilerplate, but the new version improved that quite a bit.