44. NGXS State Management
🦋 NGXS — Альтернативный State Management
Заголовок раздела «🦋 NGXS — Альтернативный State Management»NGXS — это state management библиотека для Angular, вдохновлённая NgRx/Redux, но с более лаконичным синтаксисом через декораторы. Если NgRx кажется слишком многословным — NGXS стоит рассмотреть 🎯
Установка
Заголовок раздела «Установка»npm install @ngxs/storenpm install @ngxs/logger-plugin @ngxs/storage-plugin @ngxs/devtools-pluginКлючевые отличия от NgRx
Заголовок раздела «Ключевые отличия от NgRx»| NgRx | NGXS | |
|---|---|---|
| Actions | createAction() + отдельные файлы | Классы с декораторами |
| Reducers | createReducer() + on() | @Action методы в State |
| Selectors | createSelector() | @Selector методы в State |
| Effects | Отдельный @Injectable Effects | @Action может вернуть Observable |
| Boilerplate | Больше | Меньше |
| TypeScript | Лучше | Хуже (из-за декораторов) |
| Экосистема | Больше | Меньше |
@State декоратор — определение состояния
Заголовок раздела «@State декоратор — определение состояния»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 modelexport 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 }); }}Регистрация
Заголовок раздела «Регистрация»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)); }}Плагины NGXS
Заголовок раздела «Плагины NGXS»Storage Plugin — персистентность
Заголовок раздела «Storage Plugin — персистентность»withNgxsStoragePlugin({ keys: ['auth', { key: 'users', serialize: JSON.stringify, deserialize: JSON.parse }], storage: StorageOption.SessionStorage // или LocalStorage (по умолчанию)})Logger Plugin
Заголовок раздела «Logger Plugin»withNgxsLoggerPlugin({ disabled: environment.production, expanded: false, logger: console,})