Make NgRx hold Business Logic: Dumb components, Smart store

Post Editor

Our team “moved everything” into NgRx. Follow our journey and learn why the whole team is still happy with it.

8 min read
0 comments
post

Make NgRx hold Business Logic: Dumb components, Smart store

Our team “moved everything” into NgRx. Follow our journey and learn why the whole team is still happy with it.

post
post
8 min read
0 comments
0 comments

State management is now essential for front-end apps. It came from React community with Redux. With Angular, you can also use Redux for state management but NgRx is considered as the go-to for Angular.

NgRx Store provides reactive state management for Angular apps inspired by Redux — NgRx website

Six months ago, our team made the choice to “move everything” into NgRx. It means at least every HTTP call response must be in the store. This isn't recommended but we did it anyway.

After developing new features all in NgRx, the whole team still 100% agree with this choice. In this article, you won’t learn what is state management or how NgRx works. This is focused on why we moved all our business logic to NgRx and why you should consider it.

A brand new project
Link to this section

Our team started a new project using Angular, a reporting app called Instant Insights. It displays a bunch of data through plots and tables. The user can apply many filters on the whole page such as fields to show and report period.

Every single component needs to access to those filters. A common issue with component data sharing is prop drilling.

<>Copy
@Component({ selector: 'my-dashboard', template: `<my-graph [filters]="filters"></my-graph>` }) export class DashboardComponent { // Don't need filter but necessary to pass it to my-graph // It's called prop drilling @Input() filters; }

Another issue is the complexity of components interactions. There is a lot of @Input and @Output whose purpose is to trigger other components reload. It makes the code hard to digest since our app contains a lot of filters.

<>Copy
@Component({ selector: 'app-root', template: ` <app-sort [defaultSort]="currentSort" (sortChange)="updateData($event)"> </app-sort> <app-data [sort]="currentSort"></app-data> `, }) export class AppComponent { // Sort filter is stored here in the end currentSort = 'Price'; updateData($event) { this.currentSort = $event; } } @Component({ selector: 'app-sort', template: ` <input *ngFor="let sort of sorts" type="radio" name="sort" [value]="sort" [checked]="sort === defaultSort" (change)="sortClick($event)"/>{{ sort }} `, }) export class SortComponent { @Input() defaultSort; @Output() sortChange = new EventEmitter(); sorts = ['Price', 'Size']; sortClick($event) { const sort = $event.target.value; this.sortChange.emit(sort); } } @Component({ selector: 'app-data', template: `<div>Data sorted by {{ sort }}</div>`, }) export class DataComponent { @Input() sort; }

In this configuration, the top AppComponent hold the filters values. It's also responsible for listening changes through @Output and update other components thanks to @Input.

A common method to simplify interactions consists of using singleton services that hold shared data. The code is less verbose but almost prevent you from using OnPush strategy. Check this Stackblitz to see how it looks with services.

Using NgRx to handle filters and trigger some components reload was our solution. This way, the filters are held in the store. Any component can get the store instance using dependency injection to retrieve the filters. You can consult the store content and history with the Redux DevTool extension..

This solution doesn't come without drawbacks. It made our component less reusable since there are no more inputs and outputs, all data are in the store.

Content imageContent image
Redux DevTool extension

With this architecture, both components and the store hold the business logic. To access data from the store, a component needs to subscribe to it (and also unsubscribe). In templates, the async pipe does the job for you. Yet, when you need to access store data from component methods it can’t help.

The brilliant idea : Let’s move everything
Link to this section

Depending on the feature the business logic was either in components or in NgRx store. Reading and grasp the code isn’t smooth: you don’t always know where to look at.

Like many apps, Instant Insights loads user details with HTTP requests. This information needs to be available for many components. Thus, we need to avoid prop drilling and repeating the same request in each component. The team came with two solutions:  cache the request or store the result somewhere (for instance in the NgRx store).

NgRx looked again as our savior. Our team did overview the power of NgRx when using it for filters. Updating one filter can trigger reload for all or some components. Also, it provides a clear architecture for the codebase: actions, effects, reducers, selectors.

Content imageContent image
NgRx architecture from official documentation

In the meantime, we learned another team at Smart was already NgRx. They used it to handle all their business logic and they were happy about it.

The doubt : Frequent complains about NgRx
Link to this section

Before jumping in, we read many articles about state management. Our aim was to gather the caveats and drawbacks as well as the benefits. Likewise, we also read about NgRx.

NgRx = Angular + ReactiveX (RxJS in our situation)

It’s important because this library uses a very specific way of doing things, far away from Redux. After all, NgRx goal is to add Angular and ReactiveX on the top of state management.

By far, the main complaint is to many boilerplate. Putting a simple value in the store involves creating many files: Action, Reducer, State, Selector and Effect (potentially).

Note the development team worked hard to reduce the boilerplate for creating a store with NgRx version 8.
Content imageContent image
NgRx boilerplate

You may omit index.js which declares the root state and its reducers. Also, the state in  state.js is often included with the reducer.

More code to write means slower development and painful debug but also more bugs. That’s why you aren’t supposed to use the store for everything in your app. Only when many components need to access the same piece of information.

Dan Abramov working in React team and Redux author, explains it well in his article: You Might Not Need Redux.

A few others complaints:

  • Many concepts to learn (effects is the worst)
  • Learning curve with RxJS
  • State immutability means copying the entire state on each update
  • All data in one place, seems like a god object

Finding more complaints is quite easy, just type Redux + drawbacks, pitfalls, hell, sins, etc. Try to look for the drawbacks instead benefits only. It helps to get the full picture and choose whether using a technology is relevant in your situation.

The self congratulation: A few months later
Link to this section

No spoil, you already know the team consider this shift as a success. It wasn’t straightforward and we still keep learning about NgRx mechanics. The major issue, if you ask are effects and more precisely RxJS.

Content imageContent image
RxJS merge operator marble diagram (from https://rxmarbles.com/)

In short, the learning curve for NgRx isn’t steep if you already know reactive programming, observables and ReactiveX. State management concepts are far easier to grasp and debug.

Using NgRx to host our business logic helped us to have a clear architecture across the codebase. Large service methods logic is now split between reducers, selectors and effects.

This results in smaller pieces of code easier to test, more robust and less bug-prone. Note reducers must be pure functions (functional programming concept)

Besides, you can even split the tasks for new features:

  • Write action and reducers
  • Load data in effects
  • Display store data in components using selectors

Enormous Angular components turned into simple-minded (aka dumb) components. It means the smart component does the heavy-lift by loading data and manipulating it while the dumb is roughly a template.

Redux introduced in its early ages the Smart and Dumb component concept
<>Copy
export class ItemListComponent { user$ = this.store.select(selectUser); config$ = this.store.select(selectConfig); items$ = this.store.select(selectItems); constructor(private store: Store<RootState>) {} addItem (item) { this.store.dispatch(addItem(item.id)); } removeItem (item) { this.store.dispatch(removeItem(item.id)); } }

From NgRx point of view, there is no need for smart components. It turns all your Angular components into dumb components. They use selectors to get fine formatted data and dispatch actions according to user interactions. This is how the majority of our components looks like.

Do you remember the example for before with SortComponent and DataComponent? Here is how it looks with NgRx.

<>Copy
@Component({ selector: 'app-sort', template: ` <input *ngFor="let sort of sorts" type="radio" name="sort" [value]="sort" [checked]="sort === (filters$ | async).sort" (change)="updateSort($event)"/>{{ sort }} `, }) export class SortComponent { sorts = ['Price', 'Size']; filters$ = this.store.select(selectFilters); constructor(private store: Store<RootState>){} updateSort($event) { const sort = $event.target.value; this.store.dispatch(selectSort(sort)); } } @Component({ selector: 'app-data', template: `<div>Data sorted by {{ (filters$ | async).sort }}</div>`, }) export class DataComponent { filters$ = this.store.select(selectFilters); constructor(private store: Store<RootState>){} }

This is similar to the singleton services approach (except the NgRx boilerplate). The game-changer for our team are effects. It allows us to handle interactions between all filters in a single place.

For instance, updating the sorting filter may change another filter on condition and trigger a reload. There is no need to code this logic in each place you want to update the sorting filter. Redux DevTool extension will show each action in the right order.

A dash of fine tuning
Link to this section

As explained before, NgRx and state management bring a clear but opinionated architecture. It means you may not be able to do something because of NgRx. In general, it’s because you shouldn’t do it at all.

The architecture is flexible enough: selectors, as well as, effects can access the entire application state.

Though sometimes you need to break the rules. For instance, you may pay attention to parameterized selectors.

Here is a practical example of our filter story from before. Remember our team build a reporting tool with filters reloading graphs and plots?

Content imageContent image
Graph loading effect

loadGraph$ effect observes action dispatched to the store whenever a filter changes. It transforms an action into another action according to filters.

This is a Context based action decider effect. Read more about it in NgRx: Patterns and Techniques

The trick is it doesn’t always trigger a new action. For instance, at the first addField action, no reload is needed. This makes the effect a bit hard to understand. Also, the effect gets bigger with reloading conditions logic.

One solution might be to define a DoNothing action dispatched when the reload doesn’t occur. The team decided to go further and split the logic between reloading conditions and the actual graph data loading.

Content imageContent image
Composing effects to split business logic

It seems more complex but easier to digest for us. loadGraph$ effect is only responsible for loading graph data. Besides, reloadGraph$ listen to all filters changes and sometimes trigger graph reload.

This is effects composition: an effect triggers another effect. This method can be helpful, but we should not abuse it. Tracking actions among all the app effects and reducers might be hard. Even with the amazing Redux DevTool extension.

It does exist a tracing solution but not yet implement in NgRx. For an alternative based on static analysis, check NgRx-Vis project.


Wrapping up
Link to this section

Thanks for reading! That was a sum up of our team experience with NgRx. Getting grasp over this technology is hard but very rewarding at the end. Making the move definitively worth it.

Don’t forget all projects are different. For instance, Instant Insights only show data but never edit and persist information. Using NgRx as we did might not be helpful for small projects as well.

Don’t hesitate to share your experience with us or comment to ask questions.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Avid learner about web development and writer for sharing tips and tricks, Full-Stack Developer @SmartAdserver

author_image

About the author

Jérémy Bardon

Avid learner about web development and writer for sharing tips and tricks, Full-Stack Developer @SmartAdserver

About the author

author_image

Avid learner about web development and writer for sharing tips and tricks, Full-Stack Developer @SmartAdserver

Featured articles