NGRX: Is it worth it?
If you ever developed any application bigger than a to-do list, chances are that you have encountered problems with state management.
NgRx is a set of Angular libraries that uses the redux pattern and reactive extensions (RxJS) to solve state management problems in Angular.
State management problems often sound like:
- Do I modify data in the child component or bubble an event to its container component?
- Do I request the same data from the API again?
- Why is the modified data not reflected in the parent component?
- Should I move this component higher in the component tree?
- Refactor! Refactor! Refactor! As your app scales, so does your frustration!
Yes, poor state management can take developers pain and frustration to a whole new level.
How exactly did our application get to this point and what is the solution?
N.B. In these article, I will often refer to NgRx as @ngrx.
The component based architecture maintains state uniformity in a uni-directional parent to child or children direction where data is passed down as input to other components that need to use them.
A common problem of the component based architecture occurs when there is a need for state modification (e.g state passed into a custom component through an input) across same-level and or higher-level components. The result is that the data modification will only be reflected in the component where the change is made and its children while other components on the same and higher level will not reflect the changed data which basically results in state management problems.
An immediate solution is to move the modifying component to a higher level in the component tree such that other components that use the same data become its children while another solution is to create a shared service that will hold the state to be modified and the modification logic, while the components get and modify the state by accessing methods of the shared service.
So, shared services (Dependency Injection) solves the problems of state management to an extent. However:
- One can still easily make the mistake of copying state from a service and modifying it within the component.
- Lots of services required for even a medium-sized user interactive application.
- The application is highly prone to changing requirements.
- More tightly coupled components.
- Potential Issues with Angular change detection e.g. data will not be changed in components if the data reference changes in the service.
So, how can we prevent creating sloppy solutions which are destined to be refactored while keeping up with change requirements and deadlines?
Welcome NgRx!
NgRx = Angular + Redux + RxJS(Reactive extensions JavaScript)
If you are new to any of these concepts, here are some pointers to get you up and running.
NgRx is based on the flux architecture with the concept of a single universal store which serves as the single source of truth in the application at all times. i.e so long as data is from the store, it is always correct.
Furthermore, NGRX is based on Reactive Programming principles which take advantage of the Angular OnPush change detection strategy as well as keep track of state changes in the application at run-time.
As mentioned earlier, NgRx is is a set of Angular libraries which includes:
- @ngrx/store
- @ngrx/effects
- @ngrx/router-store
- @ngrx/entity
- @ngrx/schematics
of which @ngrx/store and @ngrx/effects libraries are the basic essentials.
What is @ngrx/store?
@ngrx/store is an abstract concept. It is formed by importing all the applications reducer functions into the `StoreModule` which is imported from @ngrx/store library into our application module.
What are actions?
Actions are basically typescript classes that implement the @ngrx/store Action interface which consist of a type and an optional payload parameter. The type is usually a simple string that tells what the action does and is often declared as readonly and bound to a constant as it never changes. e.g `LOGIN_ACTION` while the payload usually contains JavaScript first class data types such as strings, numbers and objects.
The payload carries data that should be modified, added or removed from the store by the reducer.
Most actions are acted on by the reducer, which does the actual logic of modification, addition or deletion from the store while other actions go to effects.
It is also possible for an action to be processed by the reducer and effect simultaneously.
What are reducers?
Reducers are functions that takes 2 parameters:
1. The current application state (usually undefined at application startup).
2. The action dispatched to the store.
In the reducer function is a switch statement that switches through all available action types and does the logic of returning a new state whenever a valid action is dispatched. It also contains a default case which returns the current application state.
What are effects?
Effects are basically angular services that use the NgRx @Effect() decorator to create a side-effect. They are what I often refer to as action hijackers. Effects simply lay-in wait for an action to be dispatched and latch onto it as soon as it is dispatched to perform some asynchronous operation(s). These operations are usually tasks such as server-side authentication and API communication.
An effect usually fires another action once it completes its asynchronous operation(s) depending on whether the asynchronous task was successfully completed or not. For example if a `GET_USER_ACTION` was dispatched from a component, an effect listening to the `GET_USER_ACTION` may be configured to send the user id to the server, fetch the user data and then dispatch another action `GET_USER_SUCCESS_ACTION` which is processed by the reducer to add the user data to the store, or in case of an error, catch the error and send a `GET_USER_FAILURE_ACTION` explaining what went wrong.
Effects are passed in to the EffectsModule parameter as an array and the EffectsModule is imported from @ngrx/effects library.
Finally, coming to our Angular components, all we have to do to get data from the store into the components is to inject @ngrx/store. There are two store methods that are frequently accessed by the component:
- select: This is used to retrieve some data from the store. It is worth noting that the data returned is an observable and automatically updates whenever a new state is available. It is advisable to use the Angular async pipe to display the actual observable values on the components template as opposed to subscribing to the observable.
- dispatch: This is used to dispatch a new action to the store(via the reducer) or to an effect.
What are the pros of using NgRx?
- State management problem solved!
- No more need for tightly coupled components which makes addition of new features easy.
- Simpler components since modification logic has been delegated to reducers and effects.
- Immutability of state at component level which eliminates loads of bugs.
- Works with the Angular OnPush change detection strategy.
- Easy debugging using @ngrx/store-dev-tools.
- Availability of the current Angular router state via @ngrx/router-store.
- Actions, reducers, and effects can be split across multiple files and modules.
- Support for lazy loading through the StoreModule.forFeature() and EffectsModule.forFeature() module imports which helps to improve app performance as each module loads only the parts of the store it requires.
- @ngrx/entity which helps to simplify reducer logic and state selection for records.
- NgRx flux architecture can be combined with component based architecture allowing for creation of container components and presentational components.
- Official documentation of NgRx available here.
And the cons?
- Initially more boilerplate code i.e. you need to create actions, reducers, effects. However, @ngrx/schematics provides a command line solution for generating effects, actions, reducers and entities on the fly to hasten this process.
- Finding appropriate names for actions and effects may be difficult.
- Steep learning curve especially for new Angular developers.
- NgRx is not a google project and support for future angular versions cannot be guaranteed.
- Good knowledge of reactive extensions javascript (RxJS) is required.
Although an NgRx based project may require more knowledge and boilerplate code, it teaches you to adapt to a new development mindset, makes your application less prone to change requirements and helps you to develop maintainable, scalable and easily debuggable Angular applications.
Given all these pros and cons, is NgRx really worth a try?
Yes! NgRx is definitely worth a try as a solution to your state management problems. ‘A trial will convince you!’
Visit the official page of NgRx here.