Orchestrating ngrx store access, rest API calls, and front-end logic

Orchestrating ngrx store access, rest API calls, and front-end logic

This article builds on the prior series showing how to create an application using Angular 2 or later, and leverage ngrx store for application state.  The article will extend the same project by moving application logic to a central class for business logic, defend against API changes, and reduce setup of test dependencies.

Problem: Calculated values in components

In the starting code before this article, some state calculations are determined in multiple places. In a larger, more complex application, this problem may be much larger, and some calculations may be duplicated in multiple components.  This example is simple, but its not hard to imagine that you may have much more complicated calculations around state.

//person-search.component.ts
this.hasResults$ = this.searchStatus$.select((s: SearchStatus) => 
    s === 'complete' || s === 'empty');

Imagine if a new status was added and this line needed to be updated, you would then need to find all places that used a searchStatus and ensure the calculation was correct in each case. To guard against missing the updated code in each place, you should have a unit test wrapping each bit of business logic, including this check, meaning you need to test this component to ensure the proper value of hasResults.

The code prior to any changes in this article can be found here:
https://github.com/michael-lang/sample-ng2-mvc/tree/dbdd7f815919a1989b431283a71106b9bbe795b8

Fix: moving logic to a service class

By moving logic to a service class, or just a static method elsewhere you can move the common logic to one place and call it from anywhere that needs it. Since this common logic is using the store, you need to inject the store, so our choice here it to put it into a service so that Angular does the injection for us.

This logic could be moved into the current PersonService class, but that is not ideal. The PersonService already manages the calls to a REST service, handling the Http call and response into an Observable. Adding logic to be tested into that service not only increases the overhead in creating tests around the calculation, but also violates the single responsibility principle. Each class should be responsible for only one thing, and PersonService‘s one thing is to make the Rest calls for the person endpoint (get, search, save).

A PersonOrchestratorService is more along the lines of what is needed here, although you can name it whatever you want. Since it interacts with the store and the REST API, both are injected in the constructor.

//person-orchestrator.service.ts
/**
 * Orchestrates the interactions with the REST API, business logic, and access to the local store.
 */
@Injectable()
export class PersonOrchestratorService {
    constructor(private _store: Store<AppState>, private _itemService: PersonService) { }

    get searchStatus(): Observable<SearchStatus> {
        return this._store.select((s: AppState) => s.person.searchStatus);
    }
    get hasResults(): Observable<boolean> {
        return this.searchStatus.select((s: SearchStatus) => s === 'complete' || s === 'empty');
    }
    // ...omitted some code for clarity } 

Now the components that had a copy of this logic for hasResults can now get it from the same central place.

export class PersonSearchComponent {
    public hasResults$: Observable<boolean>;

    constructor(private _service: PersonOrchestratorService) {
        this.hasResults$ = _service.hasResults;
        // ... omitted some code for clarity
    }
}

Problem: store logic in the REST API

In the starting code before this article, the PersonService was converted from being a simple REST API consumer to also include telling ngrx store when search started, search completed, and items were inserted or updated. This was very possibly a mistake, violating the single responsibility principle. The alternative was having the component dispatch each of those actions as needed, also not ideal, hence the experiment to put it in the service.

Fix: Store and REST calls coordinated in a dedicated logic class

The PersonOrchestratorService is the best place to add the logic around dispatching the store updates since it fits in the same single responsibility.

//person-orchestrator.service.ts
export class PersonOrchestratorService {
    constructor(private _store: Store<AppState>, private _itemService: PersonService) { }
    // ... omitted some code for clarity

    public search(criteria: PersonCriteria): void {
        this._store.dispatch(new PersonSearchAction(criteria));
        this._itemService
            .search(criteria)
            .subscribe(
            (persons: Person[]) => { this._store.dispatch(new PersonSearchCompleteAction(persons)); },
            error => console.log(error)
            );
    }
}

Problem: Creating a combined observable in a component

The PersonComponent requires a list of tabs to send to the TabsetComponent, but the tabs are not in the store. The store contains a list of open items (people), which does not include the search tab. The tabs also require other computed properties on a tab not defined as part of the item (person). I would rather not store the same data in multiple places of the store state just for display in different components. Also, keeping the creation of tabs from open items in the PersonComponent makes it a little harder to test, and impossible to reuse the logic if I needed that list for display elsewhere.

Fix: Create combined observables in a service

Moving the calculated tabs list observable is easily added to the new PersonOrchestratorService. The code is almost identical, except updated references to where the two source observables come from.

//person-orchestrator.service.ts
export class PersonOrchestratorService {
    constructor(private _store: Store<AppState>, private _itemService: PersonService) { }
    // ... omitted some code for clarity

    get openList(): Observable<PersonHolder[]> {
        return this._store.select((s: AppState) => s.person.openList);
    }
    get activeTabId(): Observable<string> {
        return this._store.select((s: AppState) => s.person.activeTabId);
    }

    get tabs(): Observable<Tab[]> {
        let searchTab = new Tab();
        searchTab.id = 'tab-id-search';
        searchTab.title = 'Search';
        searchTab.template = 'search';
        searchTab.active = true;
        searchTab.closeable = false;
        searchTab.itemId = '';
        return this.openList
            .combineLatest(this.activeTabId)
            .map(([people, activeId]) => {
                searchTab.active = activeId === searchTab.id || !activeId;
                return [searchTab].concat(people.map(item => {
                    let exists = item.Person && item.Person.PersonId && item.Person.PersonId.length > 0;
                    let t = new Tab();
                    t.id = item.PlaceholderId;
                    t.title = exists ? item.Person.LastName + ' ' + item.Person.FirstName : 'Add Person';
                    t.template = 'item';
                    t.active = activeId === t.id;
                    t.closeable = true;
                    t.item = item;
                    t.itemId = item.Person.PersonId;
                    return t;
                }));
            });
    }
}

Moving forward

The next step in this project is to apply this pattern to the other states of this application, Locations and Trips. The code changes for Location will be almost identical, however Trips will be a little more involved and will reap more benefits. A trip combines data from location and person as sub-objects, which will show more of the power of summed up observables. For more detail, review Lukas’s post on the topic noted in further reading below.

In the future when ngrx-store-freeze is added to prevent improper state manipulation outside of the reducer logic, we may find other uses for this new orchestrator service in between the component layer and the store. Look for this in the next article of this series.



Further Reading

Full latest source code for this article series
https://github.com/michael-lang/sample-ng2-mvc

The code version locked after this article can be found at this commit:
https://github.com/michael-lang/sample-ng2-mvc/tree/f59f4dbb5ccb5d6c29159dcbdb4a98ae9d8111ed

Lukas Ruebbelke: Handle Multiple Angular 2 Models in ngrx with Computed Observables
http://onehungrymind.com/handle-multiple-angular-2-models-ngrx-computed-observables/

How to build a real world Angular app with ngrx store, Part I: Define the state

How to build a real world Angular app with ngrx store, Part II: Components using Store

Evolving Angular to remember unsaved changes

Ngrx without a switch statement

 

About the Author

Michael Lang

Co-Founder and CTO of Watchdog Creative, business development, technology vision, and more since 2013, Developer, and Mentor since 1999.

See the about page for more details.

1 Comment