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

38. NgRx: Введение

NgRx — это реализация Redux-паттерна для Angular на базе RxJS. Это не просто библиотека, это архитектурное решение для управления состоянием крупных Angular-приложений. Когда данные текут через всё приложение, и ты уже запутался — кто их обновляет и откуда — NgRx приходит на помощь 🏛️


Представь приложение с 50+ компонентами. Данные о текущем пользователе нужны в хедере, в боковом меню, в настройках профиля и ещё в 10 местах. Без централизованного состояния это превращается в ад:

// Без NgRx — данные передаются через @Input/@Output цепочки
// или через сервисы с BehaviorSubject
// Это работает, но становится неуправляемым при росте
@Component({}) // Header
export class HeaderComponent {
user$ = this.userService.user$; // Каждый компонент подписывается сам
}
@Component({}) // Sidebar
export class SidebarComponent {
user$ = this.userService.user$; // И изменяет состояние сам
updatePermissions() {
this.userService.updateUser(...); // Кто вызвал это? Когда? Зачем?
}
}

Главные проблемы:

  • 🔀 Изменения состояния происходят в разных местах — сложно отследить
  • 🐛 Баги сложно воспроизвести — нет истории изменений
  • 🧪 Компоненты со сложной логикой сложно тестировать
  • 🔄 Синхронизация состояния между компонентами — головная боль

Redux — это паттерн: одно состояние → чистые функции → неизменяемость

UI (Component)
↓ dispatch(Action)
Actions (Что произошло?)
Reducers (Как изменить состояние?)
Store (Единственный источник правды)
↓ select(Selector)
UI (Component) ← получает новое состояние

  1. Единственный источник правды — всё состояние приложения в одном Store
  2. Состояние только для чтения — изменить состояние можно только через dispatch(Action)
  3. Чистые функции — Reducers — это (state, action) => newState без побочных эффектов

// Весь AppState выглядит так:
interface AppState {
users: UsersState;
auth: AuthState;
router: RouterReducerState;
ui: UiState;
}
// Это объект в памяти, доступный из любого места через inject(Store)
// Action — это просто объект с полем type
// NgRx: createAction создаёт type-safe action creator
import { createAction, props } from '@ngrx/store';
// Без данных
export const loadUsers = createAction('[Users] Load Users');
// С данными
export const loadUsersSuccess = createAction(
'[Users] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[Users] Load Users Failure',
props<{ error: string }>()
);

Соглашение по именованию Action:

[Источник] Описание события
[Users Page] Load Users
[Auth API] Login Success
[Router] Navigation End
import { createReducer, on } from '@ngrx/store';
interface UsersState {
users: User[];
loading: boolean;
error: string | null;
}
const initialState: UsersState = {
users: [],
loading: false,
error: null,
};
export const usersReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true, error: null })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false,
})),
on(loadUsersFailure, (state, { error }) => ({
...state,
error,
loading: false,
}))
);
import { createFeatureSelector, createSelector } from '@ngrx/store';
// Выбираем кусок состояния
const selectUsersState = createFeatureSelector<UsersState>('users');
// Делаем производный селектор
export const selectAllUsers = createSelector(
selectUsersState,
state => state.users
);
export const selectUsersLoading = createSelector(
selectUsersState,
state => state.loading
);
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class UsersEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.userService.getAll().pipe(
map(users => loadUsersSuccess({ users })),
catchError(error => of(loadUsersFailure({ error: error.message })))
)
)
)
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}

NgRx DevTools — это расширение для Chrome, которое позволяет:

  • Видеть все dispatched actions в реальном времени
  • Путешествовать во времени (time-travel debugging)
  • Отслеживать изменения состояния
  • Импортировать/экспортировать состояние
app.config.ts
import { provideStoreDevtools } from '@ngrx/store-devtools';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(reducers),
provideStoreDevtools({
maxAge: 25,
logOnly: environment.production, // В продакшене только логирование
autoPause: true,
})
]
};

NgRx — мощный инструмент, но он добавляет сложность. Используй правило SHARI:

БукваПринципПример
SharedСостояние используется в нескольких несвязанных компонентахДанные пользователя
HydratedСостояние нужно сохранить при навигацииФильтры таблицы
AvailableСостояние доступно при входе в приложениеКонфигурация
RetrievedСостояние загружается с сервераСписок продуктов
ImpactedСостояние влияет на другие части приложенияКорзина покупок

НЕ используй NgRx, если:

  • Состояние используется только в одном компоненте → signal() или локальный state
  • Маленькое приложение → сервис с BehaviorSubject
  • Данные не нужно синхронизировать между страницами → ComponentStore

Привет, команда!
NgRx — это как строгий бухгалтер в офисе.
Раньше каждый мог взять деньги из кассы (изменить состояние)
когда хотел и как хотел. Было удобно, но никто не знал,
сколько денег осталось и куда они делись.
Теперь:
1. Ты хочешь взять деньги? Заполни заявку (dispatch Action)
2. Бухгалтер проверяет заявку по правилам (Reducer)
3. Деньги выдаются строго по регламенту (новый State)
4. Всё записывается в журнал (DevTools)
5. Если нужно перевести деньги из банка — есть отдел
по работе с банками (Effects)
Да, немного бюрократии. Но зато:
— Всегда знаешь, что происходит
— Можно откатиться назад
— Легко находить баги
С уважением,
NgRx архитектура