Managing state in large single page applications can quickly get messy. A central store following the redux pattern can help to clearify the responsibilities of managing state. In this blogpost we describe how to use NgRx as a central store in an Angular application.
Why Manage State? - A Typical Problem
A typical single page application (SPA) consists of several components that have to be synchronized to provide the user with a consistent view of data shown in the components.
Consider two components with a logical parent child relation. One of the common ways to realize this is to use a list of items and next to it details for the item that is currently selected in the list. As a user, I intuitively expect a strong binding of the data shown in those two controls. If I select an item from the list, I expect to see the details of that specific item (and not of some other item). The same is true in the other direction. If I use the detail view to change an attribute of the item that is also shown in the list, I expect the item in the list to immediately reflect the changes I perform in the detail view.
With the out-of-the-box mechanisms of Angular components, that would probably mean to fire events on each component and and wire them through the containing screen component.
Let’s say a [.code]CharactersScreenComponent[.code] is composed of a [.code]CharactersListComponent[.code] and a [.code]CharacterDetailsComponent[.code].
On changing the selected character in the list, the characterSelected event gets fired, probably resulting in a change of a corresponding field (e.g. [.code]currentCharacter[.code]) in the controller of the [.code]CharactersScreenComponent[.code]. This change can be reflected via Angular data binding mechanism to the [.code]CharacterDetailsComponent[.code].
On the other side, the [.code]CharacterDetailsComponent[.code] communicates changes of details of the selected character by emitting the [.code]characterUpdated[.code] event to the [.code]CharactersScreenComponent[.code], which then will reflect this change via data binding to the [.code]CharactersListComponent[.code].
The following diagram shows the relationships and communication paths:
While this is doable for very simple user interfaces, it is easy to spot the problems with this approach:
- Tight coupling - Components need to have intimate knowledge about events and properties of other components, leading to tight coupling between them.
- Hard to follow interactions - Sending of events on the one hand and the reaction to received events with triggering changes to state on the other hand are spread amongst the components. This might make it hard to find all the places where an adoption has be made and to understand and assess the impact of a planned change.
- Non-local impact of changes - If a new component gets added to the application, other components might need to be changed to react to events of this new component. Those cross-dependencies can force us to change parts of the application just to maintain consistency of interaction.
- Data exposition - Each component has to provide access to some of its data. That could be designed by each event carrying the relevant data in its payload, or by giving access to portions of the component’s state via getters or methods.
- No single source of truth - Portions of data of a domain object might reside in different components. Some parts might only be available in one component, some parts might be duplicated in different components.
This makes it difficult to distribute the relevant data to the respective components on loading data from a data source such as an API. On writing back the data after changes in the UI, this portions have to be collected and combined again to get a complete picture of the data to be sent to the backend.
Missing out on one of those connections can lead to inconsistency in the data represented in each individual component. - Hard to test - In this model, state is always bound to components. This makes it hard to write automated tests checking the effects of changes to the state. On this model, testing involves instantiating components including lifetime management. While this is totally possible with the Angular tooling, it makes the tests cumbersome to write and slow on execution.
Having seen the transformative impact NgRx can have on state management within Angular, the path to modernizing your software with such reactive frameworks becomes evident. Our Software Modernization services are tailored to empower your applications with the latest in reactive state management, ensuring your software remains at the cutting edge.
Maintaining Central State
To overcome most of the issues depicted above, one idea is to establish a single central place for all the state of the application. This part is often referred to as “store”.
With such a store in place, components can get their data from there and report changes to it:
The Redux Pattern
This is basically the idea made popular in the world of React by Redux in 2015. For the Angular world, there are several implementation following that same idea. In this post, we will refer to the concepts of NgRx - Reactive State for Angular.
An NgRx store utilizes these concepts:
- The store is immutable.
On each attempt to change the state, the previous state is replaced by a new version including the changes. - The transition from one version of the state to the next is done by reducers, triggered by actions.
Reducers are pure functions, combining the old state and the payload (if any) of the triggering action and returning the new state. - Views of the data in the store can be derived by selectors.
A selector is an object wrapping a pure function defining a projection of the data in the store. Selectors utilize RxJS observables, allowing the client code to react to changes in the store. In combination with Angular's change detection, one can easily bind components to the result of a selector. Without any boilerplate, the component will reflect relevant changes in the store in its visual representation on the screen.
This picture shows the pieces and their interaction:
List and Detail Extended
With that tooling in place, the structure for the screen depicted above might look similar to this:
Most of the parts of the application are logically situated around the central store.
Let’s revisit the main parts in this example:
- Data for the components is provided by selectors:
The selector [.code]selectAllCharacters[.code] provides the list of characters for the list component. The selector [.code]selectCurrentCharacter[.code] provides the data for the detail component. - Desired changes to the store are conveyed by actions:
In reaction to the list component triggering the [.code]characterSelected[.code] event, the action [.code]setCurrentCharacter[.code] is dispatched. If the detail component triggers the [.code]characterUpdated[.code] event, the action [.code]updateCharacter[.code] is dispatched. This could be done in the controllers of the respective components or in the containing [.code]CharactersScreenComponent[.code]. - The next state of the store is defined by reducers:
One reducer is executed in reaction to dispatching the [.code]setCurrentCharacter[.code] action, e.g. taking the id of the currently selected item from the action’s payload and the previous state of the store to yield the full new version of the store. Another reducer is triggered by dispatching the action. It takes the changed detail data of the character from the payload of the action and merges it into the previous state of the store to provide the new version.
As promised on introducing the store, the picture shows that there are no dependencies between the list component and the details component. All data changes are communicated solely via the central store.
A component just pushes its changes to the store by dispatching an action, not caring at all which or how many other components depend on that data. All those dependent components get new data pushed via their selectors, without the need to know where this change comes from. In the center of the picture, the reducer's single responsibility is to take the information from the triggering action and properly merge it with the current state of the system.
NgRx in Code
To get a better insight, let us inspect the central parts in code of working with NgRx. We deliberately leave out all the setup code to carve out the essence of the pattern. The full code for the sample application can be found on GitHub: https://github.com/squer-solutions/ngrx-blogpost
Model for the Character
The model for a character contains all the fields we know from the user interface along with an id. Altough it would probably be used even without using NgRx, let’s depict it here to get the whole picture (refer to domain.ts):
Definition of the StoreModel
The model of the data in the store is defined using an interface (refer to characters.state.ts):
For this simple sample application, the store just contains of the list of all characters and the id of the currently selected character (if any).
The Selectors
Definition
The two selectors [.code]selectAllCharacters[.code] (returning an array of all characters) and [.code]selectCurrentCharacter[.code] (returning the currently selected character) derive just those parts from the store that are needed for that specific use case the selector is used for (refer to characters.selectors.ts):
Note that [.code]selectAllCharacters[.code] returns not just all Characters as stored in the store, but it returns an array of [.code]CharacterListItems[.code] enhancing each character with a boolean flag showing if the character is currently selected (refer to view-model.ts). This flag is not stored as is, but derived from the id of the respective character and the [.code]currentCharacterId[.code] that is saved in the store (refer to the model in [.code]CharactersState[.code]).
This shows the nice possibility to project the data in the store into the exact form needed for the consumer side.
Usage
Using the selectors is pretty simple, as we just have to pass them to the [.code]select[.code] function of an injected [.code]Store[.code] instance (refer to characters-screen.component.ts):
The observables returned from the selectors can be used with usual angular mechanics for further use, e.g. with the [.code]AsyncPipe[.code] (refer to characters-screen.component.html):
The Actions
The actions define the events that can be used to progress the state of the store.
Definition
Actions are based on a unique string complemented by the payload, an object containing all the data that is needed for the reducers to derive the next generation of the state (refer to characters.actions.ts):
For the case of [.code]setCurrentCharacter[.code] it is just the id of the current character. For [.code]updateCharacter[.code] it is the whole character object.
Usage
For the changes defined by the action to actually happen, the actions have to be dispatched using the [.code]dispatch()[.code] method of the [.code]Store[.code] object.
The following snipped shows how the action [.code]onCharacterSelected[.code] is dispatched (refer to characters-screen.component.ts):
The Reducers
Now reducers can react to each action by defining the next generation of the whole state. Usually this involves copying the current state along with the modifications to be done according to the payload of the action (refer to characters.reducer.ts):
After setting up the initial state of the store, for each action a block started with [.code]on[.code] is provided.
The first block starting in line 4 reacts on the action [.code]setCurrentCharacter[.code]. While the first argument of the [.code]on[.code] function denotes the action, the second argument defines the reducer for that specific action. This reducer is a function that merges the current state of the store with the payload of a the action to derive the new state of the store. For the [.code]setCurrentCharacter[.code] action it modifies the [.code]currentCharacterId[.code] while keeping the rest untouched. This is done by copying the whole state using the spread-operator [.code]{...state}[.code] and overwriting just that single item.
The same idea goes for updating one of the characters in the list in reaction to the action [.code]updateCharacter[.code]. Here we first derive the index of the character to be updated and then produce a copy of the characters, overwriting the proper character. We use that modified character list to overwrite the character list with a new copy of the state.
Redux - A Functional Approach
I want to explicitly point out, that we see here a completely functional approach. Both reducers and selectors are basically just pure functions written in plain TypeScript, with no dependency to Angular. Therefore they can be perfectly tested in isolation in simple unit tests.
The actions are essentially just bags to carry the meaning of the action and some payload (if any) without any logic. So no need to test them in isolation.
Conclusion
In this article we saw that the redux pattern provides us with a great means to untangle the dependencies between components regarding data flow in a single page application. By having a central place for the state of the application and a clear separation of the concerns of reading and changing the data, it is easier to maintain non-trivial to large applications.
Selectors describe the projection of the state in the store to the very special needs of component. Changes to the store are triggered by dispatching Actions, while Reducers are then responsible to provide a new state of the store as reaction to that dispatched action.
This separation makes it easy to keep changes within an easy to oversee area and to reason about what’s going on in the case of problems.
The push-model allows all consuming parts of the application to concentrate on the formulation of what data is needed, without caring about updates. The sources of data or changes do not have to care about how to get a change done or the consumers. Each possible transformations of the state is done in a single place, that can be easily implemented.
Based on functional nature of the parts, testability is supported with no hurdles.
To further strengthen the power of NgRx in the context of modern Angular applications, we recommend familiarizing yourself with complementary architectural patterns that optimize the development of frontend applications in a microservices environment. Of particular note are our article on Backends for Frontends, which provides a customized strategy for integrating micro frontends, and Microservices From Entities to highly independent Customer Journeys, which provides a deep dive into designing highly independent customer journeys in a microservices architecture. These concepts can help you realize the full potential of state management with NgRx by ensuring efficient data handling and seamless integration between different parts of your application.
Criticisms - and why we Still Consider NgRx at SQUER
While the basic idea of centralizing state management in an application is still valid and not so much discussed, in the last years a lot of criticism came up (refering to post such as Why I stopped using NgRx by Lior Caspi or Why I don't like NgRx and what are the alternative options? by Trung Vo).
The Criticism
As far as I can see, the discussion revolves around one sentiment
NgRx’s ceremony might not carry its weight
People blame NgRx for having too many moving parts, too much syntax to know, being too hard to follow or debug.
Are they right? Well - yes, and no.
Our Take on it
Of course when using NgRx in an angular application a handful of concepts come to the scene:
- The store models defining the shape of the data in the store
- The reducers to get from one version of the state to the next. Action by action.
- The actions with their payload to trigger reducers and effects
- The selectors for all the projections of the data in the store
- The effects to handle side-effects (not covered in this post)
All of that is code. And code has to be managed so that it does not get out-of-hand. With all code you bring to the application you better prove its usefulness. Otherwise, it just adds to the inventory that the team has to maintain over time.
Factors to Consider
From our experience at SQUER Solutions, we think there are a few factors to consider when deciding on using NgRx:
- The richness of user interaction - The more intertwined the interactions are, the more benefit you gain from using a central store.
If your application basically is of the type often referred to a forms over data where most of the pages just show a specific portion of data from the backend, allow the user to change some fields and write the data back on the push of a button, you probably would get a lot of boilerplate compared to a very limited benefit.
On the other hand, if the application offers a lot of different ways to interact with the same data and many places showing different aspects of the data, the wiring of all the concerned components can become a mess pretty quick. The clearly defined interaction patterns that NgRx brings in can be a big relief. - How many components share data - The more components operate on the same portion of the data, the more managing the flow of events and data between them might get an issue. If you have very focused components with one clearly bounded responsibility each (and you strive for that, don’t you?) chances are that more components share data. If they share data, also interactions with one component might have consequences on other components. With NgRx, the code for this interactions gets a pretty clear structure. That can help greatly in restricting the context the developer has to keep in her head when working on a specific task.
- Team size and the life time of the application - The more developers are expected to work on the code base, the more you benefit from following well established patterns. Of course, this holds true for component interaction and data sharing. As long as your colleague and you are the single developers working full time on that application for a long time, you might be successful with a less formal approach.
But if you find yourself in an environment where developers are expected to move from project to project or where the team size is considerably high, you will do yourself a favor in adhering to a pattern, that is clearly structured and well document beyond the project’s private confluence space. When new team members find familiar patterns or at least can find a lot of material online, it helps getting them up to speed tremendously.
And What About the Size of the Application?
We do not think that the size of the application in itself is too much of a decisive factor.
We benefited a lot from refactoring an application with a single main page with a lot of interaction from ad-hoc component interactions to an NgRx store.
On the other hand we are happy without a store on applications that basically are just a simple frontend for tables in a database. Having two or three convenience features on some screens can be easily maintained without a central store, just by following the existing angular features in conjunction with common clean code patterns.
Things to Watch Out
Make a Deliberate Decision
The decision on using a store for an application should be based on solid arguments (see above). Positive experiences of one developer on the team can definitely lower the barrier of adopting tools such as NgRx in a project. But just because someone fell in love with a tool does not mean that it is really suitable for the task at hand.
Code Structure
NgRx brings new concepts to your code base. So better be protective on the structure of this code. Our approach is to start off with established patterns, but to be alert in all phases of the project if the structures that evolve over time still support comprehension and maintainability best.
Final Thoughts
Of course, with applications of reasonable size, the selectors, actions and reducers have to be managed. For example some thought should be given to simple things such as naming conventions of proper placement of the artifacts. But with a structure that is easy to comprehend and developed by the whole team, this is not a big deal.
Get the Discussion Going
What are your thoughts? Do you like the idea? Do you think its frightening? Do you work with a central store? What are your experiences? Did you decide against using a store and why? Did you use another library or even roll your own?
We would love to hear your story and maybe help you with your challenges.
Stay tuned!