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

в 0:23, , рубрики: angular, AngularJS, javascript, mobx, ngrx, rxjs, state

State Managment

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

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

Два наиболее популярных решения это ngrx/store, вдохновленной по большей части Redux, и Observable сервисы данных.

Лично мне очень нравится Redux, и он стоит каждой строчки бойлерплейт кода. Но, к сожалению, некоторе со мной могут не согласиться или Redux не особо применим в их приложениях.

Поэтому я решил поведать вам, как может пригодится Mobx, в решении проблемы управления состоянием. Идея заключается в том, чтобы объединить два мира, Redux и Mobx.

Итак, давайте возьмем иммутабельность Redux, мощь Rx+ngrx, и возможности управления состоянием Mobx. Эта комбинация позволит на использовать асинхронные пайпы в сочетании с OnPush стратегией, чтобы достичь наибольшей производительности.

Перед тем как мы начнем, подразумевается, что у вас есть достаточные знания по Mobx и Angular.

Для простоты мы будем создавать традиционное туду приложение. Ну что ж, начнем?

Сторы

Я хочу придерживаться принципа единой ответственности, поэтому я создаю сторы для фильтра и тудушек (вы можете объединить их в один, если нужно).

Давайте создадим стор фильтра.

import { Injectable } from '@angular/core';
import { action, observable} from 'mobx';

export type TodosFilter = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';

@Injectable()
export class TodosFilterStore {
  @observable filter = 'SHOW_ALL';

  @action setFilter(filter: TodosFilter) {
    this.filter = filter;
  }

}

Добавим стор доя тудушек.

export class Todo {
  completed = false;
  title : string;

  constructor( { title, completed = false } ) {
    this.completed = completed;
    this.title = title;
  }
}

@Injectable()
export class TodosStore {

  @observable todos: Todo[] = [new Todo({ title: 'Learn Mobx' })];

  constructor( private _todosFilter: TodosFilterStore ) {}

  @action addTodo( { title } : Partial<Todo> ) {
    this.todos = [...this.todos, new Todo({ title })]
  }

  @computed get filteredTodos() {
    switch( this._todosFilter.filter ) {
      case 'SHOW_ALL':
        return this.todos;
      case 'SHOW_COMPLETED':
        return this.todos.filter(t => t.completed);
      case 'SHOW_ACTIVE':
        return this.todos.filter(t => !t.completed);
    }
  }

}

Если вам знаком Mobx, код выше вам покажется довольно простым.

На заметку, хорошая практика всегда использовать @action декоратор. Он помогает придерживаться концепции "Не меняй стейт напрямую", известной нам еще с Redux. В доках Mobx сказано:

В strict режиме не допускается менять стейт за пределами экшена.

RxJS Мостик

Одна из крутых штук RxJS это возможность конвертировать любой источник данных в RxJS Observable. В нашем случае, мы будем использовать computed функцию из Mobx, чтобы слушать изменение стейта и отдавать нашим подписчикам в Observable.

import { Observable } from 'rxjs/Observable';
import { computed } from 'mobx';

export function fromMobx<T>( expression: () => T ) : Observable<T> {

  return new Observable(observer => {
    const computedValue = computed(expression);
    const disposer = computedValue.observe(changes => {
      observer.next(changes.newValue);
    }, true);

    return () => {
      disposer && disposer();
    }
  });
}

В Rx computed что то вроде BehaviorSubject вперемешку с distinctUntilChanged()

Каждый раз, когда происходит изменение (изменение по ссылке) в выражении, выполняется коллбэк, который передает новое значение нашим подписчикам. Теперь у нас есть мостик между Mobx и Rx.

Компонент тудушки

Давайте создадим туду компонент, который принимает Input() и эмитит событие когда выбран.
Обратите внимание, что здесь мы используем onPush стратегию для определения изменений.

@Component({
  selector: 'app-todo',
  template: `
    <input type="checkbox" 
           (change)="complete.emit(todo)" 
           [checked]="todo.completed">
    {{todo.title}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  @Input() todo: Todo;
  @Output() complete = new EventEmitter();
}

Компонент списка тудушек

Давайте создадим компонент списка тудушек, который принимает Input() и эмитит событие когда что то выбрано.
Обратите внимание, что здесь мы используем onPush стратегию для определения изменений.

@Component({
  selector: 'app-todos',
  template: `
    <ul>
      <li *ngFor="let todo of todos">
        <app-todo [todo]="todo" 
                  (complete)="complete.emit($event)">
        </app-todo>
      </li>
    </ul>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos: Todo[] = [];
  @Output() complete = new EventEmitter();
}

Компонент страницы тудушек

@Component({
  selector: 'app-todos-page',
  template: `
   <button (click)="addTodo()">Add todo</button> 
   <app-todos [todos]="todos | async"   
              (complete)="complete($event)">
    </app-todos>
  `
})
export class TodosPageComponent {
  todos : Observable<Todo[]>;

  constructor( private _todosStore: TodosStore ) {
  }

  ngOnInit() {
    this.todos = fromMobx(() => this._todosStore.filteredTodos);
  }
  addTodo() {
    this._todosStore.addTodo({ title: `Todo ${makeid()}` });
  }
}

Если вы работали с ngrx/store, вы будете чувствовать себя как дома. Свойство todos это Rx Observable и будет срабатывать, только когда произойдет изменение filteredTodos свойства в нашем сторе.

Свойство filteredTodos это computed значение, которое тригерит изменение если произойдет чистое изменение в filter или в todos свойстве нашего стора.

Ну и конечно же мы получаем все плюшки Rx такие как combineLatest(), take() и т.д, так как теперь это Rx поток.

Это все. Вот вам готовый пример.

https://stackblitz.com/edit/angular-mobx-netanel-xcvpmt

Думаю вам понравился этот небольшой концепт, надеюсь вам было интересно.

заметил очепятку, в личку

Автор: Николай Костюрин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js