DevGang
Авторизоваться

MobX State Tree (MST) - Управление состоянием 

Привет, разработчики! Все используют различные библиотеки управления состоянием в своем приложении, многие из нас уже используют Redux, Redux Saga, Redux Rematch. Сегодня мы рассмотрим MobX, самую популярную альтернативу Redux. MobX State Tree (MST) - это мощная библиотека управления состоянием, которую вы можете использовать как в малых, так и в корпоративных приложениях, и ее очень просто подключить и запустить. Я проведу вас от базовой концепции к интеграции на уровне компонентов. Итак, продолжим.

Что мы будем освещать?

  1. Что такое MobX-State-Tree?
  2. Почему я должен использовать MobX-State-Tree?
  3. Установка MobX-State-Tree
  4. Начало работы - MobX Entity
  5. Создание модели
  6. Создание экземпляра модели
  7. Типы встреч
  8. Изменение данных
  9. Снапшот
  10. Снапшот к модели
  11. Приступаем к пользовательскому интерфейсу
  12. Повышение производительности рендеринга
  13. Вычисляемые свойства
  14. Рекомендации

Что такое 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 имеет много свойств по сравнению с другими средствами управления состоянием, давайте проверим несколько из них,

  1. MST предлагает отличную совместимость с React Native, ReactJS, VueJS, AngularJS и другими приложениями JavaScript.
  2. Вместо беспорядочного кода в приложении, MST предоставляет централизованные хранилища для быстрого доступа и обмена данными.
  3. Инкапсуляция - Ваши данные не могут быть изменены извне, они могут быть изменены в «действиях». Таким образом, к нему легко получить доступ, но он защищен от доступа извне.
  4. Проверка типов во время выполнения - поможет вам написать чистый код и не даст пользователям назначать неверные данные дереву.
  5. Все, что вы изменяете в состоянии, отслеживается, и вы можете создать снапшот своего состояния в любое время.

Установка 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 - имя пользователя.

Итак, наши сущности будут выглядеть примерно так,

Задача

  1. taskName
  2. taskStatus

Пользователь

  1. userID
  2. 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 - приложение со ссылками, идентификаторами, маршрутизацией, тестированием и т. д.

Спасибо, что прочитали статью

Источник:

#JavaScript #React #ReactNative #TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

В подарок 100$ на счет при регистрации

Получить