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

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 ) {
    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( expression: () => T ) : Observable {

  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: `
    
    {{todo.title}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  @Input() todo: Todo;
  @Output() complete = new EventEmitter();
}


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


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


@Component({
  selector: 'app-todos',
  template: `
    
`, changeDetection: ChangeDetectionStrategy.OnPush }) export class TodosComponent { @Input() todos: Todo[] = []; @Output() complete = new EventEmitter(); }


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


@Component({
  selector: 'app-todos-page',
  template: `
    
   
    
  `
})
export class TodosPageComponent {
  todos : Observable;

  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


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


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

© Habrahabr.ru