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