Frontend
October 15, 2023

Управление состоянием с Akita в Angular

Зачем вообще нужно управлять состоянием?

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

Чтобы отслеживать все эти изменения, нужен специальный механизм управления состоянием. Он позволяет:

  • Хранить данные в определенном месте (например, в специальных объектах - сторах)
  • Обновлять эти данные по определенным правилам
  • Получать актуальные данные в любой момент
  • Автоматически обновлять UI при изменении данных
  • Отменять изменения и возвращать предыдущее состояние

Без управления состоянием код быстро превращается в хаос из глобальных переменных, сложных зависимостей и дублирования данных. Поэтому использование спецальных инструментов вроде Akita - это очень хорошая практика.

Что такое Akita?

Akita - это библиотека для управления состоянием в Angular-приложениях. Вот её основные преимущества:

  • Простота. Интуитивный и лаконичный API.
  • Надежность. Строго типизированное состояние, иммутабельность.
  • Гибкость. Кастомизируй под свои нужды.
  • Производительность. Оптимизирована для скорости.
  • Масштабируемость. Подходит как для маленьких, так и для крупных приложений.

То есть с Akita ты можешь быстро организовать управление состоянием в своём приложении и не париться по поводу производительности или масштабирования. Дальше разберём как это делается.

Установка Akita

Чтобы начать использовать Akita, нужно выполнить deux простых шага:

1. Установить пакеты:

npm install @datorama/akita --save
npm install @datorama/akita-ng-entity-service --save

2. Импортировать модули Akita в app.module.ts:

import { AkitaNgDevtools } from '@datorama/akita-ngdevtools';
import { AkitaNgRouterStoreModule } from '@datorama/akita-ng-router-store';
import { AkitaTypedFormModule } from '@datorama/akita-ng-forms';

@NgModule({
  declarations: [AppComponent],
  imports: [
    AkitaNgDevtools.forRoot(),
    AkitaNgRouterStoreModule.forRoot(), 
    AkitaTypedFormModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Вот и всё! Теперь Akita готова к использованию в твоём приложении. Давай разберём как это делать на практике.

Создание Store

Основа управления состоянием в Akita - это Store. Store похож на обычный JavaScript объект, но с некоторыми дополнительными свойствами:

  • Хранит данные в строго типизированном виде
  • Позволяет изменять данные только через специальные методы
  • Может отслеживать изменения и выполнять действия при обновлении
  • Даёт доступ к данным через удобные запросы

Давай создадим простой Store для хранения списка товаров в корзине:

import { Injectable } from '@angular/core';
import { Store, StoreConfig } from '@datorama/akita';

export interface CartState {
  items: string[];
}

export function createInitialState(): CartState {
  return {
    items: []
  };
}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'cart' })
export class CartStore extends Store<CartState> {

  constructor() {
    super(createInitialState());
  }
  
}

Вот что тут происходит:

  1. Описываем интерфейс для состояния
  2. Создаём initialState - начальное состояние
  3. Создаём класс Store, наследуя от базового класса Akita
  4. В конструктор передаём начальное состояние

Теперь у нас есть store для хранения данных о корзине! Давай посмотрим как им пользоваться.

Действия и мутации

Чтобы изменить данные в store, нужно выполнить действие и совершить мутацию.

Действие - это просто функция, которая описывает какие изменения нужно сделать.

Например, добавим действие для добавления товара в корзину:

export class CartStore extends Store<CartState> {

  addItem(item: string) {
    this.update(state => {
      return {
        items: [...state.items, item]  
      };
    });
  }

}

Мутация - это функция, которая на самом деле модифицирует состояние.

В нашем случае это метод update из базового класса Store. Он принимает callback, который возвращает новое состояние.

Таким образом мы добавляем новый товар в массив items. При этом старый массив не мутируется, т.к создаётся новый с использованием оператора spread. Это очень важно для производительности и предсказуемости!

Чтобы вызвать это действие, делаем так:

this.cartStore.addItem('Новый товар');

Теперь данные в store обновятся иммутабельным способом!

Запросы к хранилищу

Чтобы получить данные из store, используются селекторы.

Селектор - это просто функция, которая принимает состояние и возвращает нужную часть:

export class CartStore extends Store<CartState> {

  items$ = this.select(state => state.items);
  
}

А в компоненте достаточно подписаться на этот селектор:

constructor(private cartStore: CartStore) {}

ngOnInit() {
  this.items$ = this.cartStore.items$;
}

И использовать в шаблоне через async pipe:

<div *ngFor="let item of items$ | async">
  {{ item }}  
</div>

Akita автоматически отследит изменения в store и обновит данные в шаблоне! Очень удобно и эффективно.

Асинхронные операции

А что если нужно выполнить асинхронный запрос при обновлении store? Например, сохранить корзину на сервере?

Для этого есть метод setLoading(true) который позволяет отследить загрузку:

saveCart() {
  this.cartStore.setLoading(true);
  
  this.http.post('/save', this.cartStore.getValue()).pipe(
   tap(() => this.cartStore.setLoading(false)) 
  );
}

А в компоненте можно отслеживать loading$:

<div *ngIf="loading$ | async"> Saving...</div>

Таким образом мы можем элегантно обрабатывать асинхронные операции и обновлять UI accordingly.

Пример использования в проекте

Давай я покажу как мы использовали Akita в одном из своих проектов.

У нас был интернет магазин, и нам нужно было реализовать корзину и списки желаний. Вот как мы это сделали:

1. Создали CartStore для хранения товаров в корзине:

export interface CartState {
  items: CartItem[];
}

export class CartStore extends Store<CartState> {
  // ...действия, мутации, запросы 
}

2. Создали WishListStore для хранения избранных товаров:

export interface WishListState {
  items: string[]; 
}

export class WishListStore extends Store<WishListState> {
  // ...
}

3. В сервисе для работы с API делали запросы и обновляли store:

updateCart(items) {
  this.cartStore.update(state => ({
    items  
  }));
}

addToWishList(product) {
  this.wishListStore.addItem(product);
}

4. В компонентах подписывались на нужные селекторы и отображали данные.

5. Для общих данных использовали селекторы из EntityService.

Таким образом мы разделили всю логику на небольшие части и создали чистое управление состоянием!

Итог

В этой статье мы разобрали:

  • Зачем нужно управлять состоянием в приложениях
  • Как использовать Akita для этого в Angular
  • Как создавать Store, выполнять действия и мутации
  • Как делать запросы и обрабатывать асинхронные операции
  • Пример интеграции Akita в реальном проекте

Главное - Akita позволяет организовать предсказуемое и эффективное управление состоянием без лишней сложности. А это очень важно для современных веб-приложений! Надеюсь, эта статья была полезна и понятна.