MobX — управление состоянием приложения
17.12.2021
Теги: Frontend • JavaScript • React.js • Web-разработка • Модуль • Состояние
MobX — это проверенная в боевых условиях библиотека, которая делает управление состоянием простым и масштабируемым за счет прозрачного применения функционального реактивного программирования (TFRP). Философия MobX проста — все, что может быть получено из состояния приложения, должно быть получено автоматически. Рассмотрим для начала простейший пример таймера.
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
import timerStore from './TimerStore.js' import TimerView from './TimerView.js' setInterval(() => { timerStore.increase() }, 1000) export default function App() { return <TimerView timerState={timerStore} /> }
import { observer } from 'mobx-react' const TimerView = observer((props) => { const { timerState } = props return ( <> <h3>Seconds: {timerState.seconds}</h3> <button onClick={() => timerState.reset()}>Reset timer</button> </> ) }) export default TimerView
import { makeAutoObservable } from 'mobx' class TimerStore { seconds = 0 constructor() { makeAutoObservable(this) } increase() { this.seconds++ } reset() { this.seconds = 0 } } const timerStore = new TimerStore() export default timerStore
Обертка observer
вокруг компонента TimerView
автоматически определит, что рендер компонента зависит от observable-переменной timerState.seconds
, хотя это явно не определено. Когда эта переменная изменится, MobX позаботится о новом рендере компонента TimerView
.
Каждое событие (onClick
, setInterval
) вызывает действие (increase
, reset
), обновляющее наблюдаемое состояние (seconds
). Изменения наблюдаемого состояния распространяются на все вычисления и побочные эффекты (рендер TimerView
), которые зависят от вносимых изменений.
Основные концепции
У MobX их всего три и они довольно простые:
- Состояние — это данные, которые управляют приложением
- Действие — любой кусок кода, который изменяет состояние
- Производные — все, что может быть получено из состояния
1. Ослеживание изменения состояния
Чтобы следить за изменением состояния, каждое значение состояния должно быть отмечено как observable
:
import { makeObservable, observable, action } from 'mobx' class TodoItemStore { id = Math.random() title = '' finished = false constructor(title) { makeObservable(this, { title: observable, finished: observable, toggle: action }) this.title = title } toggle() { this.finished = !this.finished } }
2. Обновление состояния с помощью действий
Метод toggle
изменяет observable-значение состояния, поэтому отмечен как action
(действие). Использование action
помогает структурировать код и предотвращает непреднамеренное изменение состояния, когда это не планировалось.
3. Производные, которые автоматически реагируют на изменения состояния
Все, что может быть получено из состояния без дальнейшего взаимодействия, является производным. Производные существуют во многих формах:
- Пользовательский интерфейс
- Производные данные, например количество оставшихся
todos
- Бэкэнд-интеграции, например, отправка изменений на сервер
MobX различает два вида производных:
- Вычисляемые значения — которые всегда можно получить из текущего состояния с помощью чистой функции
- Реакции — побочные эффекты, которые должны происходить автоматически при изменении состояния
3.1. Вычисляемое значение
Чтобы создать вычисляемое значение, нужно создать свойство, используя функцию-геттер get
и пометить его как computed
:
import { makeObservable, observable, computed } from 'mobx' class TodoListStore { todos = [] constructor(todos) { makeObservable(this, { todos: observable, unfinishedTodoCount: computed }) this.todos = todos } get unfinishedTodoCount() { return this.todos.filter(todo => !todo.finished).length } }
MobX обеспечит автоматическое обновление свойства unfinishedCount
при добавлении задачи или изменении finished
одного из элементов todo-списка.
3.2. Реакции (побочные эффекты)
Чтобы в пользователь мог увидеть изменение состояния или вычисленных значений на экране, необходима реакция, которая перерисовывает часть графического интерфейса.
Реакции аналогичны вычисляемым значениям, но вместо получения информации они производят побочные эффекты, такие как вывод в консоль, выполнение сетевых запросов, инкрементное обновление дерева компонентов React для исправления DOM и т.д.
Чтобы сделать компоненты React реактивными, их нужно обернуть функцией observer
из пакета mobx-react
или mobx-react-lite
. Давайте соберем в единое целое пример с TodoList
, чтобы посмотреть все в действии.
Файл src/App.js
:
import TodoListStore from './todo/TodoListStore.js' import TodoListView from './todo/TodoListView.js' import TodoItemStore from './todo/TodoItemStore.js' const todoListStore = new TodoListStore([ new TodoItemStore('Первая задача'), new TodoItemStore('Вторая задача'), new TodoItemStore('Третья задача'), ]) export default function App() { return <TodoListView todoListState={todoListStore} /> }
Файл src/todo/TodoListStore.js
:
import { makeObservable, observable, action, computed } from 'mobx' import TodoItemStore from './TodoItemStore.js' class TodoListStore { todos = [] constructor(todos) { makeObservable(this, { todos: observable, unfinishedCount: computed, append: action, remove: action, }) this.todos = todos } get unfinishedCount() { return this.todos.filter(todo => !todo.finished).length } append = (title) => { this.todos.push(new TodoItemStore(title)) } remove = (id) => { this.todos = this.todos.filter(todo => todo.id !== id) } } export default TodoListStore
Файл src/todo/TodoItemStore.js
:
import { makeObservable, observable, action } from 'mobx' class TodoItemStore { id = Math.random() title = '' finished = false constructor(title) { makeObservable(this, { title: observable, finished: observable, toggle: action, rename: action, }) this.title = title } toggle = () => { this.finished = !this.finished } rename = (title) => { this.title = title } } export default TodoItemStore
Файл src/todo/TodoListView.js
:
import { observer } from 'mobx-react' import TodoItemView from './TodoItemView.js' const TodoListView = observer(({ todoListState }) => { const onAppend = () => { todoListState.append(prompt('Новая задача', 'Новая задача')); } return ( <div> <ul> {todoListState.todos.map(todo => ( <TodoItemView key={todo.id} todoItemState={todo} onRemove={todoListState.remove} /> ))} </ul> <p>Осталось задач: {todoListState.unfinishedCount}</p> <button onClick={onAppend}>Добавить задачу</button> </div> ) }) export default TodoListView
Файл src/todo/TodoItemView.js
:
import { observer } from 'mobx-react' const TodoItemView = observer((props) => { const {todoItemState, onRemove} = props const onRename = (title) => { todoItemState.rename(prompt('Изменить название', title)); } return ( <li> <input type="checkbox" checked={todoItemState.finished} onChange={() => todoItemState.toggle()} /> {todoItemState.title} <button onClick={() => onRename(todoItemState.title)}>Изменить</button> <button onClick={() => onRemove(todoItemState.id)}>Удалить</button> </li> ) }) export default TodoItemView
Исходные коды здесь, демо-сайт здесь.
Обертка observer
(HOC, Higher Order Component) автоматически подписывает компонент на все observable-данные, которые используются при рендере. В результате, при изменении любого observable-свойства, от которых зависит компонент, будет выполнен повторный рендер.
Когды мы отмечаем checkbox
, что задача завершена, это вызовет рендер компонента TodoItemView
и рендер компонента TodoListView
. Но рендер компонента TodoListView
будет вызван по той причине, что изменилось количество незавершенных задач. Если из компонента TodoListView
убрать код «Осталось задач» — то и рендера TodoListView
не будет.
В этом легко убедиться, если добавить console.log()
в компоненты TodoListView
и TodoItemView
:
const TodoListView = observer(({ todoListState }) => { /* .......... */ console.log('TodoListView render') /* .......... */ })
const TodoItemView = observer((props) => { /* .......... */ console.log('TodoItemView render') /* .......... */ })
Для работы observer
не важно, как именно observable-данные поступают в компонент — через пропсы, глобальные переменные, через react-контекст.
Вместо заключения
Вместо makeObservable
можно использовать makeAutoObservable
. В этом случае все собственные свойства будет отмечены как observable
. Свойства, у которых есть геттер get
— будут отмечены как computed
. Свойства, у которых есть сеттер set
— будут отмечены как action
.
Поиск: Frontend • JavaScript • React.js • Web-разработка • Модуль • Состояние • State • MobX