Введение в NgRx ComponentStore с практическим примером

ComponentStore — это библиотека управления состоянием, которая помогает управлять состоянием компонента. ComponentStore является частью NgRx и позиционирует себя как альтернатива реактивному подходу «Сервис с субъектом на основе push-уведомлений».

Знание NgRx, Redux или других шаблонов управления состоянием будет полезно, но в этом посте объясняется NGRX ComponentStore, предполагая отсутствие предварительного знания NGRX.

Что такое хранилище компонентов NGRX?

ComponentStore — это инструмент управления состоянием, который позволяет разработчикам легко управлять состоянием на уровне компонентов (например, локальным состоянием), вместо того чтобы управлять им глобально во всем приложении.

Это упрощает анализ состояния отдельных компонентов и реализацию сложных сценариев управления состоянием масштабируемым и удобным в сопровождении способом.

Ключевые идеи

Официальная документация отлично описывает ключевые концепции NGRX ComponentStore:

  • Локальное состояние должно быть инициализировано, но это можно сделать лениво.
  • Локальное состояние обычно привязано к жизненному циклу конкретного компонента и очищается, когда этот компонент уничтожается.
  • Пользователи ComponentStore могут обновлять состояние через setState или updater либо императивно, либо путем предоставления Observable.
  • Пользователи ComponentStore могут читать состояние через select или state$ верхнего уровня. Селекторы очень эффективны.
  • Пользователи ComponentStore могут запускать побочные эффекты с помощью effect, как синхронные, так и асинхронные, и передавать данные императивно или реактивно.

Когда следует использовать ComponentStore?

Это зависит от сложности вашего приложения.

Если сценарий представляет собой простую родительско-дочернюю структуру, мы можем выбрать декораторы @Input() и @Output(). Однако это довольно быстро сходит с ума. Особенно, когда между родителем и дочерним элементом есть несколько компонентов.

Когда приложения становятся сложными, управление состоянием может быть лучшим подходом. В этом случае NgRx или его облегченная версия, ComponentStore, должны быть предпочтительным способом поддержания единого источника достоверности и связи между компонентами.

Вот подробное сравнение между Store и ComponentStore.

В общем, если вы создаете приложение Angular и хотите управлять состоянием предсказуемым и масштабируемым способом, NGRX/ComponentStore может быть для вас хорошим выбором.

Однако важно отметить, что NGRX/ComponentStore — это лишь один из многих вариантов управления состоянием в приложениях Angular, и вы должны учитывать свои конкретные требования и варианты использования, прежде чем решить, является ли это правильным выбором для вас.

Пример хранилища компонентов NgRx

Мы собираемся создать приложение, которое отслеживает статус некоторых книг в трех сегментах: список желаний, чтение и история. Три ведра представляют

  • книги, которые вы хотите прочитать,
  • те, которые вы сейчас читаете,
  • и те, которые вы читаете.

В приложении есть компонент BooksComponent, содержащий три дочерних компонента:

  • компонент списка желаний,
  • Компонент чтения,
  • ИсторияКомпонент.

Магазин находится в файле books.store.ts в BooksComponent.

Каждый дочерний компонент похож на другой, но я хотел, чтобы они были отдельными, чтобы избежать путаницы.

Вы определенно можете реорганизовать приложение, чтобы использовать три экземпляра одного и того же компонента, но это не цель этого поста.

Вот структура приложения.

Установить магазин компонентов

Сначала установите ComponentStore в свой проект.

Я использовал ng add, но вы можете использовать npm или yarn, если хотите.

ng add @ngrx/component-store@latest

Вы можете наткнуться на следующее предупреждение

что в моем случае выдало следующую ошибку

Я использовал более старую версию rxjs.

Как только вы обновите версию rxjs до ^7.5.0 в вашем package.json и запустите npm install, вы будете готовы к установке ComponentStore.

Инициализировать ComponentStore: BooksStore

Согласно документации, ComponentStore можно инициализировать двумя способами:

  • через конструктор — путем передачи начального состояния конструктору. Это происходит внутри ComponentStore
  • используя отложенную загрузку — вызывая setState и передавая объект, соответствующий интерфейсу состояния. Это происходит внутри класса компонента, а не в ComponentStore.

Для простоты я начну с первого, а магазин будет называться BooksStore.

Вы можете выбрать ленивую инициализацию ComponentStore, следуя документации.

1. Создайте файл ComponentStore: books.store.ts

Мы можем начать с создания ComponentStore в файле, который мы создаем, например. книжный.магазин.ц. Этот файл является родственным файлу books.component.ts и будет находиться внутри BooksComponent.

С этого момента я буду использовать термины ComponentStore и BooksStore взаимозаменяемо, поскольку последний представляет собой ComponentStore.

2. Импорт

В начале BooksStore импортируйте декоратор Injectable и ComponentStore.

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

3. Инициализировать BooksStore

Наконец, мы можем инициализировать BooksStore через конструктор.

type BooksState {
  wishList: string[];
  reading: string[];
  history: string[];
}

const initialState = {
  wishList: ['Oksi', 'The Pragmatic Programmer', 'Let My People Go Surfing'],
  reading: ['4000 Weeks'],
  history: ['Zero To One', 'Extreme Economies'],
};

@Injectable()
export class BooksStore extends ComponentStore<BooksState> {
  constructor() {
    super(initialState);
  }
}

Мы инициализируем хранилище и передаем переменную с именем initialState, которая представляет собой объект, содержащий три ключа (список желаний, чтение, история) и несколько строк, представляющих названия книг, связанных с каждым ключом.

«Инициализация через конструктор делает состояние немедленно доступным для потребителей ComponentStore.»

Обратите внимание, что вы можете рассматривать хранилище как службу, расширяющую ComponentStore. Однако в данном случае мы не предоставляем его в «руте», как это часто бывает с сервисами.

На данный момент у нас есть базовая настройка хранилища, которую мы можем внедрить в BooksComponent, чтобы один и тот же экземпляр хранилища стал доступен для всех дочерних компонентов BooksComponent.

// books.component.ts

import { Component } from '@angular/core';
import { BooksStore } from './books.store';

@Component({
  selector: 'app-books',
  templateUrl: './books.component.html',
  styleUrls: ['./books.component.css'],
  providers: [BooksStore],
})
export class BooksComponent {}

BooksComponent практически пуст, но нам нужно, чтобы он внедрил BooksStore в массив поставщиков, чтобы сделать хранилище доступным для всех дочерних компонентов BooksComponent.

Состояние чтения

Теперь мы хотим прочитать состояние в хранилище от дочерних компонентов. Кроме того, мы хотим, чтобы каждый компонент мог считывать только соответствующее подмножество состояния.

WishListComponent не нужно знать, сколько книг будет в HistoryComponent, и так далее для любого другого компонента.

Поэтому я хочу, чтобы WishListComponent читал подмножество состояния в BooksStore.

1. Подготовка BooksStore к раскрытию данных

Мы можем использовать метод select, чтобы прочитать состояние в BooksStore и сделать свойство wishList$ доступным в других компонентах. Знак $ — это соглашение, указывающее, что свойство является наблюдаемым.

@Injectable()
export class BooksStore extends ComponentStore<BooksState> {
  
  readonly wishList$: Observable<string[]> = this.select(
    (state) => state.wishList
  );
...

2. Чтение данных состояния в WishListComponent

В WishListComponent нужно сделать три вещи.

  • Импортируйте магазин в компонент,
  • объявить хранилище в конструкторе,
  • использовать хранилище для доступа к нужному нам свойству
import { Component } from '@angular/core';
import { BooksStore } from '../books.store';

@Component({
  selector: 'app-wish-list',
  templateUrl: './wish-list.component.html',
  styleUrls: ['./wish-list.component.css'],
})
export class WishListComponent {
  wishList$ = this.booksStore.wishList$; // access data in store 

  constructor(private readonly booksStore: BooksStore) {}
}

Свойство wishList$ — это Observable, которое можно использовать в шаблоне для перечисления книг, принадлежащих разделу списка пожеланий состояния.

Поскольку свойство wishList$ является наблюдаемым, мы можем использовать асинхронный канал следующим образом.

<li *ngFor="let book of wishList$ | async">{{ book }}</li>

Мы можем повторить два шага (предоставление данных в хранилище и чтение данных в компоненте) как для ReadingComponent, так и для HistoryComponent.

В итоге у нас должно получиться что-то вроде следующего.

Обновление состояния

Как сообщается в документации, существует три способа обновить состояние в ComponentStore:

  • создайте updater в магазине и пропустите через него входные данные
  • использовать setState из компонента
  • использовать patchState из компонента

Для простоты мы рассмотрим первый и второй способ обновления данных в ComponentStore.

Щелчок по любой книге в списке желаний переместит книгу в список для чтения с помощью updater.

Щелчок по любой книге в списке чтения переместит книгу в список истории с помощью setState.

  1. Обновление

Обновление принимает чистую функцию с текущим состоянием и значением в качестве аргументов и должно возвращать новое состояние, неизменно обновляемое.

В магазине мы можем создать updater, чтобы при его срабатывании книга перемещалась из списка желаний в список чтения.

// books.store.ts

...
readonly moveToReading = this.updater((state, title: string) => ({
    ...state,
    wishList: [
      ...state.wishList.filter((titleInList) => titleInList !== title),
    ],
    reading: [...state.reading, title],
}));

Состояние неизменяемое, поэтому мы не можем напрямую присвоить новое свойство состоянию, иначе детекторы изменений и селекторы не обнаружат изменений.

Вот почему я использую деструктурирование массива, чтобы «скопировать-вставить» старое состояние, а затем изменить его, чтобы создать новые массивы, которые назначаются для списка желаний и ключей чтения.

Обновление готово. Мы можем создать метод в WishListComponent для вызова средства обновления в магазине.

// wish-list.component.ts

handleClick(title: string) {
    this.booksStore.moveToReading(title);
}

Шаблон вызывает метод handleClick, когда пользователь нажимает на название книги.

Метод handleClick берет название книги и передает его методу updater. Логика вне компонента.

<!-- wish-list.component.html -->
<ul>
    <li *ngFor="let book of wishList$ | async" (click)="handleClick(book)">
      {{ book }}
    </li>
</ul>

2. установить состояние

Мы могли бы использовать еще один updater, чтобы переместить книгу из списка чтения в историю, однако мы будем использовать setState.

В отличие от updater, setState вызывается в компоненте и не требует никакого метода в хранилище.

Метод setState можно вызвать

  • предоставляя объект состояния. Это «сбрасывает все состояние до предоставленного значения. Таким же образом выполняется ленивая инициализация».
  • в качестве обратного вызова. Мы будем использовать этот вариант.

Хотя HTML-код точно такой же, как в Wish-list.component.html, функция обработчика теперь использует setState, передавая функцию обратного вызова, которая обновляет состояние по мере необходимости.

// reading-component.ts
handleClick(title: string) {
    this.booksStore.setState((state) => ({
      ...state,
      reading: [
        ...state.reading.filter((titleInList) => titleInList !== title),
      ],
      history: [...state.history, title],
    }));
}

На этом этапе вы должны быть в состоянии щелкнуть заголовок, чтобы переместить его из WishList в Reading и из Reading в History.

patchStateметод можно использовать очень похожим образом.

Выводы

В этом посте мы изучили основы NgRx ComponentStore.

Мы создали хранилище, инициализировали состояние, создали несколько селекторов для отображения части состояния и обновили состояние, используя updater и setState.

Вы можете найти код на GitHub внутри BooksComponent.

Есть еще одна вещь, которую мы должны пройти, эффекты. Как поясняется в документации, «Эффекты предназначены для извлечения любых побочных эффектов (таких как сетевые вызовы) из компонентов и обработки потенциальных условий гонки».

Я напишу и свяжу другую статью, посвященную этой части.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.