One of the challenging tasks when building a web application is persisting the state across the application.
Before we talk about persisting the state, we need to understand a few terms here, State and State Management.
State is what an application knows about the user, their current interaction with the application, where they are in the application or may be what information they have entered so far.
Whether or not we are managing it, in an interactive application there is always a state — User’s perform actions and things change in response to those actions.
State management makes the state of our application tangible in the form of a data structure (may be as simple as an object) that we can read from and write to. It makes our ‘invisible’ state clearly visible to work with.
Different state management strategies are available, we will talk through them in this post. Which strategy suits your current application is something that you need to take a call on, this decision is critical, as the last thing you want any programmer to do with your application is to mess with the internal state of the application.
Below we will talk about possible options for managing the state of our Angular applications, we won’t go to the implementation part of it in detail, but on how to approach a state management strategy that fits our application. Let’s keep it simple and try to depict the explanations through diagrams and examples when needed. Let’s get going!
1. @Input @Output
The idea of Input and Output is to exchange data between components. Input is used to receive the data in, whereas, Output sends the data out by exposing event producers, usually EventEmitter objects.
Consider your application has a container component and uses presentation components to render the view as shown in the below diagram. Now, when your container wants to communicate with the child (presentation component) it can do so easily using @Input and if the child performs some action and wants to pass the data to the parent container component it can do so using the @Output EventEmitter.
This is an efficient way to exchange state between the components when,
- Our Angular application is small and maintains a parent child relationship between the components that communicate data
But, when an application has the following use cases, we can't rely only on this method to manage our state completely
- Sibling components need to communicate
- Component at a deeper level wants to talk to a component at a way higher level, as shown in the below diagram, say the component 4 has to talk to component 1, 3 levels of passing data, rabbit hole, right!
Services come to the rescue.
To avoid traversing data through deeper levels of nested parent child components or if we want to share data with sibling components, the data that has to be shared between components can be stored in a service file that when injected by these components, the shared data is accessible easily.
As shown in the below diagram, we can see how services are utilized to share the data between components at deeper levels and between the sibling components.
Sharing data through services make communicating the state information simple and maintainable until it doesn’t!
As the project scope increases, we keep adding more services, and when the data in the service is shared and modified across the application, it becomes difficult to track what data in which service is modified from where, as depicted in the diagram below. This is when we will want to rethink the state management strategy for our application.
We can solve the above problem by sticking to a few principles,
CQRS — Command Query Separation
It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both.
Redux makes the application state more predictable by allowing the unidirectional data flow throughout the application where all the application state is maintained in a single store
There are several ways to achieve this,
- Observable Store Service
An observable data service is an Angular injectable service that can be used to provide data to multiple parts of the application.
In a simple angular project we can apply the ideas of Redux and use just the right tool we already know well as an Angular developer, getters/setters and RxJS’s Behaviour Subject for managing the state!
We can implement the Observable store as shown in the below diagram,
Explained as follows,
- Every store entity is exposed as a private Behavior subject. Behavior subject because it gives out the recent value that was emitted from the subject when subscribed
- This Behavior subject which has the store value, is exposed as a public observable, as we don’t want the components to modify the store data from anywhere
- Business components can now subscribe to the store data using the observable exposed as a Getter in step 2
- Pure functions are then exposed as Setters using which the store data can be updated by the business components
The Observable store option is a great solution for simple application instead of cumbersome third-party library to achieve state management, but can turn out to be challenging in terms of maintainability, when the number of behavior subjects(for every store entity) increases, that’s when we need to look for a stronger and more centralized solution, NgRx!
- NgRx Store
NgRx provides a more centralized approach for state management by strictly following the Redux principles.
It implies a strict rule on how the data flows, how it’s modified and accessed in the application as shown in the below diagram,
- As from the diagram we see that the data in the application flows in a single direction.
- Store is a single source of truth that maintains state data in the form of a data structure(Object tree)
- Component can access the store data, using Selectors that are exposed
- Component can modify the data, by dispatching Actions that are exposed
- Reducers are pure functions, responsible for immutably updating the state data when an action is dispatched
Another great advantage of using NgRx is the tooling it provides for debugging, through Time Travel, you can see exactly how your state object changed and the action that caused it.
Before we choose on a state management strategy its important to know the size/behavior of our application and the amount of state data it needs to hold.
- If our application has a huge state data to maintain, having centralized state management strategy with libraries like NgRx is of great help
- If our application has just a considerable state and a bunch of components, you can always go for something as simple as an Observable store. Because, you wouldn’t want to import a whole third party library or you just wouldn’t want to touch 3 files (reducer, action, selector) to get a simple thing done?
- And also, In a simple angular application we can still think of achieving the state interaction between different sibling/grandchild components simply through services and interaction between parent child components using Input/Output, depending on the scenario.
Remember, NGRX complicates things for simple applications, and simplifies things for complex applications