Настройка State Management (NgRx) для Angular-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка State Management (NgRx) для Angular-приложения
Сложная
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Настройка State Management (NgRx) для Angular-приложения

NgRx — Redux-архитектура для Angular, построенная на RxJS. Однонаправленный поток данных: Actions → Reducers → State → Selectors → Components → Actions. Дополнительно — Effects для сайд-эффектов (HTTP, WebSocket, localStorage) и Entity для работы с коллекциями.

Оправдан в крупных корпоративных Angular-приложениях с несколькими командами, сложной бизнес-логикой и требованием полной трассируемости изменений. Для небольших проектов использование NgRx сопряжено с избыточным boilerplate.

Что входит в работу

Установка пакетов NgRx, настройка Store, Actions, Reducers, Selectors, Effects. Интеграция с Angular Router через @ngrx/router-store. Feature-модули с lazy loaded state. NgRx Entity для коллекций. DevTools. Тестирование.

Установка

ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/entity@latest
ng add @ngrx/store-devtools@latest
ng add @ngrx/router-store@latest

# или через npm
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools @ngrx/router-store

ng add автоматически обновляет app.module.ts или app.config.ts (standalone).

Структура файлов

src/app/
  store/
    app.state.ts          # корневой интерфейс состояния
  features/
    products/
      store/
        products.actions.ts
        products.reducer.ts
        products.selectors.ts
        products.effects.ts
        products.facade.ts  # опционально
      products.module.ts

Actions

// features/products/store/products.actions.ts
import { createAction, createActionGroup, emptyProps, props } from '@ngrx/store'

export const ProductsActions = createActionGroup({
  source: 'Products',
  events: {
    'Load Products': props<{ categoryId: string }>(),
    'Load Products Success': props<{ products: Product[] }>(),
    'Load Products Failure': props<{ error: string }>(),
    'Select Product': props<{ productId: string }>(),
    'Add To Cart': props<{ product: Product }>(),
    'Clear Selection': emptyProps(),
  },
})

createActionGroup генерирует типизированные экшены с автоматическим type string: [Products] Load Products.

State и Reducer с NgRx Entity

// features/products/store/products.reducer.ts
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'
import { createReducer, on } from '@ngrx/store'
import { ProductsActions } from './products.actions'

export interface ProductsState extends EntityState<Product> {
  selectedProductId: string | null
  loading: boolean
  error: string | null
}

export const adapter: EntityAdapter<Product> = createEntityAdapter<Product>({
  selectId: (p) => p.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name),
})

const initialState: ProductsState = adapter.getInitialState({
  selectedProductId: null,
  loading: false,
  error: null,
})

export const productsReducer = createReducer(
  initialState,
  on(ProductsActions.loadProducts, (state) => ({
    ...state,
    loading: true,
    error: null,
  })),
  on(ProductsActions.loadProductsSuccess, (state, { products }) =>
    adapter.setAll(products, { ...state, loading: false })
  ),
  on(ProductsActions.loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  })),
  on(ProductsActions.selectProduct, (state, { productId }) => ({
    ...state,
    selectedProductId: productId,
  })),
  on(ProductsActions.clearSelection, (state) => ({
    ...state,
    selectedProductId: null,
  }))
)

export const { selectAll, selectEntities, selectIds, selectTotal } =
  adapter.getSelectors()

Selectors

// features/products/store/products.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store'
import { ProductsState, selectAll, selectEntities } from './products.reducer'

export const selectProductsState =
  createFeatureSelector<ProductsState>('products')

export const selectAllProducts = createSelector(
  selectProductsState,
  selectAll
)

export const selectProductsLoading = createSelector(
  selectProductsState,
  (state) => state.loading
)

export const selectProductsError = createSelector(
  selectProductsState,
  (state) => state.error
)

export const selectSelectedProductId = createSelector(
  selectProductsState,
  (state) => state.selectedProductId
)

export const selectSelectedProduct = createSelector(
  selectProductsState,
  selectEntities,
  selectSelectedProductId,
  (_, entities, id) => (id ? entities[id] ?? null : null)
)

export const selectProductsByCategory = (categoryId: string) =>
  createSelector(selectAllProducts, (products) =>
    products.filter((p) => p.categoryId === categoryId)
  )

Effects

// features/products/store/products.effects.ts
import { Injectable } from '@angular/core'
import { Actions, createEffect, ofType } from '@ngrx/effects'
import { catchError, map, switchMap } from 'rxjs/operators'
import { of } from 'rxjs'
import { ProductsActions } from './products.actions'
import { ProductsService } from '../products.service'

@Injectable()
export class ProductsEffects {
  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ProductsActions.loadProducts),
      switchMap(({ categoryId }) =>
        this.productsService.getByCategory(categoryId).pipe(
          map((products) => ProductsActions.loadProductsSuccess({ products })),
          catchError((err) =>
            of(ProductsActions.loadProductsFailure({ error: err.message }))
          )
        )
      )
    )
  )

  // эффект без диспатча (навигация, analytics)
  trackProductView$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(ProductsActions.selectProduct),
        tap(({ productId }) => this.analytics.track('product_view', { productId }))
      ),
    { dispatch: false }
  )

  constructor(
    private actions$: Actions,
    private productsService: ProductsService,
    private analytics: AnalyticsService
  ) {}
}

Регистрация в модуле

// products.module.ts
import { StoreModule } from '@ngrx/store'
import { EffectsModule } from '@ngrx/effects'

@NgModule({
  imports: [
    StoreModule.forFeature('products', productsReducer),
    EffectsModule.forFeature([ProductsEffects]),
  ],
})
export class ProductsModule {}

Для standalone-приложений (Angular 17+):

// app.config.ts
import { provideStore } from '@ngrx/store'
import { provideEffects } from '@ngrx/effects'
import { provideStoreDevtools } from '@ngrx/store-devtools'

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore(),
    provideEffects(),
    provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
  ],
}

Использование в компоненте

@Component({
  selector: 'app-products',
  template: `
    <div *ngIf="loading$ | async">Загрузка...</div>
    <app-product-card
      *ngFor="let product of products$ | async"
      [product]="product"
      (addToCart)="onAddToCart($event)"
    />
  `,
})
export class ProductsComponent implements OnInit {
  products$ = this.store.select(selectAllProducts)
  loading$ = this.store.select(selectProductsLoading)
  error$ = this.store.select(selectProductsError)

  constructor(
    private store: Store,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.route.params.pipe(
      map((p) => p['categoryId']),
      distinctUntilChanged()
    ).subscribe((categoryId) => {
      this.store.dispatch(ProductsActions.loadProducts({ categoryId }))
    })
  }

  onAddToCart(product: Product) {
    this.store.dispatch(ProductsActions.addToCart({ product }))
  }
}

Facade-паттерн

Фасад скрывает NgRx от компонентов — компонент работает только с фасадом:

@Injectable({ providedIn: 'root' })
export class ProductsFacade {
  allProducts$ = this.store.select(selectAllProducts)
  loading$ = this.store.select(selectProductsLoading)
  selectedProduct$ = this.store.select(selectSelectedProduct)

  constructor(private store: Store) {}

  loadProducts(categoryId: string) {
    this.store.dispatch(ProductsActions.loadProducts({ categoryId }))
  }

  selectProduct(productId: string) {
    this.store.dispatch(ProductsActions.selectProduct({ productId }))
  }

  addToCart(product: Product) {
    this.store.dispatch(ProductsActions.addToCart({ product }))
  }
}

NgRx Router Store

// app.config.ts
import { provideRouterStore, routerReducer } from '@ngrx/router-store'

providers: [
  provideStore({ router: routerReducer }),
  provideRouterStore(),
]

// selectors
import { getSelectors } from '@ngrx/router-store'
const { selectCurrentRoute, selectQueryParams, selectRouteParams } = getSelectors()

// использование в эффекте
this.store.select(selectRouteParams).pipe(
  map((params) => params['id']),
  filter(Boolean),
  switchMap((id) => this.service.getById(id))
)

Тестирование

// reducer
describe('productsReducer', () => {
  it('устанавливает loading при loadProducts', () => {
    const state = productsReducer(initialState, ProductsActions.loadProducts({ categoryId: '1' }))
    expect(state.loading).toBe(true)
  })

  it('записывает products при успехе', () => {
    const products = [{ id: '1', name: 'Test', price: 100, categoryId: '1' }]
    const state = productsReducer(
      { ...initialState, loading: true },
      ProductsActions.loadProductsSuccess({ products })
    )
    expect(state.ids).toEqual(['1'])
    expect(state.loading).toBe(false)
  })
})

// effects
describe('ProductsEffects', () => {
  let effects: ProductsEffects
  let actions$: Observable<Action>

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ProductsEffects,
        provideMockActions(() => actions$),
        { provide: ProductsService, useValue: { getByCategory: jest.fn() } },
      ],
    })
    effects = TestBed.inject(ProductsEffects)
  })

  it('dispatches loadProductsSuccess', () => {
    const products = [{ id: '1', name: 'Test', price: 100, categoryId: '1' }]
    const service = TestBed.inject(ProductsService) as jest.Mocked<ProductsService>
    service.getByCategory.mockReturnValue(of(products))

    actions$ = hot('-a', { a: ProductsActions.loadProducts({ categoryId: '1' }) })
    const expected = cold('-b', { b: ProductsActions.loadProductsSuccess({ products }) })
    expect(effects.loadProducts$).toBeObservable(expected)
  })
})

Что делаем

Устанавливаем и настраиваем весь NgRx-стек, проектируем feature-сторы под предметную область, реализуем эффекты для HTTP-запросов, настраиваем router-store, при необходимости оборачиваем в Facade, покрываем тестами reducers и effects.

Срок: 5–10 дней в зависимости от количества feature-модулей и сложности бизнес-логики.