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 поток.


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


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


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

Комментарии (1)


  1. vdasus
    04.11.2017 22:33
    -1

    Читаю вот это и еще раз рад, что выбрал vue + vuex… Насколько проще и логичнее там все выглядит для не 100% фронтендера.