NgRx Best Practices Series: 2. Modularity

In this series of articles, I am sharing the lessons that I have learned from building reactive applications in Angular using NgRx state management. 

My introduction explained how I came to use NgRx. The first part showed how to add caching functionality to it. Here, we’ll look at state management from an architectural point of view.

The source code is available on GitHub.

If you prefer watching over reading, here is the video version:

Modularity: Container, Presentation & State

This architectural pattern integrates state management into the well-known container/presentation architecture.

We create a module just for the NgRx-relevant code. Next to that, we divide up our components into two further modules. The first one holds the container components which are responsible for the communication with the state. The second one contains the presentation components. They are responsible for the rendering.

Having three distinct modules with clear responsibilities improves tremendously the maintainability of our application.

Container & Presentational Components
Container & Presentational Components

The underlying problem is not new and is very well known in other areas of programming. A class/function/component does too many things. If we see a component that stretches over 100 lines of code, we have probably found a specimen. Due to that size, it is hard to quickly understand its purpose. This makes it less maintainable, and testable. It goes against the Single Responsibility Principle.

Very often, we find the same in the Angular world. There is always a component that contains non-trivial code along with the HTML and CSS. Examples for non-trivial code or "logic" can be dispatching actions to NgRx, using the router, or some interaction with services.

Be careful! Communicating with NgRx is one thing, rendering is another. Don't squeeze that into a single component. One component should do only one thing. It is rendering or executing "logic". That's the basic idea behind the container and presentation component pattern.

So we have a so-called presentation component that is responsible for the visualisation. It consists mainly of HTML & CSS. In our example, this would mean showing the list or the form of the customer. The component does not know how to fetch data from NgRx. It doesn't even know if NgRx is used at all. It just gets its input via the @Input(). It also does not know what should happen when the user clicks on a submit button. It just emits an event via @Output() and hopes that somebody up in the component hierarchy knows what to do.

That somebody is the container component. It is context-aware. It knows how to deal with NgRx, what selectors are required, how to access the route, etc. It does not know how the data is visualised. It just fetches, transforms, and passes it on. A container component typically has no CSS, only minimal HTML. That HTML holds the selector of the underlying presentation component.

The container component should also not have any Input or Output bindings. As a context-aware component, it knows its place in the application, where to get all the data from, and whom to call once an event is happening.

There exist alternative terms for this pattern. Very common terms include Smart/Dumb component or Feature/UI components.

Of course, it is not always possible to adhere to these strict rules. There will be times when a presentation component needs to use Angular's dependency injection. For example, if it wants to show a form (like the customer detail screen), it should inject the `FormBuilder`. The same is true for container components. There can be valid use cases where it receives data via @Input from a parent component. But please, let's try to avoid having multiple levels of nested container components linked together via property binding.

What about the state?

Splitting up the components is the hard part. What comes next is easy. The components types get their own modules. We can call the modules with the container components "feature" and the one with the presentations "ui". This is the naming schema that is also used by nx (see below).

The same is true for the state. We move all relevant NgRx code into a third module which is just called “data” (nx naming).

Finer grained use cases

An immediate perceivable advantage is that we can now reuse the same form component but with different behaviour. The presentation component with the form is the same, regardless if it is about editing or adding a customer. The container components handle the differences. Depending on the differences between the editing or adding, we can have two of them.

So we end up with three components. It might look like overkill at first sight. That's not the case. The components are very small in size and we immediately know where to look when we want to change the logic for editing or adding a customer (container components) or if we want just to change the form itself (presentation component).

Easier Testing

This strict separation has other advantages as well. We could easily create unit tests for the container component. 

Creating unit tests for frontend technologies is very hard. The problem is the mixture between actual code and HTML/CSS. With container components, only the code stays. It is a normal class with methods and we can write normal unit tests against it as we would do in the backend.

You must decide whether testing the code part of the presentation component is needed. We can skip it entirely or switch to a special testing technique like Visual Regression. This decision will very likely depend on our application type and its quality requirements.

More Code == Better???

This is for readers who just started their programming career: Splitting up components makes our codebase more maintainable. For freshman developers, this might look counter intuitive. We have just made two components - or even three - instead of one. Along with all the component boilerplate (TypeScript class declaration, @Component metadata), we might end up having even more lines of code than before. Why should that be better?

Growing code size and better maintainability are not mutually exclusive. This is normal. We don't eliminate the complexity, we just slice it up into smaller units. It is easier for us if we can work on three small problems instead of one big problem. Or to put it another way, it is easier to work with 3 files with 45 lines of code instead of one file with 120 lines of code.

Classes get shorter and responsibilities are clear. If we have to search for bugs with the state, we look into the state module. If the layout is broken, we go to the UI module. If the state is fine but the wrong data ends up in the UI, it is probably a bug in the container component.

Dependency Rules

We only allow container components to talk to NgRx. How to enforce that?

We use nx, an extension for the Angular CLI, and use its feature to define dependency rules. Done!

From now on, whenever a presentation component tries to use something from the data module (=NgRx related module), we get a linting error and the build breaks.

Linting Error
Linting Error due to defined Dependency Rules

But that's not all. We can go even further. In NgRx, we want certain actions to only be called by the container components. For example: If we use the LoadStatus pattern, then it should be forbidden for any component outside of NgRx to dispatch the load method (or even loaded).

In order to achieve that, the data module only exports a limited list of actions. This list is defined in its index.ts. Any component can still access a non-exported action by directly referencing the NgRx actions file. But this is called a deep import and nx would step in here again and throw a linting error.

NgRx Actions exposing one set for internal usage and another for public
index.ts exposing only NgRx actions defined in PublicCustomerActions
Component with Deep Import
Linting Error due to Deep Import
Linting Error due to Deep Import

Where to put the models

In which of the three modules should we put the interfaces that define our domain models? In none of them. If we put the interfaces into the state or container module, our UI components cannot have them as input or output values. Unless our application only has a generic UI, this would be a problem.

We also can't add to the UI because we don't want to couple state management with our UI. We have the container components as mediators.

The best place for the models would be a module of their own where our three modules have a dependency to.

Module Types Dependencies
Dependencies between Module Types

Outlook

The next part of this series will deal with a very common type of problem. We want to redirect the user after an effect has successfully sent a request to the backend. 

Another similar use case, based on the same type of problem, is to display a notification message after a backend communication.

Should the effect do that? Would it be better in a component? Stay tuned to find out the answer!

Leave a Reply