@DataAction

@DataAction() - This decorator emulates the execution of asynchronous or synchronous actions. Actions can either be thought of as a command which should trigger something to happen.

Before (origin NGXS behavior)

export class AddTodo {
    public static type = '[Add todo]';
    constructor(public payload: string) {}
}

@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @Action(AddTodo)
    public add({ setState }: StateContext<string[]>, { payload }: AddTodo): void {
        setState((state) => state.concat(payload));
    }
}
@Component({
    selector: 'app',
    template: `
        <input #inputElement />
        <button (click)="addTodo(inputElement.value)">Add todo</button>
    `
})
class AppComponent {
    constructor(private store: Store) {}

    public addTodo(value: string): void {
        this.store.dispatch(new AddTodo(value));
    }
}

After (Data-plugin behavior)

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public add(@Payload('todo') todo: string): void {
        this.ctx.setState((state) => state.concat(todo));
    }
}
@Component({
    selector: 'app',
    template: `
        <input #inputElement />
        <button (click)="todo.add(inputElement.value)">Add todo</button>
    `
})
class AppComponent {
    constructor(private todo: TodoState) {}
}

The method todo.add(payload) is the same as store.dispatch({ type: '@todo.add', todo: payload }).

Also you can invoke simple setState action:

@Component({
    selector: 'app',
    template: `
        <input #inputElement />
        <button (click)="todo.setState(inputElement.value)">Add todo</button>
    `
})
class AppComponent {
    constructor(private todo: TodoState) {}
}

The method todo.setState(payload) is the same as store.dispatch({ type: '@todo.setState($arg0)', payload: todo }).

What are the benefits?

  • No need to create action classes

  • Typing improvements for state context

  • Explicit interaction with states

Don't use nested actions

Bad

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public add(@Payload('todo') todo: string): void {
        // bad (action in action)
        this.setState((state) => state.concat(todo));
    }
}

Good

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public add(@Payload('todo') todo: string): void {
        this.ctx.setState((state) => state.concat(todo));
    }
}

Bad

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public add(@Payload('todo') todo: string): void {
        // bad (action in action)
        this.concat(todo);
    }

    @DataAction()
    private concat(@Payload('todo') todo: string): void {
        this.ctx.setState((state) => state.concat(todo));
    }
}

Good

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public add(@Payload('todo') todo: string): void {
        this.concat(todo);
    }

    private concat(todo: string): void {
        this.ctx.setState((state) => state.concat(todo));
    }
}

What difference between this.ctx.setState and this.setState?

In order to understand the difference, you need to give some examples:

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {}

@Component({
    /* ... */
})
class TodoComponent {
    constructor(private todoState: TodoState) {}

    public addTodo(todo: string): void {
        this.todoState.setState(todo);
    }
}

setState - this is a public method of the NgxsDataRepository class, it is annotated by the action decorator. This means that when it is called, an action will be registered into Store and will be called dispatch from Store. Thus you will see the state of the changed state through the logger or devtools plugins. When you call setState then it calls the ctx.setState method from state context.

State Context provides a way to pass data through the global states tree without having to pass new state manually at every level.

However, there is an unpleasant moment here, if you call the context directly, then you may not see anything in the devtools, because the context will be called immediately without dispatching states:

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    public addTodo(todo: string): void {
        this.ctx.setState(todo);
    }
}

@Component({
    /* ... */
})
class TodoComponent {
    constructor(private todoState: TodoState) {}

    public addTodo(todo: string): void {
        // When you make a call, the state will be changed directly,
        // without notifying the NGXS lifecycle services
        // This is a bad approach!
        this.todoState.addTodo(todo);
    }
}

Therefore, the context should only be called inside the action.

// GOOD

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public addTodo(@Payload('todo') todo: string): void {
        // call context from under the action
        this.ctx.setState(todo);
    }
}

// GOOD

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    public addTodo(@Payload('todo') todo: string): void {
        // call context inside another action
        this.setState(todo);
    }
}

However, there are very difficult situations in which you yourself need to determine which method should be an action and which should not:

@StateRepository()
@State<PersonModel>({
    name: 'person',
    defaults: { title: null, description: null }
})
@Injectable()
export class PersonState extends NgxsDataRepository<PersonModel> {
    constructor(private readonly personService: PersonService) {
        super();
    }

    @DataAction()
    public getContent(): Observable<PersonModel> {
        return this.personService.fetchAll().pipe(tap((content: PersonModel): void => this.ctx.setState(content)));
    }
}

In this situation, we have some problem:

@Component({
    /* ... */
})
class PersonComponent implements OnInit {
    constructor(private personState: PersonState) {}

    public ngOnInit(): void {
        this.personState.getContent().subscribe(() => console.log('loaded'));
    }
}

Why didn’t we see anything? why didn’t we see a new state?

Everything is very simple, we made our method an action, this method is asynchronous and when the request ends, the action ends. But then we manually change the state directly through the context, but the previous action has already completed and we will not get the difference in states in the devtools.

We can try it differently:

@StateRepository()
@State<PersonModel>({
    name: 'person',
    defaults: { title: null, description: null }
})
@Injectable()
export class PersonState extends NgxsDataRepository<PersonModel> {
    constructor(private readonly personService: PersonService) {
        super();
    }

    @DataAction()
    public getContent(): Observable<PersonModel> {
        return this.personService.fetchAll().pipe(
            tap((content: PersonModel): void => {
                // use setState instead ctx.setState
                this.setState(content);
            })
        );
    }
}

Now everything works, but in this case, we understand that the getContent method should be an ordinary method, not an action, because during its execution the state does not change, the state changes only after the request is completed. Therefore, it would be more correct to write such code:

@StateRepository()
@State<PersonModel>({
    name: 'person',
    defaults: { title: null, description: null }
})
@Injectable()
export class PersonState extends NgxsDataRepository<PersonModel> {
    constructor(private readonly personService: PersonService) {
        super();
    }

    public getContent(): Observable<PersonModel> {
        return this.personService.fetchAll().pipe(
            tap((content: PersonModel): void => {
                this.setState(content);
            })
        );
    }
}

Payload

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public addTodo(@Payload('todo') todo: string): void {
        if (todo) {
            this.ctx.setState((state: string[]) => state.concat(todo));
        }
    }
}

By default, all arguments have no name when automatically creating an action.

If during logging you want to see the payload, then you need to specify which action argument is this payload, using the @Payload() decorator.

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public addTodo(@Payload('todo') todo: string): void {
        if (todo) {
            this.ctx.setState((state: string[]) => state.concat(todo));
        }
    }
}

If you do not want to see the payload, but want to see the name of the arguments normally, you can use the named decorator.

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public addTodo(@Named('x') todo: string): void {
        if (todo) {
            this.ctx.setState((state: string[]) => state.concat(todo));
        }
    }
}

Decorators can be combined:

@StateRepository()
@State<string[]>({
    name: 'todo',
    defaults: []
})
@Injectable()
export class TodoState extends NgxsDataRepository<string[]> {
    @DataAction()
    public addTodo(@Payload('TODO') @Named('x') todo: string): void {
        if (todo) {
            this.ctx.setState((state: string[]) => state.concat(todo));
        }
    }
}

How to check whether the code is in NgZone?

The most common use of action is to optimize performance when starting a work consisting of one or more asynchronous or synchronous tasks that don't require UI updates or error handling to be handled by Angular. Such tasks can be kicked off via runOutsideAngular and if needed, these tasks can reenter the Angular zone via run.

By default, all action methods are called outside the Angular zone. But if you want to change this, you can define a parameter insideZone:

@StateRepository()
@State({
    name: 'counter',
    defaults: 0
})
@Injectable()
class CounterState extends NgxsDataRepository<number> {
    @DataAction({ insideZone: true })
    public incrementInZone(): void {
        console.log('expect is in Angular Zone', NgZone.isInAngularZone());
        this.ctx.setState((state) => ++state);
    }
}

Use @DataAction without subscription

With Data-plugin in case, @DataAction returns an Observable, you have to subscribe to the @DataAction function to fire an action itself. Without a subscribe, action will not be fired, and you will not see updates in the store. This can be confusing and unexpected for developers that are used to pure NGXS. Also, it can provide issues while integrating Data-plugin into the projects that were developed a long time without it.

To achieve an origin behavior of Actions you can pass option subscribeRequired with false value to the @DataAction decorator.

@StateRepository()
@State<PersonModel>({
    name: 'person',
    defaults: { title: null!, description: null! }
})
@Injectable()
export class PersonState extends NgxsImmutableDataRepository<PersonModel> {
    constructor(private readonly personService: PersonService) {
        super();
    }

    // Note: Also can be configured globally by providing custom NGXS_DATA_CONFIG
    @DataAction({ subscribeRequired: false })
    public getContent(): Observable<PersonModel> {
        return this.personService.fetchAll().pipe(tap((content: PersonModel): void => this.setState(content)));
    }
}

The same behavior can be achieved globally for all @DataAction in the app by providing a global config property.

@NgModule({
    declarations: [AppComponent],
    imports: [
        AppRoutingModule,
        BrowserAnimationsModule,
        BrowserModule,
        FormsModule,
        ReactiveFormsModule,
        NgxsModule.forRoot([], {
            developmentMode: !environment.production,
            executionStrategy: NoopNgxsExecutionStrategy
        }),
        NgxsLoggerPluginModule.forRoot(),
        NgxsDataPluginModule.forRoot([NGXS_DATA_STORAGE_EXTENSION, NGXS_DATA_STORAGE_CONTAINER])
    ],
    providers: [{ provide: NGXS_DATA_CONFIG, useValue: { dataActionSubscribeRequired: false } }],
    bootstrap: [AppComponent]
})
export class AppModule {}

Last updated