
Каждый разработчик знает, что управление состоянием довольно сложная штука. Постоянно отслеживать, что где и когда поменялось, это просто кошмар, особенно в больших приложениях.
В мире 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 поток.
Это все. Вот вам готовый пример.
Думаю, вам понравился этот небольшой концепт, надеюсь, вам было интересно.
заметил очепятку, в личку
