MobX State Tree (MST) - Управление состоянием
Привет, разработчики! Все используют различные библиотеки управления состоянием в своем приложении, многие из нас уже используют Redux, Redux Saga, Redux Rematch. Сегодня мы рассмотрим MobX, самую популярную альтернативу Redux. MobX State Tree (MST) - это мощная библиотека управления состоянием, которую вы можете использовать как в малых, так и в корпоративных приложениях, и ее очень просто подключить и запустить. Я проведу вас от базовой концепции к интеграции на уровне компонентов. Итак, продолжим.
Что мы будем освещать?
- Что такое MobX-State-Tree?
- Почему я должен использовать MobX-State-Tree?
- Установка MobX-State-Tree
- Начало работы - MobX Entity
- Создание модели
- Создание экземпляра модели
- Типы встреч
- Изменение данных
- Снапшот
- Снапшот к модели
- Приступаем к пользовательскому интерфейсу
- Повышение производительности рендеринга
- Вычисляемые свойства
- Рекомендации
Что такое MobX-State-Tree?
MobX-State-Tree (MST) - это библиотека управления React состоянием. Это контейнерная система, построенная на MobX.
MobX - State Management Engine и MobX-State-Tree предоставляют структуру, которая имеет тип + состояние для хранения ваших данных. MST является наиболее предпочтительным для приложений уровня малого или корпоративного уровня, где код и функциональность будут периодически масштабироваться. По сравнению с Redux, MST предлагает высокую производительность и меньше строк кода.
MobX поддерживает множество функций для современной системы управления состоянием и все в одном пакете MobX, а не больше дополнительных зависимостей.
Почему я должен использовать MobX-State-Tree?
MST имеет много свойств по сравнению с другими средствами управления состоянием, давайте проверим несколько из них,
- MST предлагает отличную совместимость с React Native, ReactJS, VueJS, AngularJS и другими приложениями JavaScript.
- Вместо беспорядочного кода в приложении, MST предоставляет централизованные хранилища для быстрого доступа и обмена данными.
- Инкапсуляция - Ваши данные не могут быть изменены извне, они могут быть изменены в «действиях». Таким образом, к нему легко получить доступ, но он защищен от доступа извне.
- Проверка типов во время выполнения - поможет вам написать чистый код и не даст пользователям назначать неверные данные дереву.
- Все, что вы изменяете в состоянии, отслеживается, и вы можете создать снапшот своего состояния в любое время.
Установка MobX-State-Tree
Как мы уже обсуждали ранее, MobX - это управление состоянием, а MobeX-State-Tree дает вам структуру для хранения ваших данных. Итак, нам нужно установить mobx, mobx-state-tree.
NPM: npm install mobx mobx-state-tree --save
Yarn:yarn add mobx mobx-state-tree
Создадим приложение для React,npx create-react-app todo-app
Теперь давайте установим зависимость,npm install mobx mobx-state-tree mobx-react-lite
Запустить приложение ToDo,npm run start
Начало работы - MobX Entity
Начнем с создания приложения ToDo. В приложении ToDo есть две сущности: Задача и Пользователь. Сущность задачи имеет два атрибута: taskName - имя задачи, taskStatus - для определения завершенных задач. Сущность пользователя имеет два атрибута: userID - идентификатор пользователя, userName - имя пользователя.
Итак, наши сущности будут выглядеть примерно так,
Задача
- taskName
- taskStatus
Пользователь
- userID
- userName
Создание модели
Дерево = Тип + Состояние - Каждое дерево имеет форму (информацию о типе) и состояние (данные). Создаём модель с помощью types.model
import { types } from "mobx-state-tree"
const Task = types.model({
taskName: "",
taskStatus: false
})
const User = types.model({
userID: 1,
userName: ""
})
Создание экземпляра модели
Просто создайте экземпляр, вызвав .create ()
import { types, getSnapshot } from "mobx-state-tree"
const Task = types.model({
taskName: "",
taskStatus: false
})
const User = types.model({
userID: 1,
userName: ""
})
const kpiteng = User.create()
const articleWriting = Task.create({taskName: “Article Writing”})
console.log("User: kpiteng:", getSnapshot(kpiteng))
console.log("Task: articleWriting:", getSnapshot(articleWriting))
M
Типы встреч
MobX проверяет проверку типов во время выполнения, помогает разработчикам определять неправильные данные, переданные в аргументе. Это очень полезно, когда в крупномасштабном приложении задействовано несколько разработчиков.
const articleWriting = Task.create({ taskName: "Article Writing", taskStatus: 95 })
Здесь вы получите сообщение об ошибке, например, 95 не может быть назначен типу boolean, поскольку вы выбрали taskStatus как boolean, поэтому вы не можете передавать целочисленный тип данных.
const Task = types.model({
taskName: types.optional(types.string, ""),
taskStatus: types.optional(types.boolean, false)
})
const User = types.model({
userID: types.optional(types.number, 1),
userName: types.optional(types.string, "")
})
Пространство имен типов является производным от пакета MST. Вы можете проверить множество широко используемых типов, таких как массив, карта, возможность, объединение и многие другие. Вы можете проверить различные типы, доступные в MST.
Теперь пришло время создать корневую модель, давайте объединим модель задачи и пользователя.
import { types } from "mobx-state-tree"
const Task = types.model({
taskName: types.optional(types.string, ""),
taskStatus: types.optional(types.boolean, false)
})
const User = types.model({
userID: types.optional(types.number, 1),
userName: types.optional(types.string, "")
})
const RootStore = types.model({
users: types.map(User),
tasks: types.optional(types.map(Task), {})
})
const store = RootStore.create({
users: {}
})
Примечание. Если вы не передаете значение модели по умолчанию в .create (), вы должны указать значение по умолчанию для второго аргумента types.optional (arg1, arg2).
Изменение данных
MST - узел дерева изменяется только в действиях.
const Task = types
.model({
taskName: types.optional(types.string, ""),
taskStatus: types.optional(types.boolean, false)
})
.actions(self => ({
setTaskName(newTaskName) {
self.taskName = newTaskName
},
toggle() {
self.taskStatus = !self.taskStatus
}
}))
const User = types.model({
userID: types.optional(types.number, 1),
userName: types.optional(types.string, "")
});
const RootStore = types
.model({
users: types.map(User),
tasks: types.map(Task)
})
.actions(self => ({
addTask(userID, taskName) {
self.tasks.set(userID, Task.create({ taskName }))
}
}));
const store = RootStore.create({
users: {}
});
store.addTask(1, "Article Writing");
store.tasks.get(1).toggle();
render(
<div>{JSON.stringify(getSnapshot(store))}</div>,
document.getElementById("root")
);
/*
{
"users": {
},
"taks": {
"1": {
"taskName": "Article Writing",
"taskStatus": true
}
}
}
*/
Вы обратили внимание на self, self - обьект созданный при создании экземпляра вашей модели. Это бесплатно, вы можете получить к нему доступ, используя self.
Снапшот
Допустим, вы хотите увидеть значение, хранящееся в вашем состоянии, что означает взглянуть на снапшот. Просто используйте getSnapshot (). Каждый раз, когда вы обновляете свое состояние и хотите проверить, отражены ли изменения в состоянии, вы можете проверить это с помощью getSnapshot ().
Чтобы прослушать изменение состояния, используйте это - onSnapshot (store, snapshot => console.log (snapshot))
console.log(getSnapshot(store))
/*
{
"users": {},
"tasks": {
"1": {
"taskName": "Article Writing",
"taskCompleted": true
}
}
}
*/
Снапшот модели
На предыдущем шаге мы видим, что получили снапшот модели. Но возможно ли восстановить модель из снимка? Да все просто. Посмотрим как.
Перед этим я хотел бы связать этот процесс с Redux, чтобы вы быстро поняли. В Redux у нас есть Reducer, где у нас есть State - и мы инициализируем переменные State значениями по умолчанию, такими как users: [], tasks: []). Теперь, когда пользователь впервые открывает приложение, у нас нет ни одного снимка / пустого хранилища, поэтому хранилище будет пополняться с использованием значения модели по умолчанию (значение состояния по умолчанию). После взаимодействия с приложением вы обновили значения в магазине. Когда вы вернетесь в следующий раз, он получит данные из магазина и пополнит вашу модель / состояние. То же самое мы и сделаем здесь.
В MobeX мы можем добиться этого двумя разными способами: Первый - передать значение хранилища по умолчанию, Второй - передать хранилище и значение хранилища по умолчанию (значение снапшота).
// 1st
const store = RootStore.create({
users: {},
tasks: {
"1": {
taskName: "Article Writing",
taskStatus: true
}
}
})
// 2nd
applySnapshot(store, {
users: {},
tasks: {
"1": {
taskName: "Article Writing",
taskStatus: true
}
}
})
Приступаем к пользовательскому интерфейсу
Теперь пришло время поработать с пользовательским интерфейсом, чтобы подключить хранилище MST к компоненту React, нам потребовался mobex-react-lite. Мы собираемся использовать наблюдателя - имя само по себе говорит все. Это просто: он наблюдает за магазином и обновляет компоненты React / компоненты Render React всякий раз, когда что-либо изменяется в магазине.
import { observer } from 'mobx-react-lite'
import { values } from 'mobx'
const App = observer(props => (
<div>
<button onClick={e => props.store.addTask(randomId(), "Article Writing")}>Add Task</button>
{values(props.store.tasks).map(todo => (
<div>
<input type="checkbox" checked={task.taskStatus} onChange={e => task.toggle()} />
<input type="text" value={task.taskName} onChange={e => task.setTaskName(e.target.value)} />
</div>
))}
</div>
))
Повышение производительности рендеринга
В предыдущих шагах мы отрисовали Задачи - для каждой задачи мы дали возможность пометить ее как выполненную. Теперь каждый раз, когда мы проверяем / снимаем отметку с задачи, которую будет отображать наш пользовательский интерфейс, мы добавили наблюдателя. Обязанность наблюдателя обновлять компоненты, когда что-либо обновляется в магазине. Итак, как избежать этой ситуации повторного рендеринга. Это просто, посмотрим.
const TaskView = observer(props => (
<div>
<input type="checkbox" checked={props.task.taskStatus} onChange={e => props.task.toggle()} />
<input
type="text"
value={props.task.taskName}
onChange={e => props.task.setTaskName(e.target.value)}
/>
</div>
))
const AppView = observer(props => (
<div>
<button onClick={e => props.store.addTask(randomId(), "Article Writing")}>Add Task</button>
{values(props.store.tasks).map(task => (
<TaskView task={task} />
))}
</div>
))
У нас есть отдельная бизнес-логика для TaskView, обратите внимание - мы добавили наблюдателя в TaskView. Поэтому, когда кто-либо изменяет TaskStatus Check / UnCheck, отображается только TaskView. AppView повторно отображает только в случае добавления новой задачи или удаления существующей задачи.
Вычисляемые свойства
До предыдущих шагов мы показывали задачи, добавленные пользователем. Что мне нужно сделать, чтобы показать количество выполненных задач и ожидающих задач? С MobX это просто, добавьте свойство getter в нашу модель, вызвав .views, он посчитает, сколько задач выполнено и ожидает выполнения. Посмотрим код.
const RootStore = types
.model({
users: types.map(User),
tasks: types.map(Task),
})
.views(self => ({
get pendingTasksCount() {
return values(self.tasks).filter(task => !task.taskStatus).length
},
get completedCount() {
return values(self.tasks).filter(task => task.done).length
}
}))
.actions(self => ({
addTask(userID, taskName) {
self.tasks.set(userID, Task.create({ taskName }))
}
}))
const TaskCountView = observer(props => (
<div>
{props.store.pendingTaskCount} Pending Tasks, {props.store.completedTaskCount} Completed Tasks
</div>
))
const AppView = observer(props => (
<div>
<button onClick={e => props.store.addTask(randomId(), "Article Writing")}>Add Task</button>
{values(props.store.tasks).map(task => (
<TaskView task={task} />
))}
<TaskCountView store={props.store} />
</div>
))
Рекомендации
Теперь пришло время назначить пользователя для каждой Задачи в Задачах. Для этого нам нужно указать MST, который является уникальным атрибутом (первичным ключом на языке db) в каждом экземпляре модели User. Вы можете реализовать это с помощью автора типов types.identifier.
const User = types.model({
userID: types.identifier,
userName: types.optional(types.string, "")
})
Теперь нам нужно определить ссылку на модель задачи. Все просто - это можно сделать с помощью types.reference (User). Часто это циклическая ссылка, поэтому для ее решения нам нужно использовать types.late (() => User). Возможно, запись пользователя найдена нулевой, чтобы решить, что нам нужно использовать type.maybe (...), поэтому, наконец, давайте посмотрим, как выглядит код,
const Task = types
.model({
taskName: types.optional(types.string, ""),
taskStatus: types.optional(types.boolean, false),
user: types.maybe(types.reference(types.late(() => User)))
})
.actions(self => ({
setTaskName(newTaskName) {
self.taskName = newTaskName
},
setUser(user) {
if (user === "") {
self.user = undefined
} else {
self.user = user
}
},
toggle() {
self.taskStatus = !self.taskStatus
}
}))
const UserPickerView = observer(props => (
<select value={props.user ? props.user.userID : ""} onChange={e => props.onChange(e.target.value)}>
<option value="">-none-</option>
{values(props.store.users).map(user => (
<option value={user.id}>{user.name}</option>
))}
</select>
))
const TaskView = observer(props => (
<div>
<input type="checkbox" checked={props.task.taskStatus} onChange={e => props.task.toggle()} />
<input
type="text"
value={props.task.name}
onChange={e => props.task.setName(e.target.value)}
/>
<UserPickerView
user={props.task.user}
store={props.store}
onChange={userID => props.task.setUser(userID)}
/>
</div>
))
const TaskCountView = observer(props => (
<div>
{props.store.pendingTaskCount} Pending Tasks, {props.store.completedTaskCount} Completed Tasks
</div>
))
const AppView = observer(props => (
<div>
<button onClick={e => props.store.addTask(randomId(), "Article Writting")}>Add Task</button>
{values(props.store.tasks).map(task => (
<TaskView store={props.store} task={task} />
))}
<TaskCountView store={props.store} />
</div>
))
Мы рассмотрели почти все необходимые темы из MobeX-State-Tree. MobeX предоставил несколько примеров, загрузите ToDoMVC - приложение, использующее React и MST, и Bookshop - приложение со ссылками, идентификаторами, маршрутизацией, тестированием и т. д.
Спасибо, что прочитали статью