Перейти к содержимому

44. NGXS State Management

NGXS — это state management библиотека для Angular, вдохновлённая NgRx/Redux, но с более лаконичным синтаксисом через декораторы. Если NgRx кажется слишком многословным — NGXS стоит рассмотреть 🎯


Окно терминала
npm install @ngxs/store
npm install @ngxs/logger-plugin @ngxs/storage-plugin @ngxs/devtools-plugin

NgRxNGXS
ActionscreateAction() + отдельные файлыКлассы с декораторами
ReducerscreateReducer() + on()@Action методы в State
SelectorscreateSelector()@Selector методы в State
EffectsОтдельный @Injectable Effects@Action может вернуть Observable
BoilerplateБольшеМеньше
TypeScriptЛучшеХуже (из-за декораторов)
ЭкосистемаБольшеМеньше

store/users/users.state.ts
import { Injectable } from '@angular/core';
import { State, Action, StateContext, Selector, NgxsOnInit } from '@ngxs/store';
import { produce } from 'immer'; // Опционально, для удобства
import { tap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
// Actions — просто классы
export class LoadUsers {
static readonly type = '[Users] Load Users';
}
export class LoadUsersSuccess {
static readonly type = '[Users] Load Users Success';
constructor(public users: User[]) {}
}
export class LoadUsersFailure {
static readonly type = '[Users] Load Users Failure';
constructor(public error: string) {}
}
export class CreateUser {
static readonly type = '[Users] Create User';
constructor(public user: Partial<User>) {}
}
export class DeleteUser {
static readonly type = '[Users] Delete User';
constructor(public id: string) {}
}
export class SelectUser {
static readonly type = '[Users] Select User';
constructor(public userId: string | null) {}
}
export class SetFilter {
static readonly type = '[Users] Set Filter';
constructor(public filter: string) {}
}
// State model
export interface UsersStateModel {
users: User[];
loading: boolean;
error: string | null;
selectedUserId: string | null;
filter: string;
}
@State<UsersStateModel>({
name: 'users',
defaults: {
users: [],
loading: false,
error: null,
selectedUserId: null,
filter: '',
}
})
@Injectable()
export class UsersState implements NgxsOnInit {
constructor(private userService: UserService) {}
// Вызывается при инициализации State
ngxsOnInit(ctx: StateContext<UsersStateModel>) {
ctx.dispatch(new LoadUsers());
}
// ===== SELECTORS =====
@Selector()
static allUsers(state: UsersStateModel): User[] {
return state.users;
}
@Selector()
static loading(state: UsersStateModel): boolean {
return state.loading;
}
@Selector()
static error(state: UsersStateModel): string | null {
return state.error;
}
@Selector()
static filter(state: UsersStateModel): string {
return state.filter;
}
// Составной селектор
@Selector()
static filteredUsers(state: UsersStateModel): User[] {
if (!state.filter) return state.users;
return state.users.filter(u =>
u.name.toLowerCase().includes(state.filter.toLowerCase())
);
}
@Selector()
static selectedUser(state: UsersStateModel): User | null {
return state.users.find(u => u.id === state.selectedUserId) ?? null;
}
// ===== ACTIONS =====
@Action(LoadUsers)
loadUsers(ctx: StateContext<UsersStateModel>) {
ctx.patchState({ loading: true, error: null });
return this.userService.getAll().pipe(
tap(users => ctx.dispatch(new LoadUsersSuccess(users))),
catchError(error => {
ctx.dispatch(new LoadUsersFailure(error.message));
return of(null);
})
);
}
@Action(LoadUsersSuccess)
loadUsersSuccess(ctx: StateContext<UsersStateModel>, action: LoadUsersSuccess) {
ctx.patchState({
users: action.users,
loading: false,
});
}
@Action(LoadUsersFailure)
loadUsersFailure(ctx: StateContext<UsersStateModel>, action: LoadUsersFailure) {
ctx.patchState({
error: action.error,
loading: false,
});
}
@Action(CreateUser)
createUser(ctx: StateContext<UsersStateModel>, action: CreateUser) {
return this.userService.create(action.user).pipe(
tap(user => {
ctx.patchState({
users: [...ctx.getState().users, user],
});
})
);
}
@Action(DeleteUser)
deleteUser(ctx: StateContext<UsersStateModel>, action: DeleteUser) {
ctx.setState(
produce(ctx.getState(), draft => {
const idx = draft.users.findIndex(u => u.id === action.id);
if (idx !== -1) draft.users.splice(idx, 1);
})
);
}
@Action(SelectUser)
selectUser(ctx: StateContext<UsersStateModel>, action: SelectUser) {
ctx.patchState({ selectedUserId: action.userId });
}
@Action(SetFilter)
setFilter(ctx: StateContext<UsersStateModel>, action: SetFilter) {
ctx.patchState({ filter: action.filter });
}
}

app.config.ts
import { provideStore } from '@ngxs/store';
import { withNgxsLoggerPlugin } from '@ngxs/logger-plugin';
import { withNgxsStoragePlugin } from '@ngxs/storage-plugin';
import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin';
import { UsersState } from './store/users/users.state';
import { AuthState } from './store/auth/auth.state';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(
[UsersState, AuthState],
withNgxsLoggerPlugin(),
withNgxsReduxDevtoolsPlugin(),
withNgxsStoragePlugin({ keys: ['auth'] }) // Персистить auth state
)
]
};

@Component({
standalone: true,
imports: [AsyncPipe, ...],
template: `
@if (loading$ | async) { <mat-spinner /> }
<input [value]="filter$ | async" (input)="onFilter($event)" />
@for (user of users$ | async; track user.id) {
<app-user-card
[user]="user"
(delete)="onDelete(user.id)"
/>
}
`
})
export class UsersPageComponent {
private store = inject(Store);
// Select через StateClass.selector
loading$ = this.store.select(UsersState.loading);
users$ = this.store.select(UsersState.filteredUsers);
filter$ = this.store.select(UsersState.filter);
// Dispatch — просто new Action()
onFilter(event: Event) {
const filter = (event.target as HTMLInputElement).value;
this.store.dispatch(new SetFilter(filter));
}
onDelete(id: string) {
this.store.dispatch(new DeleteUser(id));
}
}

withNgxsStoragePlugin({
keys: ['auth', { key: 'users', serialize: JSON.stringify, deserialize: JSON.parse }],
storage: StorageOption.SessionStorage // или LocalStorage (по умолчанию)
})
withNgxsLoggerPlugin({
disabled: environment.production,
expanded: false,
logger: console,
})