Вы находитесь на странице: 1из 99

MobX: простое

управление состоянием
веб-приложения
Азат Разетдинов, руководитель отдела разработки
интерфейсов персональных сервисов
CV

3
Состояние приложения
Текущий урл

Состояние Состояние на
Отображение
приложения сервере

Локальное
хранилище
4
Состояние — это дерево

5
Состояние — это живое дерево

6
Как отслеживать изменения
состояния?
Ручная подписка на изменения

1. Используем текущее состояние


2. Подписываемся на изменения
3. Реагируем на изменения

8
Ручная подписка на изменения

render(state);

state.on(‘change’, () => {
render(state);
});

9
Проблемы ручной подписки
на изменения
│ Переподписка:
│ отслеживание
│ изменения всего
│ дерева избыточно
│ Недоподписка:
│ отслеживание
│ отдельных полей
│ в конце концов ведёт
│ к неконсистентности
13
MobX:
автоматическая подписка
@observable
@observable

class Person {
@observable firstName = 'Vasya';
@observable lastName = 'Pupkin';
@observable nickName;

...

16
@observable

A B

1 First Name: Vasya

2 Last Name: Pupkin

3 Nickname:

17
│ @observable содержит
│ исходные данные
@computed
@computed

class Person {

...

@computed get fullName() {


return this.firstName + ' ' + this.lastName;
}

...

20
@computed

A B

1 firstName: Vasya

2 lastName: Pupkin

3 nickName:

= B1&" "&B2
4 fullName:

21
│ @computed зависит
│ от исходных данных
│ и возвращает
│ производные данные
@action
@action

class Person {

...

@action setNickName(nickName) {
this.nickName = nickName;
}
}

24
│ @action изменяет
│ исходные данные
http://www.reactiongifs.com/magic-3/ 26
Реакции
│ Реакции срабатывают
│ при каждом изменении
│ исходных данных,
│ от которых они зависят
autorun
autorun

autorun(() => {
console.log(person.nickName || person.fullName);
});

30
autorun

A B

1 firstName: Vasya

2 lastName: Pupkin

3 nickName:

= B1&" "&B2
4 fullName:

5 autorun: =IF(B3="",B4,B3)

31
autorun

autorun(() => {
console.log(person.nickName || person.fullName);
});

32
│ @observable
│ и @computed
│ используют геттеры для
│ подписки на изменения
autorun

autorun(() => {
console.log(person.nickName || person.fullName);
});

34
@computed

class Person {

...

@computed get fullName() {


return this.firstName + ‘ ‘ + this.lastName;
}

...

35
Наблюдаемые поля

› person.nickName
› person.fullName
› person.firstName
› person.lastName

36
Дерево зависимостей

firstName

fullName

lastName autorun

nickName

37
Уведомление
об изменении данных
@action

person.setNickName('Bacek');

39
@action

class Person {

...

@action setNickName(nickName) {
this.nickName = nickName;
}
}

40
│ @observable
│ использует сеттер
│ для уведомления
│ об изменении данных
autorun

autorun(() => {
console.log(person.nickName || person.fullName);
});

42
autorun

autorun(() => {
console.log(person.nickName || person.fullName);
});

43
Наблюдаемые поля

› person.nickName

Ненаблюдаемые поля

› person.fullName
› person.firstName
› person.lastName

44
Дерево зависимостей

firstName

fullName

lastName autorun

nickName

45
│ autorun при каждом
│ выполнении заново
│ собирает список
│ зависимостей
│ Минимальный набор
│ подписок может быть
│ получен только
│ в рантайме
@observer
@observer

@observer
class ProfileView extends React.Component {
render() {
const person = { this.props };
if (person.nickName) {
return <div>{person.nickName}</div>;
}
return <div>{person.fullName}</div>};
}
}

49
│ @observer
│ перерисовывывает
│ React-компонент
│ при изменении данных
@observer

@observer
class SomeComponent extends React.Component {

render() {
if (!this.props.isEnabled) {
return null;
}
... много кода ...
}

51
│ Динамическая подписка
│ кардинально снижает
│ число перерисовок
│ React-компонента
Кэширование
@computed
Кэширование @computed

class Person {

...

@computed get fullName() {


return this.firstName + ' ' + this.lastName;
}

...

54
Кэширование @computed

firstName

fullName

lastName autorun

nickName

55
│ @computed кэширует
│ своё значение, пока
│ есть хотя бы один
│ подписчик
Кэширование @computed

firstName

fullName

lastName autorun

nickName

57
│ При отсутствии
│ подписчиков
│ @computed работает
│ как обычный геттер
Асинхронные данные
Асинхронные данные

class Person {
bio: fromPromise(loadBio())
}

person.bio.state // pending|fulfilled|rejected
person.bio.value

60
Итого
Плюсы MobX

› Минимум бойлерплейта
› Автоматическая подписка в рантайме
› Производительность искаропке

62
Секундочку…
› А как же immutability?
› А как же time travel?
› А как же devtools?

64
Redux
Redux

› ООП → функциональное программирование


› Модели → immutable-структуры
› Методы → экшны + редьюсеры
› Связи → нормализация + селекторы

› Очень много бойлерплейта :(

66
67
© Metro-Goldwyn-Mayer
68
© Paramount Pictures
69
70
https://i.ytimg.com/vi/abLSL1eAE3U/maxresdefault.jpg
mobx-state-tree
│ mobx-state-tree
│ совмещает все плюсы
│ mobx и redux
types.model

const Todo = types.model({


});

73
types.string

const Todo = types.model({


title: types.string
});

74
types.optional

const Todo = types.model({


title: types.string,
isCompleted: types.optional(types.boolean, false)
});

75
types.optional

const Todo = types.model({


title: types.string,
isCompleted: false
});

76
types.reference

const Message = types.model({


title: types.string,
isCompleted: false,
folder: types.reference(Folder)
});

77
types.identifier()

const Folder = types.model({


id: types.identifier(),
title: types.string
});

78
types.array

const TodoStore = types.model({


todos: types.array(Todo),
folders: types.array(Folder)
});

79
views

const TodoStore = types.model({


todos: types.array(Todo),
folders: types.array(Folder)
}).views(self => ({
get completedTodos() {
return self.todos.filter(todo => {
return todo.isCompleted;
});
}
}));

80
actions

const Folder = types.model({


id: types.identifier(),
title: types.string
}).actions(self => ({
setTitle(title) {
self.title = title;
}
}));

81
Model.create

const data = {
todos: [{title: 'Выспаться', folder: '1'}],
folders: [{id: '1', title: 'Срочные дела'}]
};

const todoStore = TodoStore.create(data);

82
@observer

@observer
class TodoView extends React.Component {
render() {
const todo = { this.props };
...
<div class="title">{todo.title}</div>
<div class="folder">{todo.folder.title}</div>
...
}
}

83
84
https://i.ytimg.com/vi/abLSL1eAE3U/maxresdefault.jpg
© Cake Entertainment 85
getSnapshot

const snapshot = getSnapshot(todoStore);

86
│ getSnapshot
│ сериализует модель
│ в простой объект
onSnapshot

onSnapshot(todoStore, (snapshot) => {


console.dir(snapshot);
});

88
│ onSnapshot
│ генерирует снэпшоты
│ при каждом
│ изменении модели
│ Неизменившиеся
│ части дерева
│ реиспользуются
│ (structural sharing)
applySnapshot

applySnapshot(todoStore, snapshot);

91
│ applySnapshot
│ реиспользует модели
│ с совпадающими
│ идентификаторами
│ (reconciliation)
Живая модель и снэпшоты

https://youtu.be/iDbfWT-hSG8 93
Redux Store и Redux Devtools

const reduxStore = asReduxStore(todoStore);

connectReduxDevtools(require("remotedev"), todoStore);

94
Итого
Плюсы mobx-state-tree

› ООП-модели вместо immutable-структур


› Живые ссылки вместо селекторов
› Дешёвое получение снэпшотов всего дерева
› Применение снэпшотов с реконсайлингом
› Адаптеры для Redux Store и Redux Devtools

96
│ MobX —
│ Like React, but for Data

Daniel Earwicker
Chief Software Architect, FISCAL Technologies Ltd
Ссылки

› Mobx https://mobx.js.org
› Mobx-state-tree https://github.com/mobxjs/mobx-state-tree
› Becoming fully reactive: an in-depth explanation of MobX
https://clck.ru/B4DL9
› Mobx vs. Redux Performance: https://clck.ru/B4DW5
› An artificial example where MobX really shines and Redux is not
really suited for it https://clck.ru/B4DjN

98
Спасибо!
Азат Разетдинов
руководитель отдела разработки интерфейсов

azat@yandex-team.ru
@razetdinov