When implementing the user story to list top stories in the last chapter, the actual data is stored in RxJS observables. Although the implementation in the ItemService is very concise, it’s not very easy to understand, especially the usage of different RxJS operators. The combination of RxJS operators can be very complicated, which makes the debugging and testing very hard. It’s better to use explicit state management in the app. We’ll use NgRx for state management.
The Importance of State Management
You may wonder why the concept of state management is introduced in this chapter. Let’s start with a brief history of web applications. In the early days of the Internet era, the web pages were very simple. They just displayed static content and offered very limited user interactions. With the prevalence of Web 2.0 ( https://en.wikipedia.org/wiki/Web_2.0 ), web apps have evolved to take on more responsibility. Some business logic has shifted from the server-side to the browser-side. A typical example is the Gmail web app. These kinds of web apps may have complicated page flows and offer rich user interactions. Since these web apps usually have only one web page, they are called Single Page Applications, or SPAs. Ionic apps are a typical example of SPAs.
Global state: This state contains global data shared by all pages, including system configurations and metadata. This state is usually populated from the server-side when the app is loaded and rarely changes during the whole life cycle of the app.
State to share between pages and components. This state is necessary for collaboration between different pages and components. States can be passed between different pages using the router, while components can share states using ancestor components or custom events.
Internal state for components. Some components may have complicated user interactions or back-end communications, so they need to manage their own state. For example, a complex form needs to enable/disable form controls based on user input or results of validation logic.
With all these kinds of states, we can manage them in an ad hoc fashion. For example, we can simply use global objects for the global state and use component properties for the internal state. This approach works well for simple projects, but for most real-world projects, it makes it hard for developers to figure out the data flow between different components. A state may be modified in many different places and have weird bugs related to time. Error diagnosis and bug fixing could be nightmares for future maintainers when dealing with tightly coupled components.
The trend is to use a global store as the single source of an app state. This is the Flux architecture ( https://facebook.github.io/flux/ ) promoted by Facebook. There are several libraries that implement this architecture or its variants, including Redux ( https://redux.js.org/ ) and NgRx used in this chapter.
Introduction to NgRx
@ngrx/store – State management with RxJS for Angular applications.
@ngrx/effects – Side effects to model actions that may change an application state.
@ngrx/entity – Entity state adapter to manage records.
@ngrx/router-store – Integrate with Angular Router for @ngrx/store.
@ngrx/store-devtools – Dev tools for store instrumentation and time traveling debugging.
@ngrx/schematics – Scaffolding library for Angular applications.
Store – Store is a single immutable data structure that contains the whole state of the application.
Actions – The state stored in the store can only be changed by actions. An action has a mandatory type and an optional payload.
Reducers – Reducers are pure functions that change the state based on different actions. A reducer function takes the current state and the action as the input and returns the new state as the output. The output will become the new state and is the input of the next reducer function call.
Selectors – Components use selectors to select the pieces of data from the whole state object for rendering.
When the application’s state is managed by the NgRx store, components use data from the state object in the store to render their views. Components can dispatch actions to the store based on user interactions. These actions run through all reducers and compute the next state in the store. The state changes trigger the updates in the components to render with the updated data. Store is the single source of truth of the application’s state. It’s also very clear to track the changes to the state as all changes must be triggered by actions. With the powerful time-traveling debugging support of the @ngrx/store-devtools, it’s possible to go back to the time after any previous action and view the state at that particular time. By checking the state, it’s much easier to find the cause of bugs.
With an internal state separated out from components, components are much easier to implement and test. The view of a component is only determined by its input properties. We can easily prepare different inputs to verify its view in unit tests.
The global state in the store can be viewed as a big object graph. It’s typical for large applications to divide the state object by features, where each feature manages one part of the whole state object. It’s a common practice to organize features as Angular modules. Components can select any data from the state object for rendering. When the selected state changes, components are re-rendered automatically.
As we mentioned before, actions trigger state changes by running reducers. There are other actions that may have side effects. For example, loading data from the server may dispatch new actions to the store. If the data is loaded successfully, a new action with loaded data is dispatched. However, if an error occurred during the loading, a new action with the error is dispatched. NgRx uses effects to handle these kinds of actions. Effects subscribe to all actions dispatched to the store and perform certain tasks when some types of actions are observed. The execution results of these tasks are actions that are also dispatched to the store. In the data loading example, the effect may use Angular HttpClient to perform the request and return different types of actions based on the response.
Use NgRx
Now we are going to update the implementation of the top stories page with NgRx.
The first step is to define the state object for the application. This is usually an iterative progress. Properties in the state object may be added and removed during the whole development life cycle. It’s typical to divide the state object by features. The state of each feature depends on the requirements of components. For the top stories page, we’ll have two features. The first feature items is for all loaded items. This is because items are shared by different pages in the application. The second feature topStories is for the top stories page itself. The state of this feature contains ids of all top stories and pagination data.
When using NgRx, we have a convention for directory structure. TypeScript files of actions, reducers, and effects are put into the directories actions, reducers, and effects, respectively. Each module may have its own set of actions, reducers, and effects. The final application state is composed of states from all the modules.
Items Feature
Let’s start from the state of the feature items.
Define State
State of the feature items
Create Actions
Actions of the feature items
Create Reducers
After defining the state and actions, we can create reducers. For each action defined, the reducer function needs to have logic to handle it. A reducer function takes two parameters: the first parameter is the current state; the second parameter is the action. The return result is the next state. If a reducer function doesn’t know how to handle an action, it should return the current state. When the reducer function runs for the first time, there is no current state. We also need to define the initial state.
As shown in Listing 6-3, because the feature state in Listing 6-1 extends from EntityState in @ngrx/entity, we can use the function createEntityAdapter to create an EntityAdapter object that has several helper functions to manage the state. To create the adapter, we need to provide an object with two properties: the property selectId is the function to extract the id from the entity object; sortComparer is the comparer function to sort the entities if sorting is required. Here we use false to disable the sorting. The function getInitialState creates the initial state for the EntityState with initial values for additional properties loading and error.
Reducer of items
Add Effects
Effects of items
With the effect loadItems$, the action Load triggers actions LoadSuccess or LoadFail.
Note
It’s a convention for variables of Observables to use $ as the name suffix.
Top Stories Feature
To load items, we need to dispatch actions Load with ids of items. These ids are loaded from the Hacker News API of top stories ids. We also need to manage the state of top stories ids.
Define the State
State of top stories
Create Actions
Actions of top stories
Create Reducers
Reducer of top stories
Pagination
State and reducer function for pagination
Add Effects
Effects of top stories
State and reducers of top stories
Selectors
After finishing the states, actions, and reducers, we need to update the components to interact with the store. Components get data from the store for the view and may dispatch actions to the store. A component is usually interested in a subset of the global state. We use the selectors provided by NgRx to select states from the store. Selectors are functions that define how to extract data from the state. Selectors can be chained together to reuse the selection logic. One important characteristic of selector functions is that they memorize results to avoid unnecessary computations. Selector functions are typically defined in the same file as the reducer functions.
Selectors of the feature items
Selectors of top stories
getTopStoriesState selects the root state of the feature topStories.
getPaginationState selects the state of pagination.
getStoriesState selects the state of ids of top stories.
getStoryIds selects ids of top stories.
getDisplayItems selects the items to display by combining the results from getStoryIds, getItemEntities, and getPaginationState.
isItemsLoading selects the loading state of items.
getItemsError selects the error of loading items.
isTopStoriesLoadings selects the loading state of ids of top stories.
getTopStoriesError selects the error of loading ids.
getError selects the first error that occurred when loading items or ids.
Selectors for components
Update Components
Now we can update the TopStoriesComponent to use NgRx; see Listing 6-14. The implementation is completely different from the previous one using ItemService. TopStoriesComponent is injected with the Store from NgRx. TopStoriesComponent defines different Observables using different selectors. The observable items$ created by store.pipe(select(fromTopStories.getDisplayItems)) uses the selector getDisplayItems to select the items to display; itemsLoading$ represents the loading state of items; idsLoading$ represents the loading state of ids of top stories; errors$ represents errors. For the values in itemsLoading$, the function notifyScrollComplete is invoked to notify ion-infinite-scroll to complete. For the values in idsLoading$, functions showLoading or hideLoading are invoked to show or hide the loading spinner. For the values in errors$, the function showError is invoked to show the toast displaying error messages. The function doLoad dispatches the action topStoriesActions.Refresh or topStoriesActions.LoadMore depends on the type of loading.
Updated TopStoriesComponent using NgRx
Import NgRx modules
Import NgRx modules in the root module
Unit Testing
Unit tests of the reducer function
Testing effects is harder than reducer functions. Listing 6-18 is the unit test for ItemsEffects. To test this effect, we need to provide mocks for services Actions and AngularFireDatabase. TestActions is the mock for Actions that allows setting the dispatched actions, while dbMock is a mock object for AngularFireDatabase with the property object as a Jasmine spy. This is because ItemsEffects uses the method object of AngularFireDatabase, so we need to provide mock implementation for this method.
Unit test of effects
Use @ngrx/store-devtools
Summary
State management is an important part of any nontrivial applications. With the prevalence of Redux, developers are already familiar with concepts related to state management. NgRx is the best choice for state management of Angular applications. This chapter covers concepts like state, actions, reducers, effects, and selectors in NgRx and updates the top stories page to use NgRx to manage the state. In the next chapter, we’ll implement the user story to view web pages of top stories.