DEV Community

Cover image for Architecting the Store in NGRX
Giancarlo Buomprisco
Giancarlo Buomprisco

Posted on • Originally published at frontend.consulting

Architecting the Store in NGRX

This is the second article of a series that aims to explain in detail a step-by-step approach to building an Angular application with NGRX.

In the first article of this series, I wrote a small overview of all the concepts surrounding the NGRX platform.

If you have never worked with NGRX, or have never done something in-depth with, I’d really recommend you read it.

NGRX 8

In the previous article, the concepts were explained using the current NGRX version. In order to keep the articles up to date, from now on I will introduce and explain the same concepts using the latest features released in NGRX version 8. There’s some really cool stuff out!

In particular, we will look at how to create:

  • actions with createAction
  • reducers with createReducer
  • effects with createEffect (in the next article)

What is this article about?

In this article instead, we will explore the process of building the entities that make up our store and will be setting up the entity adapter, the actions, and the reducers for each entity.

As a follow up to one of my previous articles about creating a scalable folders structure, we will see an example of creating store modules as service modules imported by our domain module.

We will build an application that retrieves live crypto prices from Coincap** and displays them in a customizable dashboard.

We will call this demo application Cryptofolio, which I hope to publish at the end of this series.

** I explored various websites for fetching live prices and Coincap was by far the easiest and clearer provider. Kudos to the team!

Setting up Angular and NGRX

Let’s see how to set up an Angular application and NGRX.

Angular CLI Workspace

The first thing you may want to do is to create a new application with Angular CLI and add the routing and style parameters.

ng new <app> --routing --style=scss

NGRX

Let’s install all the libraries needed to work with NGRX:

npm i @ngrx/store @ngrx/effects @ngrx/entity

And you’re pretty much all set!

Project’s Folder Structure

Let’s take a brief look at the project structure I opted for:

  • What’s in store? Every folder in store is an Angular Service Module that simply sets up the NGRX store and effects for the Dashboard Module, which is a domain module where our application’s smart components are placed.

Let’s take a look at the DashboardStoreModule which is still very simple:

@NgModule({  
    imports: [  
        StoreModule.forFeature('dashboard', dashboardReducer),  
        // will import effects  
    ],  
    providers: [  
       // will import providers  
    ]  
})  
export class DashboardStoreModule {}
Enter fullscreen mode Exit fullscreen mode

The DashboardModule will then import DashboardStoreModule and the other store modules:

@NgModule({  
    declarations: [  
       // components  
    ],  
    imports: [  
        // store service modules_ DashboardStoreModule,  
        PricesStoreModule,  
        AssetsStoreModule,

        // other modules
    ],  
    exports: [RouterModule]  
})  
export class DashboardModule {}
Enter fullscreen mode Exit fullscreen mode
  • Where is DashboardModule imported? The DashboardModule is a lazy-loaded module, so we do not import it from anywhere in our application, but instead, we reference it in our routing module configuration.

In order to make lazy-loaded feature modules work with NGRX, we need to call the forRoot method, although with empty values, for both the StoreModule and the EffectsModule.

@NgModule({  
    declarations: [AppComponent],  
    imports: [  
        // other modules,  
        StoreModule.forRoot({}, { metaReducers }),  
        EffectsModule.forRoot([]),  
    ],  
    bootstrap: [AppComponent]  
})  
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Store Entities

In order to architect the store, we need to first analyze our data structures.

As I mentioned above, the application will feature a dashboard with tiles, and each widget will contain cryptocurrency price tickers. In order to retrieve to display the prices, we first need to load the assets (cryptocurrencies).

We then have 4 different entities that we will use to build our fairly simple store:

  • a dashboard that contains tiles (or widgets)
  • a list of assets (cryptocurrencies)
  • a price (for each asset subscribed)

Flat vs Nested Store

We have two ways of building the store:

  • a nested structure, by directly adding prices to the assets store
  • a flat structure, where assets and prices are separated into two separate objects and are only related based on the asset ID

I personally prefer a flat structure. 

Why? I have, mistakenly, opted for nested structures in the past and I found the following issues:

  • by adding a price directly to an asset, we’d be changing the original interface of the entity
  • deeper, nested structures are more difficult to query

In this simple example, it doesn’t really affect greatly performance or complexity. However, if you plan on building a big application with a complex state, you will quickly see how the selectors and the store complexity creeping up as a result of a nested structure.

My advice is to keep the store as a flat structure of objects and keep the relations between them using unique values.

Dashboard Store

For simplicity, we will keep the dashboard fairly minimal. We only need two things from a widget:

  • a tile ID
  • an asset ID

In order to build this part of the store, we will use @ngrx/entity .

Tile class

Let’s first create a class named Tile that represents the model of our state:

export class Tile {  
    public readonly id = uuid();

    constructor(public assetId?: string) {}  
}
Enter fullscreen mode Exit fullscreen mode

Of course, unless a tile is preloaded with an asset ID, the asset ID won’t be defined until the user decides which asset to display, which is why we mark as possibly undefined.

Dashboard Adapter

We move on and proceed to create the adapter for our state. Our state will simply be an entity state with a collection of tiles:

export const dashboardAdapter: EntityAdapter<Tile> = createEntityAdapter<  
    Tile  
>();
Enter fullscreen mode Exit fullscreen mode

Dashboard Actions

In order to create our actions, we will be using the new factory provided by NGRX 8 called createAction .

export enum DashboardActionTypes {  
    AddTile = '[Dashboard] ADD_TILE',  
    RemoveTile = '[Dashboard] REMOVE_TILE',  
    UpdateTile = '[Dashboard] UPDATE_TILE'  
}

export const addTile = createAction(  
    DashboardActionTypes.AddTile, // action name  
    props<{ payload: Tile }>() // action payload type  
);

export const removeTile = createAction(  
    DashboardActionTypes.RemoveTile,  
    props<{ payload: string }>()  
);

export const updateTile = createAction(  
    DashboardActionTypes.UpdateTile,  
    props<{ payload: Tile }>()  
);
Enter fullscreen mode Exit fullscreen mode

To summarise the code, we have created 3 actions:

  • addTile whose payload is a Tile class
  • removeTile which only receives a string as payload, which is the ID
  • updateTile which also receives a Tile class

Notice that props is a function that gets imported from @ngrx/store and gets called as a second argument.

Dashboard Reducer

In order to build the dashboard reducer, we will use the new factory method called createReducer that takes the following arguments:

  • the first argument is the initial state, that we created using the entity adapter
  • all the following arguments are the reducer functions for each action, that we define using the function on also imported from @ngrx/store 
  • we use the entity adapter methods in order to add, remove and update the dashboard’s tiles
// we create the state by adding an empty tile_

const emptyTile = new Tile(undefined);  
const initialState = dashboardAdapter.addOne(  
    emptyTile,  
    dashboardAdapter.getInitialState()  
);

export const dashboardReducerFn = createReducer(  
    initialState,  
    on(addTile, (state, { payload }) => {  
        return dashboardAdapter.addOne(payload, state);  
    }),  
    on(removeTile, (state, { payload }) => {  
        return dashboardAdapter.removeOne(payload, state);  
    }),  
    on(updateTile, (state, { payload }: { payload: Tile }) => {  
        return dashboardAdapter.updateOne(  
            { id: payload.id, changes: { assetId: payload.assetId } },  
            state  
        );  
    })  
);

export function dashboardReducer(  
    state = initialState,  
    action: Action  
): EntityState<Tile> {  
    return dashboardReducerFn(state, action);  
}
Enter fullscreen mode Exit fullscreen mode

We import the reducer in the DashboardStoreModule :

@NgModule({  
    imports: [  
         StoreModule.forFeature('dashboard', dashboardReducer),  
    ]  
// more
Enter fullscreen mode Exit fullscreen mode

Assets Store

As we are going to receive the list of assets using Coincap’s API, we’re just going to replicate their interface:

export interface Asset {  
    id: string;  
    rank: string;  
    symbol: string;  
    name: string;  
    supply: string;  
    maxSupply: string | null;  
    marketCapUsd: string;  
    volumeUsd24Hr: string;  
    priceUsd: string;  
    changePercent24Hr: string;  
    vwap24Hr: string;  
}
Enter fullscreen mode Exit fullscreen mode

Assets Actions

In order to fetch the assets, we will need to perform an HTTP request to Coincap’s API. The HTTP action will be going through the effect method we’re going to define in the next article. 

What’s important to notice here is the way I’ve broken up the assets’ actions:

  • getAssetsRequestStarted: action that gets dispatched when the request starts
  • getAssetsRequestSuccess: action that gets dispatched when the request succeeded (no error actions in this case for simplicity, but you should always create them)
  • addAssets: action that will only be used by the reducer, which is a command to add assets to the store
export enum AssetsActionsTypes {  
    GetAssetsRequestStarted = '[Assets API] GET_ASSETS_REQUEST_STARTED',  
    GetAssetsRequestSuccess = '[Assets API] GET_ASSETS_REQUEST_SUCCESS',  
    AddAssets = '[Assets] ADD_ASSETS'  
}

export const getAssetsRequestStarted = createAction(  
    AssetsActionsTypes.GetAssetsRequestStarted,  
    props<{ payload: string[] }>()  
);

export const getAssetsRequestSuccess = createAction(  
    AssetsActionsTypes.GetAssetsRequestSuccess,  
    props<{ payload: Asset[] }>()  
);

export const addAssets = createAction(  
    AssetsActionsTypes.AddAssets,  
    props<{ payload: Asset[] }>()  
);
Enter fullscreen mode Exit fullscreen mode

Assets Reducer and Adapter

The only reducer function reacting to the addAssets action will simply add all the assets to the store once they get fetched.

// adapter   
export const assetsAdapter: EntityAdapter<Asset> = createEntityAdapter<Asset>({  
    selectId: (asset: Asset) => asset.id  
});

// reducer  
const initialState = assetsAdapter.getInitialState();

export const assetsReducerFn = createReducer(  
    initialState,  
    on(addAssets, (state, { payload }) => {  
        return assetsAdapter.addAll(payload, state);  
    })  
);

export function assetsReducer(  
    state: EntityState<Asset> | undefined,  
    action: Action  
) {  
    return assetsReducerFn(state, action);  
}
Enter fullscreen mode Exit fullscreen mode

Prices Store

The prices returned by Coincap’s API are very simple and are just objects with the key of an asset and its relative price. As such, we have a very simple store for prices.

Prices Actions

We will be creating 3 actions:

  • addPrice: action for updating the store once a price is received
  • createPriceSubscription: action for creating a subscription
  • closePriceSubscription: action for closing a subscription
export enum PricesActionsTypes {  
    AddPrice = '[Prices Store] ADD_PRICE',  
    CreatePriceSubscription = '[Prices Stream] CREATE_PRICE_SUBSCRIPTION',  
    ClosePriceSubscription = '[Prices Stream] CLOSE_PRICE_SUBSCRIPTION',  
    PriceReceived = '[Prices Stream] PRICE_RECEIVED'  
}

export const addPrice = createAction(  
    PricesActionsTypes.AddPrice,  
    props<{ payload: Price }>()  
);

export const createPriceSubscription = createAction(  
    PricesActionsTypes.CreatePriceSubscription,  
    props<{ payload: string }>()  
);

export const closePriceSubscription = createAction(  
    PricesActionsTypes.ClosePriceSubscription  
);

export const priceReceived = createAction(  
    PricesActionsTypes.PriceReceived,  
    props<{ payload: Price }>()  
);
Enter fullscreen mode Exit fullscreen mode

Prices Reducer

As the prices returned by Coincap’s real-time API are simply a key with the asset and its price, we really don’t need to do much with the entity framework.

Indeed, for each price received, we simply set the key with the asset ID in our store and its price by spreading the price objects with the new payload.

If it doesn’t exist, it gets created, otherwise, it gets overwritten with its newest value.

Imagine our state is:

{ "bitcoin": "some price" };
Enter fullscreen mode Exit fullscreen mode

And our payload from the WebSocket’s stream is:

{ "ethereum": "another price" }
Enter fullscreen mode Exit fullscreen mode

This will simply become:

{   
    "bitcoin": "some price",  
    "ethereum": "another price"  
};
Enter fullscreen mode Exit fullscreen mode

And here is the code with one simple action:

const initialState: PriceState = {};

export const pricesReducerFn = createReducer(  
    initialState,  
    on(addPrice, (state, { payload }) => {  
        return { ...state, ...payload };  
    })  
);

export function pricesReducer(  
    state = initialState,  
    action: Action  
): PriceState {  
    return pricesReducerFn(state, action);  
}
Enter fullscreen mode Exit fullscreen mode

An overview of the Store

Let’s take a look at the store with some data:

Type caption for image (optional)

  • We have fetched 5 assets
  • We have one, empty tile
  • We have no prices, as the tile has not been subscribed to an asset

Takeaways

  • Lay out your application entities and analyze how they relate between each other in order to have a clear understanding of what the store’s structure could look like
  • Use NGRX Entity! It’s a great tool to reduce the boilerplate of your reducers
  • Separate your UI modules from the store using Store Service Modules
  • Prefer a flat structure over a nested one
  • Keep actions clear and granular, distinguish between commands and events

In the next article, we're going to build the effects that are responsible for fetching assets and prices from Coincap’s API.


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 (1)

Collapse
 
sebastiandg7 profile image
Sebastián Duque G

Thanks a lot for the article! Just a question:

It is certainly recommended to have your client application entities in a /models folder. When defining your state entities, and taking into a account the flat store structure over the nested one (which implies the other entities ids references in your entities attributes), what do you recommend to use?

  • The feature module entities previously defined? And designing them taking into account the required data to work with a flat store architecture (with @ngrx/entity).
  • Creating a new MyEntityStoreModel extending MyEntityModel which comes from the feature model? So you can add any necessary properties.
  • Creating a totally different entity for the store (and some mappers)?

Again... thanks a lot! Currently enjoying reading all your articles.