Maintaining a good quality of code becomes a big challenge when your application is handling an increasing number of functionalities. The Redux pattern comes to the rescue. This article addresses individuals who hesitate between the two largest Angular Redux libraries: NgRx and NGXS. I will compare semantics and their additions, followed by an analysis of their size and performance.
Introduction to Redux
Redux mainly consists of 3 elements:
- Store – centralized state container that holds the complete state tree of an application.
- Actions – plain objects that describe the type of state changes or events that occur in the application.
- Reducers – pure functions that specify how the application’s state should be updated based on the actions dispatched, updating the store.
NgRx is the most popular Angular library for handling Redux patterns (550k weekly downloads in June 2023). The NgRx library introduces the concept of reactive programming to state management in Angular. It utilizes Observables to monitor state changes and react to them in real time. This enables developers to build applications that are reactive, scalable, and easy to maintain.
At the core of NgRx is the Store, a centralized place where the application state is stored. The Store represents the application state as an immutable object, and its updates are triggered by actions. Actions describe the intent to perform a state change, while reducers are responsible for processing actions and updating the state.
NgRx also provides an effect mechanism that enables declarative handling of asynchronous operations, such as network requests. Effects are special constructs that observe actions and carry out specific operations, such as retrieving data from a server, sending requests, etc.
For more details, I strongly recommend checking the well-maintained documentation here.
In contrast to NgRx, NGXS is more opinionated and aims to provide developers with a more structured and organized approach to state management. NGXS promotes the use of decorators and classes, which are easy to understand and use.
In NGXS, the application state is stored in a Store, which is defined as a class. The Store class contains properties that represent different sections or nodes of the application state. These properties are initialized at the start of the application and can be modified by actions to update the state.
Immutable State Handling
Both NGXS and NgRx store state as immutable objects. Changes are implemented by creating new state objects that integrate the desired modifications, maintaining predictability and control over state changes.
In NGXS, action is represented by a class. Payload is kept in a class instance property. Actions are captured by action handlers defined as methods in the state class. This mechanism is kind of a combination of effects and reducer handlers in NGRX. In the action handler, the state is being modified and some side effects can be triggered.
Upon completion, the handler can trigger subsequent actions to update the application state or perform other logic. This approach helps to effectively organize and maintain handler code. Nevertheless, these functions are not pure. The corollary of this is that they cannot be reused, as can be done in NGRX.
Action Handling in NGXS
- Action Representation:
- In NGXS, actions are represented by classes.
- Payload is stored in a class instance property.
- Action Handlers:
- Actions in NGXS are captured by action handlers, defined as method methods in the state class.
- This mechanism resembles a combination of effects and reducer handlers in NgRx.
- Modification and Side Effects:
- In the action handler, the state is modified, and side effects can be triggered.
- Upon completion, the handler can initiate subsequent actions to update the application state or perform other logic.
- This approach effectively organizes and maintains the handler code.
However, these functions in NGXS are not purely reusable. Unlike NgRx, where actions and reducers can be reused across different parts of the application, NGXS’s approach doesn’t inherently support this level of reusability.
To compare these two libraries, I have developed a straightforward “Todo task manager” app.
It is loading initial state and is capable of handling simple creating, updating, and deleting tasks.
NGRX offers a factory method to facilitate creating actions. We are able to create action with or without payload.
On the other side, NGXS is using plain classes. It is suggested to use namespace to group our actions in one topic.
Reducers and Events
The NgRx library requires us to create a separate reducer, which is subsequently employed implicitly to configure the StoreModule.
Whereas effects are handled separately:
This solution obligates us to create another action to manage the outcome of our primary action, if such an outcome exists. In the example above, to handle loading tasks I had to create two actions:
- One action to trigger loading tasks loadInitialTasks.
- Another action to handle result initialTasksLoaded (In case of handling error, we would need another action).
To handle the updating action, a separate action needs to be created, allowing the state to be mutated within the reducer. The action handling process is implemented by listening to all actions and filtering them by a proper type (ofType method) in the pipe.
NGXS handles state differently. It involves creating a class corresponding to the state. Then, in the method annotated with proper action, we are able to conduct some business logic and mutate the state. In this place, we are able to do the logic of event and reducer handlers.
With this solution, there’s no need to create a result action. All methods are aware of context, which then is mutated by using patchState method or setState.
With updated action, we are able to retrieve payload and then, same as before, conduct some business logic:
The main advantage of the NgRx approach is separation of concerns. Nevertheless, using NGXS, we can achieve the same result with much less code and a shorter process.
Similar to the previous point, in NgRx, it is recommended to create a separate file for selectors.
NgRx provides us with two main factory methods:
- createFeatureSelector – selects a feature state.
- createSelector – selects a specific property from the feature state.
Then, in the component, we can use this selector with the help of injected Store.
We can easily combine selectors from different states, although some people may find this syntax a bit perplexing. NgRx inherently supports memorization in selectors by default. On the other hand, NGXS aims to consolidate everything in a single place.
There are two options to achieve selection with NGXS:
- The first option involves manually selecting the state from the root state and then injecting the State class using this selector.
- Another approach (preferred by myself) is to annotate the field in a component with the Select decorator.
Due to the separation of effects and reducers, the creation of the Store is done separately from the configuration of Effects.
In NGXS, we only need to indicate a state class
Each of the libraries offers additional features and plugins that expedite and streamline development. I will not list all of them, as some additional features may be in development and released in the upcoming weeks.
Some features of NGRX
- @ngrx/data – simplifies the management of entities and their associated state within an Angular application. It provides a set of predefined actions, reducers, and selectors to handle common CRUD operations. This diminishes the need for boilerplate code and simplifies the intricacies of data management.
- @ngrx/router-store – seamlessly integrates Angular Router with NGRX, allowing developers to synchronize and manage the router state as part of the application’s global state. It provides convenient access to router-related information, such as the current URL, route parameters, and more.
- @ngrx/component-store – a lightweight state management solution tailored for managing the state of individual components in an Angular application. It provides a simplified API for defining and accessing component-specific state, along with built-in immutability and observability features. This translates into a more succinct and effective approach to handling local component state, all without necessitating the use of a comprehensive NgRx store.
Some features of NGXS
- @ngxs/storage-plugin – persists all or selected states into a storage (session or local). By configuring it, we can decide which state we want to persist.
- @ngxs/form-plugin – aids in keeping forms and state synchronized. Any changes in the form are automatically saved into the store and vice versa. It can save a substantial amount of time by reducing the need for extensive boilerplate code.
- @ngxs/router-plugin (equivalent of @ngrx/router-store) – integrates Angular Router with store. It empowers us to manually switch routes and listen to all changes that occur on the route.
- @ngxs/websocket-plugin – this extension enables WebSocket integration in an application, allowing bidirectional communication between the client and server in real-time.
- ngxs-labs – a concept where a community can develop new extensions. As of July 2023, in its stable version, it provides extensions such as @ngxs-labs/data, @ngxs-labs/decorator, or @ngxs-labs/emitter. All these facilities significantly facilitate and accelerate the development process.
More details are accessible here.
Build of the same app has:
- with NGRX: 360KB
- with NGXS: 308KB
The difference, even though it seems to be significant, can be relatively small for building a bigger application.
For a performance test, I have created a simple function that emits a DeleteTask action for every CreateTask action emitted.
Each test was repeated 10 times, the average of the single score was taken as the result. Results are as following:
- For 10 took 8.66ms
- For 200 took 130.99ms
- For 500 took 320.63ms
- For 1000 took 986.33ms
- For 10000 took 6557.72ms
- For 10 took 9.72ms
- For 200 took 118.96ms
- For 500 took 257.72ms
- For 1000 took 740.13ms
- For 10000 took 5923.25ms
The overhead in NGRX’s performance is attributed to its architecture. We are required to create two distinct actions to handle task creation and additional two for task deletion. In contrast, NGXS only requires one action for each of these tasks.
Tools + debugging
NGRX provides us with three additional development libraries that facilitate the development process:
- @ngrx/store-devtools – a powerful developer tool that integrates with NGRX/store, offering a user-friendly interface for debugging and inspecting the application state. It enables time-traveling through state changes and dispatching actions for testing and analysis purposes, ultimately enhancing the development workflow.
- @ngrx/schematics – a set of command-line schematics that automates the generation of NGRX-related code, such as actions, reducers, effects, and selectors. It saves developers time and ensures consistent and standardized code structures.
- @ngrx/eslint-plugin – an ESLint plugin created specifically to support and enforce best practices when working with NGRX. It offers a collection of rules that help identify common mistakes, improve code quality, and promote adherence to NGRX conventions and guidelines.
Whereas NGXS offers:
- @ngxs/cli – facilitates generating code from a schema.
- @ngxs/logger-plugin – a simple console log plugin to log actions as they are processed. It allows configuration to choose which actions not to log or define a custom logger instead.
- @ngxs/devtools-plugin – similar to @ngrx/store-devtools, this tool enables inspecting the application state and time-traveling.
Both libraries are powerful and offer substantial features. NgRx emphasizes a more pronounced separation of concerns model, while NGXS enables achieving the same business logic with less code. I hope that after reading this article, the choice of a proper Redux library has become clearer for your application! Whole code for both NgRx and NGXS is available here.