Sunday, April 25, 2021

Ngrx Selectors

I look at selectors in Ngrx as queries on the data store. They are the method of getting data from the store to the components in the shape of an observable.

constructor(private store: Store) {
  this.dataobservable =  this.store.select(selectorfunction);
}

The thing to remember is that store.select passes the entire state to the selector function. Lets lay out a state structure, and see how selectors would work.

export const JobsFeatureReducer: ActionReducerMap<JobClientState> = {
  jobs: jobs.reducer,
  jobrelations: job_relations_entity.reducer,
  jobactions: job_actions_entity.reducer,
  jobfiles: job_files_entity.reducer,
  jobdispatch: job_dispatch_entity.reducer,
  jobperson: job_person_entity.reducer,
  jobquery: jobqueryreducer,
  jobprimary: primaryjobreducer,
};

and

export interface JobClientState {
    jobs: jobs.State;
    jobrelations: job_relations_entity.State;
    jobactions: job_actions_entity.State;
    jobfiles: job_files_entity.State;
    jobdispatch: job_dispatch_entity.State;
    jobperson: job_person_entity.State;
    jobquery: JobQuery;
    jobprimary: string;
   
}

Everything except the Query and JobPrimary are Entity State. 

export interface JobQuery {
    customer: string;
    jobnumber: string; //tid
    status: string;
}

JobClientState is a feature state, so any selector for this starts with

export const getJobState = createFeatureSelector<JobClientState>(jobFeatureKey);

Remember, this.store.select(selector) passes the entire state tree to the selector. If this wasn't a feature state, the first selector would look like this

export const getJobState = createSelector((state: AppState) => state=> state.Job)

where "Job" is jobFeatureKey. 

Where do I put this? The file structure of where you place your selectors is important. In this instance, the data from one entity will be needed by another to assemble the data, and if you aren't careful you can create a circular dependency between files. The solution is to build a tree. Create a file for each Entity or state property. Then create a file where the different entities or properties are combined. A tree.

Let's start with the jobs state selectors. This is an Entity state, which exposes selectors for the data. Entity state looks like this: 

{ ids: string[], entities: Dictionary<T>}

Dictionary is an object. You access the data with the id, and ids is an array of id. The id is derived from the object itself; you need a unique id for each entity. 

entity[id]

Entity selectors are generated by the schematic, and look like this

export const {

    selectIds,

    selectEntities,

    selectAll,

    selectTotal,

} = adapter.getSelectors();

With the feature selector, and the Entity selectors, we can then combine selectors and drill down to the data we want. So for the job state:

export const getJobs = createSelector(getJobState, (state) => state.jobs);

export const getJobIds = createSelector(getJobs, jobs.selectIds);

export const getJobEntities = createSelector(getJobs, jobs.selectEntities);

Each of the JobClientState properties that are Entity State will have the same type of selectors.

What about the Query and Primary states? Query is for the list of jobs to be displayed for the user to select. JobPrimary is the selected job, and has a similar selector.

export const getJobQuery = createSelector(getJobState, (state) => state.jobquery);

When a job is selected, the user navigates to an edit or view url, with the id. The router state is subscribed to, and the primaryjob state is set with that id. The view then uses this selector to get the selected job

export const getJobPrimary = createSelector( getJobState, (state) => state.jobprimary)

export const JobPrimaryEntity = createSelector( getJobPrimary, getJobEntities, (primary, entities) => entities[primary]);

As you can see, the JobPrimaryEntity returns the selected Job using the id as the property of the Job entity.

The other selectors use the primaryid to get the related files, actions, people, etc to build out the job view. To get the list of files attached to a Job.

export const JobPrimaryFiles = createSelector(
  getJobPrimary,
  getJobFileIds,
  getJobFileEntities,
  (primary, ids, entities) =>
      ids.filter(id => entities[id].jobid === primary).map(id => entities[id])
)

ids is an array, so you filter by the jobid, then map the list of ids to a list of entities. That gives a list of objects containing the job files to the component to render.

To limit the number of null checks required, make sure that you set up an initial state that will work if passed to the selectors.

If you have other states, such as contacts, or invoices that are connected to a job. Imagine you have a list of people connected to a job. The job state has this list in the form of contact ids. You can use the selectors from the Contact feature state to get this list of people connected to the job. All you have to do is ensure that the required contacts are loaded in the contacts state for the selectors to work.

Why so many selectors? In one word, memoization. The last computed value is stored, meaning the selector observable will not emit unless there is a change in the data. From the app state to each property, it is compared to the previous values, and will not emit unless it is different. What that means is that you can change a value in your contact state and the job selectors will not emit. 

I would suggest to not take shortcuts. Write out all the selectors and compose them. You will find that your code is robust to the inevitable refactors and spec changes that happen though the app development cycle. And it will perform well.

I have a simple rule that I follow with Ngrx and Angular; if I'm standing on my head to get something to work, I'm doing it wrong. If your selectors are complex and fragile, consider restructuring your state layout.



Friday, April 02, 2021

NGRX Effects

 "In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation."

In the Redux and Ngrx pattern, application state is modified one way only. An action is dispatched, and the reducer function returns a modified state. And action that does something other than that is a side effect, and in Ngrx, is handled by an Effect.

To work with Ngrx Effects you need two things.

  1. An understanding of how Effects work and what they can do.
  2. The ability to work with Rxjs and it's operators.
Lets start with Effects.

getinvoices = createEffect(() => 
    observable.pipe(),
    {dispatch: boolean})

An Effect subscribes to an observable, and merges the result into the Action stream. Or not, if you set dispatch to false.

That's it.

Not quite. There is initializing and some lifecycle hooks. https://ngrx.io/guide/effects

The magic happens in the Rxjs observable stream.

The most common usage is to subscribe to the action stream. This is what that looks like

import { Actions, createEffect, ofType } from '@ngrx/effects';

@Injectable()
export class AuthenticationEffects {
  constructor(private actions$: Actions) {}

  loginsucces$ = createEffect(() =>
    this.actions$.pipe(
      ofType(LoginSuccess),
      ...

The Actions observable stream is injected into the class, and this.actions$ is subscribed to in the effect. That means that all the actions dispatched in the app are emitted by the this.actions$ observable. 

To listen to a specific action, we could do something like this:

filter((action: Action) => action.type === 'the action type we want to find')

But we are provided with a far better solution, the ofType operator. We can list all the actions we want to listen to, or one. In this example, we are watching for LoginSuccess action. If the filter returns false, the observable chain stops, and waits for the next one.

What would you do if a user successfully logged in to the app? That action would likely have the user name, maybe an email address. So a reducer would listen for that action and update the authentication state. But there may be other side effects.
  • Navigate to a route.
  • Initialize a websocket service.
  • Fetch roles and permissions for that user from the api.
  • Load some common data for use in the app.
  • Notify the user.
Let's see how this would be done, and see some examples of RXJS operators that would be used.

loginsucceswebsocket$ = createEffect(() =>
    this.actions$.pipe(
        ofType(LoginSuccess),
        tap(() => this.wsService.initialize()
   ), {dispatch: false}
)

When logged in, initialize a service, in this case websocket. An action will not be merged into the action stream. This observable uses the tap operator, let's look at what it does.

Here we have what could be called nested side effects. Effects are a side effect to the action dispatch => reducer => modify state path. Tap is a side effect of the observable stream. Actions => filter the type => do something else without affecting the stream. Tap receives the data emitted, and allows you to do things without affecting the data stream. The next operator in line will receive the same data.

Observable streams seem like a black box full of magic. Tap exposes what is happening. This snippet inserted in the stream will tell you the data and gives a window into the black box.

observable.pipe(
   tap( value => console.log('value emitted', value,
        error => console.log('error', error),
        () => console.log('observable complete'))
)

Another use of tap is to navigate to a route. On login, I check to see if it is a new user or first use, and route to an initialize component to fill in some initial data. Note the use of the data contained within the action.

loginsuccessinit$ = createEffect(() =>
    this.actions$.pipe(
        ofType(LoginSuccess),
        filter(action => action.connectionstate.user.firstrun),
        tap((action: Action) => {    
          this.router.navigate(['initialization']);
          })
   ), {dispatch: false}
)

When the user logs in, some data is loaded into the state for use across the component tree. 

loginsuccessloaddata$ = createEffect(() =>
    this.actions$.pipe(
        ofType(LoginSuccess),
        map(() => LoadContacts())
    )
)

Notice this effect emits an action that is merged into the Action stream. The map operator is used here to change the value of the data in the stream. 

This is similar to Array.map() function, where each item in the array can be modified and returned. The observable map is passed each value that is emitted, and it returns the new value. You can confuse yourself to no end by doing something like this

const myarray = [ 1, 2, 3 ]
const arrayobservable = of(myarray).pipe(  // emits the array
            map((items: number[]) => items.map(item => item * 100))
).subscribe(value => console.log(value))  // [ 100, 200, 300 ] one emission

const arrayitemobservable = from(myarray).pipe(  // emits each item of the array
            map((item: number) => item * 100)
).subscribe(value => console.log(value))  //   100, 200, 300 , three emissions

You get the idea. map allows you to modify the data and return it. The next operator will see the new data.

What if we want to map the values from an observable, like an HttpClient call?

You would want to flatten the emissions of the inner observable, and merge them into the stream. There are four mergeMap variants. mergeMap, concatMap, switchMap and exhaustMap. With one emission they are functionally the same.

outerobservable$.pipe( mergeMap(value => this.httpclient(params)))

will map the value to what the inner observable emits in this case the httpclient call, and merge it back into the stream. The difference between the four are what happens when the outer observable emits a second value before the inner observable is finished. Think backpressure; there is data coming down the pipe that needs to be handled.
  • mergeMap will run the observable in parallel when the new emission arrives, merging the values of all of them into the stream as each one is completed.
  • switchMap will cancel the running inner observable and run the new one.
  • exhaustMap will throw away any new incoming emissions until the inner observable is completed.
  • concatMap will queue all the incoming emissions, doing them in sequence, letting each one  complete before running the next.
If you are getting the feeling that you need to know RXJS to use Effects effectively, you are right. There is one more operator you need to know, and you will use it when doing an httpclient call.

addrelations$ = createEffect(() => 
   this.actions$.pipe(
            ofType(LoadContacts),
            mergeMap(action => this.dataService.queryData(action)
                .pipe(
                    map(contacts => LoadContactsSuccess({contacts: contacts['Contacts']})),
                    catchError(e => of(RawErrorHandler(e)))
                )));

I've encapsulated the httpclient call in a service method that returns the observable. There are a few details that are important here.

The inner observable has a pipe. You can nest piped operators to your hearts content, but this one has a specific purpose. It could be written like this:

addrelations$ = createEffect(() => 
   this.actions$.pipe(
            ofType(LoadContacts),
            mergeMap(action => this.dataService.queryData(action)),
            map(contacts => LoadContactsSuccess({contacts: contacts['Contacts']})),
            catchError(e => of(RawErrorHandler(e)))
   )
);

The http call response gets merged back into the stream, mapped to the desired shape in an Action, and the error handled.

The only problem is that the error would end the subscription to this.actions$, and your action would work once. It is possible to configure Ngrx so this gets resubscribed, but this is important to know if you have long and deep observable sequences.

Rxjs makes easy things hard and hard things easy. Same with Effects. An app that does a simple api call and renders the results gets complicated with the Action => Effect => reducer => selector cycle. But if you have multiple data sources, multiple components rendering the data, complex component to component interactions, things like autosave and undo-redo, network fault tolerance etc. many of these complicated things become easy, almost trivial with the power and flexibility of Ngrx. But you need to be comfortable using Rxjs.


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]