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.



Comments: Post a Comment

Subscribe to Post Comments [Atom]





<< Home

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

Subscribe to Posts [Atom]