Ngrx store freeze reveals problems with shared state

Ngrx store freeze reveals problems with shared state

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 show how to ensure your code doesn’t modify the application state without using the dispatcher and your reducers.

Don’t modify state outside dispatch reducers

Ngrx store keeps track of your application state, holding on to the current state and returning the current state objects when requested. For performance reasons it does not return a new copy each time a state property is requested. What this means is that you can modify nrgx store state just by updating the object you subscribe to from the store. However, when you do this you break the store, subscribers won’t know about the update, your updates can be overwritten by dispatch calls, and you have a jumbled mess of inconsistent state changes that may or may not be compatible. Subtle or major bugs creep in and make your app behave improperly since different components won’t all know about the same state. This is as bad or maybe much worse than not using ngrx store at all.

Introducing store freeze, your app store’s salvation

So how do you know if you accidentally modified state outside of your dispatched reducer calls? Luckily we have ngrx-store-freeze, a simple javascript reducer function to lock items in the store from edit. Once its in the store, it cannot be edited except by creating a new object passed in through one of your state reducers. Hooking in the ngrx-store-freeze is easy.

Step 1, add the node package to package.json. On the command line enter “npm install ngrx-store-freeze –save” or if you’re using visual studio just open the package.json and enter the package to the list and it will auto-install.

"dependencies": {
    //... other packages omitted for clarity
    "ngrx-store-freeze": "^0.1.9", //pick whatever version is latest, this is latest as of today.
},

Step 2, update your app wide reducer to include the storeFreeze reducer when in development mode.

// app/app.store.ts - some code omitted from the sample for clarity
import { storeFreeze } from 'ngrx-store-freeze';
import { compose } from "@ngrx/core";

const developmentReducer: ActionReducer<AppState> = compose(storeFreeze, combineReducers)(reducers);
const productionReducer: ActionReducer<AppState> = combineReducers(reducers);

export function AppReducer(state: any, action: any) {
    if (environment.enableStoreFreeze && !environment.production)
        return developmentReducer(state, action);
    else
        return productionReducer(state, action);
}

‘environment’ is a constant value with a couple properties set based on the build process. Check the code for how that works, implementation will vary between the angular cli and systemjs.

Step 3, Include ngrx-store-freeze in your deployment (accessible for development mode – non-production). This sample project uses systemjs, so the files need to be deployed to a dist folder on gulp build and then registered on the page in your systemjs configuration.

//gulpfile.js
gulp.task("dist-libs", () => {
    gulp.src([
            //... other libs omitted for clarity
            'deep-freeze-strict/**',
            'ngrx-store-freeze/**',
    ], {
        cwd: "node_modules/**"
    })
        .pipe(gulp.dest('./dist/libs'));
});
//NgApp.cshtml (or your main app html file)
        System.config({
            packages: {
                //... other libs omitted for clarity
                'deep-freeze-strict': {
                    main: '/index.js',
                    defaultExtension: 'js'
                },
                'ngrx-store-freeze': {
                    main: '/dist/index.js',
                    defaultExtension: 'js'
                }
            },
            map: {
                //... other libs omitted for clarity
                'deep-freeze-strict': '/dist/libs/deep-freeze-strict',
                'ngrx-store-freeze': '/dist/libs/ngrx-store-freeze'
            }
        });
        System.import('app/app.module.js').catch(function (err)
        {
            console.error(err);
        });

Your page should now continue to work as it did before. however, you may suddenly have some run-time issues related to improperly mutating state.

Showing freeze in action

In the case of this sample app, there were no issues at this point as state was not being modified outside of the reducers. If your app was, you would now see an error as you load the problem causing component.

Add ngx-datatable to try to break the freeze

For the sake of this sample, I added ngx-datatable as a replacement for the plain html table in the person search results. This causes a state modification error since ngx-datatable (as of version 6.3) modifies each row by adding an $$index property to uniquely identify each row.

After the table is switched over and the page reloaded, you get this error:

EXCEPTION: Error in ./DatatableComponent class DatatableComponent - inline template:29:8 caused by: Can't add property $$index, object is not extensible

Solution: clone the state

Components that modify local state shouldn’t be given direct references to the store’s state, but rather give them a copy. You can easily do this, continuing from my last article, by adding a new property to the person orchestrator service that returns a different observable with a copy of the state results list.

export class PersonOrchestratorService {
    //... rest omitted for clarity
    get results(): Observable<Person[]> {
        return this._store.select((s: AppState) => s.person.results);
    }
    get resultsCopy(): Observable<Person[]> {
        return this.results
            .map(obs => {
                //This is needed because the ngrx-datatable modifies the result to add an $$index to each result item and modifies the source array order when sorting
                return obs.map(v => JSON.parse(JSON.stringify(v))); //TODO: convert to use lodash deep clone
            });
    }
}

Now in the search component, get the results observable from this new property.

export class PersonSearchComponent {
    //... others omitted for clarity
    public results$: Observable<Person[]>;

    constructor(private _service: PersonOrchestratorService) {
        //... rest omitted for clarity
        this.results$ = this._service.resultsCopy;
    }
}

Now when you re-run the page the search results will load upon initiating the search. Why does this work? When the store receives a state change with search results, that newly saved state is frozen and that object instance and the items in the array cannot be modified. When the ‘resultsCopy’ observable changes with the new results it then takes a copy of those array items into a new array that is not frozen. Essentially this copy can be modified all the component desires. But be careful, that’s just for local display changes, and real changes invoked by the user should still be sent to the reducer.

Updates

Two way NgModel bindings break under freeze

Typical Angular examples usually show a form component using “bananas-in-a-box” [(ngModel)] bindings to your model. This can be a problem when that form is bound to an input supplied directly from the store.

//... portion of person-search-form.component.html
<input type="text" class="form-control" [(ngModel)]="criteria.term" name="term"
    placeholder="Name or phone number"
    (keyup.enter)="search()">
<button class="btn btn-primary" (click)="search()">Search</button>

This two-way form binding will attempt to update the criteria in the component as defined here.

//... portion of person-search-form.component.ts
export class PersonSearchFormComponent {
    @Input() criteria: PersonCriteria;
    @Output() submitSearch: EventEmitter<PersonCriteria> 
      = new EventEmitter<PersonCriteria>();
    // .. other code emitted for clarity
    search() {
        console.log("search pressed", this.criteria);
        this.submitSearch.emit(this.criteria);
    }
}

The parent component retrieves the state from the store and passes it directly to this input with the async pipe, sending any update from the store into this component at all times. Without freeze enabled this form will mutate the state in place without calling the reducers until the search button is pressed.

How does enabling freeze tell us about this problem?

Well it isn’t ideal, since it does not throw an exception as it does when properties are added. This is because at this time, ‘ngrx-store-freeze’ relies on ‘deep-freeze-strict’, which just calls the browser implemented ‘object.freeze’ recursively. Unfortunately without all scripts that modify the object properties being in a ‘use strict’ block, no exception is thrown by the frozen object. This is majorly disappointing, and it may be appropriate for the ‘ngrx-store-freeze’ to implement its own non-browser optimized version of object freeze to give a better developer experience. This is only used in development mode after all, so a performance hit for the benefit of good errors would be ideal.

The way we know that the store is broken is that edits to this form do not update the criteria object in the search component at all. So when I press search, it is as if none of my changes in the form fields took effect. The result is that I get improper search results, which in most or all cases will be less likely to be noticed than an exception in the console.

How do you fix an NgForm to stop modifying state directly

Fortunately there is a great solution to this. Stop using two way bound forms and use one way binding instead, or use reactive forms. I’ll show the one-way forms option here. The first change is to update your form view to be one-way bound.

//person-search-form.component.html
<form id="personSearchForm" #ngForm="ngForm" class="panel" (submit)="submitSearch.emit(ngForm.form.value)" (reset)="resetSearch.emit()">
    <div class="panel-body">
        <div class="form-inline">
            <div class="form-group">
                <input type="text" class="form-control" name="term" [ngModel]="criteria.term"
                       placeholder="Name or phone number">
            </div>
            <button class="btn btn-primary" type="submit">Search</button>
            <button class="btn btn-default" type="reset">Reset</button>
        </div>
    </div>
</form>

On line 6, notice the bananas-in-a-box is now a plain box”[ngModel]” one-way binding. The form will be populated with the initial value of the model, but no updates flow directly back into the model, hence the store will not be manipulated. The form is also updated to include an #ngForm=”ngForm” which will make a viewchild for the form itself available in the component code.

//person-search-form.component.ts
import { Component, Input, Output, EventEmitter, ViewChild, AfterViewInit } from '@angular/core';
import { PersonCriteria } from '../person.model';
import { NgForm } from '@angular/forms';

@Component({
    selector: 'person-search-form',
    templateUrl: '/dist/js/person/search/person-search-form.component.html',
})
export class PersonSearchFormComponent implements AfterViewInit {
    @Input() criteria: PersonCriteria;
    @Output() submitSearch: EventEmitter<PersonCriteria> = new EventEmitter<PersonCriteria>();
    @Output() resetSearch: EventEmitter<void> = new EventEmitter<void>();
    @Output() criteriaChange: EventEmitter<PersonCriteria> = new EventEmitter<PersonCriteria>();
    @ViewChild('ngForm') ngForm: NgForm;
    
    ngAfterViewInit() {
        this.ngForm.form.valueChanges.debounceTime(50).subscribe(this.criteriaChange);
    }
}

The addition of a ViewChild property named ‘ngForm’ on line 15 lets us hook into change events in the form and then pass those changes to the store via a dispatcher. Line 18 subscribes to the changes in the form and sends all such changes to the parent component via an emitter. If the user is typing fast it waits to send those updates until the user has stopped for 50 milliseconds. You can set this to any amount of time that makes sense for an amount of data not to lose if they switch to another state component in the app (which disposes of your form). Since the html template sends the latest form value along with the search emit, there won’t be any data missing issues if they type non-stop fast and hit enter to submit before the debounce time elapses. the debounce save only affects saving of yet unsaved partial edits when the user switches views.

The parent search component simply responds to the criteria change emitter by sending the value to the store with the orchestrator service. You may also dispatch to the store directly if you don’t like the concept of an intermediate orchestrator service.

//partial person-search-form.component.ts - some portions emitted for clarity
export class PersonSearchComponent {
    public criteria$: Observable<PersonCriteria>;

    constructor(private _service: PersonOrchestratorService) {
        this.criteria$ = this._service.criteria;
    }

    public criteriaChange(criteria: PersonCriteria): void {
        this._service.changeSearch(criteria);
    }
    public criteriaSubmitted(criteria: PersonCriteria): void {
        this._service.search(criteria);
    }
}

Moving Forward

Enabling a freeze of objects in your store will help you find places in your code that are improperly modifying a direct reference to store properties received from an Input decorator. We’ve learned how to guard in cases where some components may be written to modify their inputs by giving a copy of the object as the input value, and that we should use one-way bound forms or reactive forms instead of two-way bound forms.

This sample has another case to consider with freeze, the item detail edit view. The component is guarded from modifying the store as is, but it may not be the best method. A future article will discuss alternate options around form binding.

Freeze can be a performance expensive operation depending on your application, so we learned how to make sure it is only done in development mode.

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/80f7fa1bd2fe503206b5e8e006de449ab0978f26

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

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

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

Leave a Comment

Your email address will not be published.