MobX — управление состоянием приложения

17.12.2021

Теги: FrontendJavaScriptReact.jsWeb-разработкаМодульСостояние

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-списка.

Эти вычисления напоминают формулы в программах для работы с электронными таблицами, таких как MS Excel. Они обновляются автоматически, но только при необходимости. То есть, если кто-то или что-то запрашивает их результат.

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

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.