На сегодняшний день ни одно большое SPA приложение не обходится без state management (управления состоянием). Для Angular по данному направлению есть несколько решений. Самым популярным из них является NgRx. Он реализует Redux паттерн с использованием библиотеки RxJs и обладает хорошим инструментарием.
В данной статье мы кратко пройдемся по основным модулям NgRx и более детально сосредоточимся на библиотеке angular-ngrx-data, которая позволяет сделать полноценный CRUD со state management за пять минут.
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
import { Action } from '@ngrx/store';
const initialState = 0;
export function counterReducer(state: number = initialState, action: Action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
case RESET:
return 0;
default:
return state;
}
}
.import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter';
@NgModule({
imports: [StoreModule.forRoot({ count: counterReducer })],
})
export class AppModule {}
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { INCREMENT, DECREMENT, RESET } from './counter';
interface AppState {
count: number;
}
@Component({
selector: 'app-my-counter',
template: `
<button (click)="increment()">Increment</button>
<div>Current Count: {{ count$ | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
`,
})
export class MyCounterComponent {
count$: Observable<number>;
constructor(private store: Store<AppState>) {
this.count$ = store.pipe(select('count'));
}
increment() {
this.store.dispatch({ type: INCREMENT });
}
decrement() {
this.store.dispatch({ type: DECREMENT });
}
reset() {
this.store.dispatch({ type: RESET });
}
}
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
// Модуль должен быть подключен после StoreModule
StoreDevtoolsModule.instrument({
maxAge: 25, // Хранятся последние 25 состояний
}),
],
})
export class AppModule {}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Action } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, of } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
@Injectable()
export class AuthEffects {
// Listen for the 'LOGIN' action
@Effect()
login$: Observable<Action> = this.actions$.pipe(
ofType('LOGIN'),
mergeMap(action =>
this.http.post('/auth', action.payload).pipe(
// If successful, dispatch success action with result
map(data => ({ type: 'LOGIN_SUCCESS', payload: data })),
// If request fails, dispatch failed action
catchError(() => of({ type: 'LOGIN_FAILED' }))
)
)
);
constructor(private http: HttpClient, private actions$: Actions) {}
}
import { EffectsModule } from '@ngrx/effects';
import { AuthEffects } from './effects/auth.effects';
@NgModule({
imports: [EffectsModule.forRoot([AuthEffects])],
})
export class AppModule {}
export interface User {
id: string;
name: string;
}
import { Action } from '@ngrx/store';
import { Update } from '@ngrx/entity';
import { User } from './user.model';
export enum UserActionTypes {
LOAD_USERS = '[User] Load Users',
ADD_USER = '[User] Add User',
UPSERT_USER = '[User] Upsert User',
ADD_USERS = '[User] Add Users',
UPSERT_USERS = '[User] Upsert Users',
UPDATE_USER = '[User] Update User',
UPDATE_USERS = '[User] Update Users',
DELETE_USER = '[User] Delete User',
DELETE_USERS = '[User] Delete Users',
CLEAR_USERS = '[User] Clear Users',
}
export class LoadUsers implements Action {
readonly type = UserActionTypes.LOAD_USERS;
constructor(public payload: { users: User[] }) {}
}
export class AddUser implements Action {
readonly type = UserActionTypes.ADD_USER;
constructor(public payload: { user: User }) {}
}
export class UpsertUser implements Action {
readonly type = UserActionTypes.UPSERT_USER;
constructor(public payload: { user: User }) {}
}
export class AddUsers implements Action {
readonly type = UserActionTypes.ADD_USERS;
constructor(public payload: { users: User[] }) {}
}
export class UpsertUsers implements Action {
readonly type = UserActionTypes.UPSERT_USERS;
constructor(public payload: { users: User[] }) {}
}
export class UpdateUser implements Action {
readonly type = UserActionTypes.UPDATE_USER;
constructor(public payload: { user: Update<User> }) {}
}
export class UpdateUsers implements Action {
readonly type = UserActionTypes.UPDATE_USERS;
constructor(public payload: { users: Update<User>[] }) {}
}
export class DeleteUser implements Action {
readonly type = UserActionTypes.DELETE_USER;
constructor(public payload: { id: string }) {}
}
export class DeleteUsers implements Action {
readonly type = UserActionTypes.DELETE_USERS;
constructor(public payload: { ids: string[] }) {}
}
export class ClearUsers implements Action {
readonly type = UserActionTypes.CLEAR_USERS;
}
export type UserActionsUnion =
| LoadUsers
| AddUser
| UpsertUser
| AddUsers
| UpsertUsers
| UpdateUser
| UpdateUsers
| DeleteUser
| DeleteUsers
| ClearUsers;
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { User } from './user.model';
import { UserActionsUnion, UserActionTypes } from './user.actions';
export interface State extends EntityState<User> {
// additional entities state properties
selectedUserId: number | null;
}
export const adapter: EntityAdapter<User> = createEntityAdapter<User>();
export const initialState: State = adapter.getInitialState({
// additional entity state properties
selectedUserId: null,
});
export function reducer(state = initialState, action: UserActionsUnion): State {
switch (action.type) {
case UserActionTypes.ADD_USER: {
return adapter.addOne(action.payload.user, state);
}
case UserActionTypes.UPSERT_USER: {
return adapter.upsertOne(action.payload.user, state);
}
case UserActionTypes.ADD_USERS: {
return adapter.addMany(action.payload.users, state);
}
case UserActionTypes.UPSERT_USERS: {
return adapter.upsertMany(action.payload.users, state);
}
case UserActionTypes.UPDATE_USER: {
return adapter.updateOne(action.payload.user, state);
}
case UserActionTypes.UPDATE_USERS: {
return adapter.updateMany(action.payload.users, state);
}
case UserActionTypes.DELETE_USER: {
return adapter.removeOne(action.payload.id, state);
}
case UserActionTypes.DELETE_USERS: {
return adapter.removeMany(action.payload.ids, state);
}
case UserActionTypes.LOAD_USERS: {
return adapter.addAll(action.payload.users, state);
}
case UserActionTypes.CLEAR_USERS: {
return adapter.removeAll({ ...state, selectedUserId: null });
}
default: {
return state;
}
}
}
export const getSelectedUserId = (state: State) => state.selectedUserId;
// get the selectors
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
// select the array of user ids
export const selectUserIds = selectIds;
// select the dictionary of user entities
export const selectUserEntities = selectEntities;
// select the array of users
export const selectAllUsers = selectAll;
// select the total user count
export const selectUserTotal = selectTotal;
import {
createSelector,
createFeatureSelector,
ActionReducerMap,
} from '@ngrx/store';
import * as fromUser from './user.reducer';
export interface State {
users: fromUser.State;
}
export const reducers: ActionReducerMap<State> = {
users: fromUser.reducer,
};
export const selectUserState = createFeatureSelector<fromUser.State>('users');
export const selectUserIds = createSelector(
selectUserState,
fromUser.selectUserIds
);
export const selectUserEntities = createSelector(
selectUserState,
fromUser.selectUserEntities
);
export const selectAllUsers = createSelector(
selectUserState,
fromUser.selectAllUsers
);
export const selectUserTotal = createSelector(
selectUserState,
fromUser.selectUserTotal
);
export const selectCurrentUserId = createSelector(
selectUserState,
fromUser.getSelectedUserId
);
export const selectCurrentUser = createSelector(
selectUserEntities,
selectCurrentUserId,
(userEntities, userId) => userEntities[userId]
);
npm install --save @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools ngrx-data
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
EntityMetadataMap,
NgrxDataModule,
DefaultDataServiceConfig
} from 'ngrx-data';
const defaultDataServiceConfig: DefaultDataServiceConfig = {
root: 'crud'
};
export const entityMetadata: EntityMetadataMap = {
Hero: {},
User:{}
};
export const pluralNames = { Hero: 'heroes' };
@NgModule({
imports: [
CommonModule,
NgrxDataModule.forRoot({ entityMetadata, pluralNames })
],
declarations: [],
providers: [
{ provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig }
]
})
export class EntityStoreModule {}
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
EntityStoreModule,
StoreDevtoolsModule.instrument({
maxAge: 25,
}),
],
declarations: [
AppComponent
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
const defaultDataServiceConfig: DefaultDataServiceConfig = {
root: 'crud'
};
export const entityMetadata: EntityMetadataMap = {
Hero: {},
User:{}
};
...
NgrxDataModule.forRoot({ entityMetadata, pluralNames })
export const pluralNames = { Hero: 'heroes' };
...
NgrxDataModule.forRoot({ entityMetadata, pluralNames })
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Observable } from 'rxjs';
import { Hero } from '@appModels/hero';
import { EntityServices, EntityCollectionService } from 'ngrx-data';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeroesComponent implements OnInit {
heroes$: Observable<Hero[]>;
heroesService: EntityCollectionService<Hero>;
constructor(entityServices: EntityServices) {
this.heroesService = entityServices.getEntityCollectionService('Hero');
}
...
}
ngOnInit() {
this.heroes$ = this.heroesService.entities$;
this.heroesService.getAll();
}
import { EntityCollectionService, EntityServices } from 'ngrx-data';
import { Hero } from '../../core';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeroesComponent implements OnInit {
heroes$: Observable<Hero[]>;
heroesService: EntityCollectionService<Hero>;
constructor(entityServices: EntityServices) {
this.heroesService = entityServices.getEntityCollectionService('Hero');
}
ngOnInit() {
this.heroes$ = this.heroesService.entities$;
this.getHeroes();
}
getHeroes() {
this.heroesService.getAll();
}
addHero(hero: Hero) {
this.heroesService.add(hero);
}
deleteHero(hero: Hero) {
this.heroesService.delete(hero.id);
}
updateHero(hero: Hero) {
this.heroesService.update(hero);
}
}
import { Component, OnInit } from '@angular/core';
import { MessageService } from '@appServices/message.service';
import { EntityServices } from 'ngrx-data';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
constructor(
public messageService: MessageService,
private entityServices: EntityServices
) {}
ngOnInit() {
this.entityServices.reducedActions$.subscribe(res => {
if (res && res.type) {
this.messageService.add(res.type);
}
});
}
}
К сожалению, не доступен сервер mySQL