DEV Community

Cover image for Announcing NGXS 3.7
markwhitfeld for Ngxs

Posted on • Updated on

Announcing NGXS 3.7

2020 has definitely been a year of ups and downs! As open source maintainers, the shock out of normality has definitely brought its challenges... with work invading the home, family invading work, and every last drop of our day being caught up in the chaos.

This update has been a long time in the making, with lengthy discussions around each new feature, and great efforts to ensure the stability and backward compatibility of the library and API, following the big changes in Angular and in TypeScript this year.

Thank you to our incredible community for your ongoing contributions, enthusiasm, and support!

Now, let's get to the release...

Overview

  • 🚀 Official Angular 10 Support
  • 🛑 Improved Error Messages
  • 🐛 Bug Fixes
  • 🔌 Plugin Improvements and Fixes

Official Angular 10 Support

Angular 10 brought many behind the scenes improvements to the library we all know and love. Updates to the underlying tooling, upgrades to versions of library dependencies (TypeScript, etc.), and further improvements to Ivy and bundle sizes.

The day that Angular 10 was released, we were ready to announce to the world that we fully supported the new version... but alas we discovered that Angular 10 had issues with HMR.

The @ngxs/store library and all of the other plugins supported Angular 10 out of the box, but the HMR plugin was not so lucky. Our commitment to the stability of the library extends to all of the core plugins, including the HMR plugin.

We tried to work around the issue, but unfortunately, there was nothing we could do and the issue wasn't receiving much attention from the Angular team. As a result, we have decided to deprecate the HRM plugin. More on this later...

Improved Error Messages

Sometimes a developer can miss something small in their app that can result in obtuse and hard to debug issues. We have improved some of our error detection and messaging in order to give better feedback to the developer about where they may have gone wrong.

Here are some of the scenarios that we covered:

Empty type Property on Action (PR #1625)

For example, if you define an action that has a type property but its value is not set, then typescript is happy and the app compiles, but the action handlers cannot correctly determine the type of action.

Now you will receive a convenient message notifying you of the action that has an empty type property.

Incorrect Arguments for ofAction* operator (PR #1616)

The ofAction* pipeable operators previously had a pretty open typings for the argument definition. We have improved this typing so that it only accepts valid action types.

Bug Fixes

Relax @Select type check (PR #1623)

In NGXS v3.6 we added a typescript trick that would show an error if the declared type of the variable decorated by the @Select decorator didn't match the type of the selector referenced by the decorator. Unfortunately, this prevented the use of this decorator with private or protected fields. Due to this being a regression over NGXS v3.5, we reverted this change.

Handle Empty Observables Correctly (PR #1615)

Previously, if an observable returned from an @Action function completed without emitting any values then this would be seen as a cancellation. This type of returned observable is entirely valid, and therefore we adjusted the internal processing of observables to accept an empty observable as a valid completion scenario.

Plugin Improvements

Logger Plugin

Feature: Action Filter in Logger Plugin (PR #1571)

The logger plugin did not have an option to ignore specific actions. It either logged every action or, when disabled, did not log any action at all. However, you may need logging actions conditionally because of several reasons like:

  • Some actions are not your focus and logging them as well makes it hard to find what you are actually working on.
  • Some actions are simply fired too often and the console becomes cumbersome.
  • You want to log an action only when there is a certain state.

With this version, the forRoot method of the NgxsLoggerPluginModule takes a filter option, which is a predicate that defines the actions to be logged. Here is a simple example:

import { NgxsModule, getActionTypeFromInstance } from '@ngxs/store';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { SomeAction } from './path/to/some/action';

@NgModule({
  imports: [
    NgxsModule.forRoot([]),
    NgxsLoggerPluginModule.forRoot({
      filter: action => getActionTypeFromInstance(action) !== SomeAction.type
    })
  ]
})
export class AppModule {}

In this example, the SomeAction action will not be logged, because the predicate returns false for it. You may pass more complicated prediactes if you want and even make use of current state snapshot in your predicates:

import { NgxsModule, getActionTypeFromInstance } from '@ngxs/store';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { SomeAction } from './path/to/some/action';

@NgModule({
  imports: [
    NgxsModule.forRoot([]),
    NgxsLoggerPluginModule.forRoot({
      filter: (action, state) =>
        getActionTypeFromInstance(action) === SomeAction.type && state.foo === 'bar'
    })
  ]
})
export class AppModule {}

The predicate given in this example lets you log only SomeAction and only when foo state is equal to 'bar'. This makes it easier to pinpoint a dispatched action while debugging it.

Important Note: The predicate will be called for every action. This may cause performance issues in development, especially when you are planning to keep the predicate after debugging. Therefore, please consider using a memoized function for filters more complicated than a simple action comparison. You may take advantage of memoization libraries for that.

Storage Plugin

Feature: Serialization Interceptors in Storage Plugin (PR #1513)

You can define your own logic before or after the state get serialized or deserialized.

  • beforeSerialize: Use this option to alter the state before it gets serialized.
  • afterSerialize: Use this option to alter the state after it gets deserialized. For instance, you can use it to instantiate a concrete class.
@NgModule({
  imports: [
    NgxsStoragePluginModule.forRoot({
      key: 'counter',
      beforeSerialize: (obj, key) => {
        if (key === 'counter') {
          return {
            count: obj.count < 10 ? obj.count : 10
          };
        }
        return obj;
      },
      afterDeserialize: (obj, key) => {
        if (key === 'counter') {
          return new CounterInfoStateModel(obj.count);
        }
        return obj;
      }
    })
  ]
})
export class AppModule {}

Form Plugin

Feature: Reset Form Action (PR #1604)

You can reset the form with the ResetForm action.

  • This action resets the form and related form state.
  • The form status, dirty, values, etc. will be reset by related form values after calling this action.

Example:

<form [formGroup]="form" ngxsForm="exampleState.form">
  <input formControlName="text" /> <button type="submit">Add todo</button>

  <button (click)="resetForm()">Reset Form</button>
  <button (click)="resetFormWithValue()">Reset Form With Value</button>
</form>
@Component({...})
class FormExampleComponent {
  public form = new FormGroup({
    text: new FormControl(),
  });

  constructor(private store: Store) {}

  resetForm() {
    this.store.dispatch(new ResetForm({ path: 'exampleState.form' }));
  }

  resetFormWithValue() {
    this.store.dispatch(
      new ResetForm({
        path: 'exampleState.form',
        value: {
          text: 'Default Text',
        },
      }),
    );
  }
}

Improvement: Simplify ngxsFormClearOnDestroy Attribute (PR #1662)

The ngxsFormClearOnDestroy attribute previously required its declaration on the form element to be exactly [ngxsFormClearOnDestroy]="true" to work.
Since this is a simple boolean attribute, the mere presence of the attribute on the form element should imply the behavior. We have improved this attribute to recognize all valid forms of specification.

For example, you can now just include the attribute like this:

<form [formGroup]="form" ngxsFormClearOnDestroy ngxsForm="exampleState.form">
  <input formControlName="text" />
</form>
@Component({...})
class FormExampleComponent {
  public form = new FormGroup({
    text: new FormControl(),
  });

  constructor(private store: Store) {}  
}

This form will be cleared once the component is destroyed. Win!

HMR Plugin

Deprecation

As mentioned above, Angular 10 has issues with HMR, and these issues prevented us from announcing official support from day 1.

After many attempts to get it working again, we admitted defeat and had to make some hard decisions. After consulting with the community, we have decided to deprecate the HMR plugin until there is official Angular support for the HMR paradigm again. This goes against some of our fundamental philosophies that make the NGXS ecosystem such a reliable choice for your application, but in this case, we are forced by things that are outside of our control.

Here is our poll to the community on our slack channel:
Slack HMR Plugin poll

The results were the following:

  • 💥 73% voted in favor of dropping the HMR plugin
  • 🙏 2% voted to keep the plugin
  • 🤷‍♀️ 25% didn't mind either way

Upon discussion with the 2%, they were not aware that the storage plugin could be used to gain an almost identical experience and then supported the deprecation too. We have included details about this workaround in our docs.


Some Useful Links

If you would like any further information on changes in this release please feel free to have a look at our changelog. The code for NGXS is all available at https://github.com/ngxs/store and our docs are available at http://ngxs.io/. We have a thriving community on our slack channel so come and join us to keep abreast of the latest developments. Here is the slack invitation link.

Top comments (0)