Managing State with ngrx

As you’ve just seen, handling state in an application can be tricky, especially when it comes time to optimize. In this section, you’ll learn about ngrx, a tool you can use to model all of your application state as a series of observable streams. The application’s state is centralized within ngrx, preventing rogue components from mistakenly modifying application state. This state centralization gives you precise control over how state can be modified by defining reducers. If you’ve ever used Redux, a lot of the same patterns apply.

Previously, applications would allow components to modify state without regard for the consequences. There was no concept of a “guard” in place to ensure that this centralized state could only be modified in approved ways. ngrx prescribes a set of patterns to bring order to this chaos. Specifically, a component emits a dispatch event when it wants to modify the application’s state. ngrx has a reducer function, which handles how the dispatched event modifies the core state. Finally, the new state is broadcast to subscribers through RxJS.

Joe asks:
Joe asks:
That Sounds Really Complicated

While that’s not a question, it’s a good point. State management tools like ngrx do require some forethought and setup. This extra work might not be worth it if you’re just building a simple form page. On the other hand, plenty of big web applications need to change state from many different components, and ngrx fits quite nicely in that case. It’s also important to remember that you don’t need to put everything into your state management system—sometimes a value is only needed in a single component, and storing the state there is just fine.

Installing ngrx

The first step is to install the basic building blocks of ngrx with npm install @ngrx/core @ngrx/store. @nrgx/core is required to use any of the tools in the ngrx project. @ngrx/store is the tool you can use to define this central state object and the rules around modifying it. ngrx has many more utilities, and I encourage you to check them out, but they are outside the scope of this book.

Defining Actions

In ngrx parlance, an Action is a defined way that the application’s state can be modified. In this case, the application has two different ways to modify the state: adding a list of patients (done in the initial load) and updating a single patient. Create a file (don’t use the ng tool) named state.ts and add the following lines at the top:

 import​ { Action } ​from​ ​'@ngrx/store'​;
 
 const​ ADD_PATIENTS = ​'ADD_PATIENTS'​;
 const​ UPDATE_PATIENT = ​'UPDATE_PATIENT'​;
 
 export​ ​class​ AddPatientsAction ​implements​ Action {
  type = ADD_PATIENTS;
 constructor​(​public​ payload) {}
 }
 export​ ​class​ UpdatePatientAction ​implements​ Action {
  type = UPDATE_PATIENT;
 constructor​(​public​ payload) {}
 }
 
 type PatientAction = AddPatientsAction | UpdatePatientAction;

Action types are defined as constant string. Nothing is stopping you from writing the string literal ’UPDATE_PATIENT’ through the entire application—this would work the same as importing the declared action from state.ts. However, having a centralized declaration of action names prevents typos and makes the intent of the code much clearer.

Then, there are two classes—one for each type of action. These classes implement Action, which means that they conform to the definition of Action and can be used anywhere one would expect an Action. Specifically, they are passed into the reducers you define in the next section. Finally, a type keyword declares a PatientAction, which is a new type that can be either of the two actions defined above.

This is a lot of boilerplate for such a simple application. In larger, more complex apps, this typing data acts as a bumper guard, ensuring that code that modifies state (one of the most bug-prone areas of an application) stays true to its original intentions. Now that you’ve defined how this application’s state can be modified, it’s time to implement these actions in a reducer.

Creating Reducers

We need to define just how these state changes work. In state.ts, we’ll define the patient reducer, a function that handles the UPDATE_PATIENT and ADD_PATIENTS actions. This application has only one type of state (an array of patients), but more complex apps have many different values stored in ngrx (user data, form elements, and the like).

export​ ​function​ patientReducer(state = [], action: PatientAction) {
switch​ (action.type) {
 case​ UPDATE_PATIENT:
return​ state.map((item, idx) =>
  idx === action.payload.index ? action.payload.newPatient : item
  );
case​ ADD_PATIENTS:
 return​ [...state, ...action.payload];
default:
 return​ state;
  }
 }

Every reducer takes two parameters—the current state and the action to modify that state. It’s good practice to include a default value for the state parameter, which becomes the initial application state. The action parameter might be undefined, when ngrx just wants to fetch the current state (this is why we have a default case in our switch statement). Otherwise, the action is defined as one of the two actions you defined in the previous section.

Speaking of the switch statement, eventually this reducer needs to handle several different types of state change events. Reducers commonly include a switch statement to help organize all of the different goings-on that might occur with their slice of the overall state.

Most importantly, we have the handler for the UPDATE_PATIENT event. In this case, it returns a new array of patients. This new array contains all the same patients as before, except for the one new patient containing the modified data from the event. The reducer returns a new array every time an action is dispatched with the UPDATE_PATIENT event. Every reducer should be a pure function, not modifying anything, but creating new arrays and objects when needed. Behind the scenes, ngrx uses object reference checks to determine what has changed. If our reducer modifies an object in place (and therefore, returns the same object reference when called), ngrx thinks nothing has changed and doesn’t notify listeners. (A common mistake: Object.assign(currentState, { foo: "bar" }). This just updates currentState and does not create a new object.) Function purity also allows tools like @ngrx/store-devtools to keep track of state change history while you’re debugging.

Somehow we need to tell the reducer about all the patients in the ward. The service that fetches the patients could loop through a succession of dispatches of UPDATE_PATIENT, but this is much simpler. When the ADD_PATIENTS event is emitted, a new array containing both the old and new patients is returned.

Every reducer should have a default case that returns the state as-is. This default is triggered in two cases: When the application is first initalized, ngrx calls all reducers without any parameters. In this case, the state parameter defaults to an empty array, and the switch returns the empty array as the initial state. The second case is when an action is dispatched to ngrx that this reducer doesn’t handle. While this won’t happen in this application, it’s common to have many reducers that handle all kinds of actions.

Plugging It All Together

Now the reducer can handle two kinds of state changes. The next step is to update the application itself to talk to this new state store.

Updating the Root Module

Before you modify any components or services, you need to register ngrx and the reducer you created with the root module. Open app.module.ts and make the following modifications:

 // Add imports
 import​ { StoreModule } ​from​ ​'@ngrx/store'​;
 import​ { patientReducer } ​from​ ​'./state'​;
 
 @NgModule({
  imports: [
  BrowserModule,
  ReactiveFormsModule,
StoreModule.forRoot({
  patients: patientsReducer
  })
  ],
  ... etc
 })

forRoot sets up the store object you’ve just used throughout the rest of the application. The argument defines store—every key is a key of the store, as defined by the reducer passed in as the value.

In this case, a single property (patients) is set to whatever your reducer returns. Initially, this reducer will run with an empty action to create the initial state of an empty array.

Updating Existing Components

Currently, all state changes go through patient-data.service.ts. While that service still needs to generate the data (or in a real-world scenario, fetch it from a server), it should not be responsible for maintaining the state through the life cycle of the application. Instead, we need the components to listen directly to their slice of the store and dispatch events directly to that store when a change is requested.

Updating the Patient Service

First, make sure that the state is populated with data once it’s been fetched. Components and services modify the state stored in ngrx by dispatching one of the actions you defined earlier. In this case, we want to dispatch the ADD_PATIENTS action and attach all of the patients generated in the service. The first thing to do is to import and inject the store service:

 constructor​(​private​ store: Store<any>) {

The <any> part of the Store definition can be used to provide a type for the application state itself. In this case, any is provided because the state is not complicated. ngrx knows what to inject, thanks to the forRoot call in the application module. Now that the store is provided in the service, you need to dispatch an action. Import AddPatientsAction from state.ts and dispatch it:

 this​.store.dispatch(​new​ AddPatientsAction(​this​.patients));

The store should be updated with the list of patients. Add a few log statements in patient-data.service.ts and state.ts until you’re sure you understand what’s happening. Once you’re confident about how actions are dispatched, it’s time to listen in to that data and display it on the page.

Listening in to Changes

At this point, ngrx initializes with an empty array and then populates with data generated by patient-data.service.ts. Next, the components need to listen in for changes to the state. Three components are involved here. Instead of reslicing state into rows each time, we can skip the row component and just use the display component, along with a few changes so that Bootstrap handles the rows for us. To allow this display, change the class on the root element in patient-display.component.html from row to col-xs-2. Update app.component.html to:

 <app-patient-display
 *​ngFor=​"let patient of patients$ | async"
 [​patient​]="​patient​"
 ></app-patient-display>

Now that the view code is out of the way, it’s time to learn how to pull data out of the store. We can pluck a given slice out of the store by injecting it and calling the select method on our store. select returns an observable that triggers whenever the chosen slice of the state changes.

 ngOnInit() {
 this​.patients$ = ​this​.store.select(​'patients'​);
 }

After you’ve entered all of this data, you’ll notice a curious bug pop up: no data appears on the page. Now, one could argue that this means the application is more performant than ever, but that probably won’t fly. The trouble here is a curious dependency resolution error. PatientDataService creates all of the patients, but only when it’s injected. Currently, the service is only injected into the patientDisplay component. The patientDisplay component is only rendered when it has patients to display. To compensate, inject PatientDataService into app.component.ts.

@ngrx/effects

images/aside-icons/note.png

In the future, you could move PatientDataService to an effect from the @ngrx/effects library.

Sending Data to Store

The final step in converting your application to use ngrx is to update the patientDisplay component to dispatch an event to the service, much like you did when initializing the patient list. The same steps apply: inject the store with the Store<any> annotation and dispatch an event through the event constructor defined in state.ts, this time UpdatePatientAction.

 constructor​(​private​ patientData: PatientDataService,
 private​ store: Store<any>) { }
 
 updateWard(newWard) {
 this​.patient.currentWard = newWard;
 this​.store.dispatch(​new​ UpdatePatientAction(​this​.patient));
 }

At this point, the application life cycle with ngrx has not functionally changed. However, the state of the application is gated and centralized. Anyone else who works on the application can clearly see the path a state change takes and know where and how to add new ways to modify state. It is much harder for the application to get into a bad state, since all interactions are clearly defined.

Additionally, you’ve fixed the massive performance bug; previously, reslicing the entire state into rows created a whole host of new arrays, bogging down performance. Now ngrx only recalculates when it absolutely needs to, speeding everything up.

As the application grows, keeping all of the actions and reducers in a single file might become a problem. When using ngrx in a production app, reducers are often kept in separate files along with their associated actions.

When ngrx Isn’t Helpful

Using a state management tool like ngrx comes with a cost—even small updates to state can come with extensive boilerplate. It’s probably overkill in this sample application. Even larger applications that mainly display data provided by a server might not have much to gain from ngrx. As with any major code change, make sure to do your research before you commit.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.254.192