Что такое Mobx?

Mobx — это библиотека, дающая разработчикам инструмент для глобального использования переменных и методов между разными компонентами.

Mobx простая и понятная в использовании библиотека, использующая в своей реализации Context API и, по сравнению с Redux, требующая минимум шаблонного кода для инициализации стора.

Mobx сторы хорошо масштабируются и для каждого стора можно определить его область видимости, помещая соответствующие компоненты внутрь провайдеров стора.

Зачем использовать глобальные сторы?

Глобальный стор упрощает передачу параметров между компонентами, изменение которых вызывает ререндер активного интерфейса.

Также в сторах удобно хранить общие методы — будь то api‑запросы или парсеры данных — которые используются в разных компонентах, но которые сложно передавать передавать между ними по дереву компонентов.

Наглядно поток передачи параметров между компонентами без и с Mobx показан на слайде:

Из чего состоит Mobx хранилище

Mobx стор состоит из двух частей — самого стора с данными и провайдера этого стора.

Провайдер отвечает за хранение стора между ререндерами и за инкапсуляцию компонентов, которые будут использовать этот стор. Также через компонент провайдера передаются исходные значения для стора. А в файле с провайдером объявляется кастомный хук для получения сохраненного стора в компоненте.

Стор в сущности функция‑генератор, которая возвращает объект. Свойства и методы этого объекта будут доступны во всех компонентах внутри провайдера, и при изменениях этих свойств будет происходить ререндер виртуального DOM.

Файловую архитектуру лучше разграничить так, что после объявления провайдера и стора, файл провайдера изменяться не будет, но из него мы только будем импортировать компонент провайдера и хук возвращающий сохраненный в контексте стор.

Изменяться и дополняться новыми атрибутами, таким образом, будет только объект стора, находящийся в отдельном файле, но помните, что фактически в компоненты из него напрямую ничего импортироваться не будет.

Начало работы

Для того чтобы начать использовать Mobx в react-приложении его нужно установить:

npm i mobx mobx-react-lite

Затем чтобы объявить стор, создадим в корне проекта папку stores, и в ней папку для нового стора - AppStore. В этой папке создадим два файла - для провайдера и для обьекта-стора соответственно. Также можно добавить в папку index.js, чтобы задать импорт провайдера из папки AppStore по дефолту.

Файловая структура стора, таким образом, будет выглядеть так:

/stores

   /AppStore

     AppStoreProvider.jsx

     AppStore.js

     index.js

Объявление провайдера и стора

Давайте обьявим провайдер и стор, в котором будет глобальная переменная test и метод для ее изменения toggleTest:

AppStoreProvider.jsx

import { createContext, useContext } from 'react';
import { observer, useLocalObservable } from 'mobx-react-lite';
import { createAppStore } from './AppStore';

const Context = createContext(null);

export const AppStoreProvider = observer(({ children, ...props }) => {
  const store = useLocalObservable(() => createAppStore(props));
  return <Context.Provider value={store}>{children}</Context.Provider>;
});

export const useAppStore = () => {
  const store = useContext(Context);
  if (!store) throw new Error('Use App store within provider!');
  return store;
};

AppStore.js

export const createAppStore = (props) => {
  return {
    test: props.test || 'Hello world',
    toggleTest: function () {
      this.test = this.test === 'Hi!' ? 'How are you?' : 'Hi!';
    },
   
  };
};

После создания провайдера и стора нужно обернуть компоненты, использующие стор, в провайдер. Это нужно для доступа к объектам хранилища.

Сделать это можно где угодно, главное, чтобы компонент, который будет использовать стор имел на каком то уровне своим предком компонент провайдера. В большинстве приложений лучше оборачивать компоненты в провайдер на уровне корневого компонента приложения:

import React, {} from 'react';
import {AppStoreProvider} from '~store/AppStore'; // index.jsx
import {MyComponent} from './MyComponent';

export const App = () => {
  return (
    <AppStoreProvider>
      <MyComponent />
    </AppStoreProvider>
  );
}

Также, здесь мы можем передать исходные значения для стора:

Затем мы можем использовать стор в компоненте.

Для того, чтобы компонент ререндерился при изменении стора, его самого нужно обернуть в функцию observer из библиотеки mobx‑react‑lite.

Сохраненный стор получают из кастомного хука useAppStore:

import React from 'react';
import {observer} from 'mobx-react-lite';
import {useAppStore} from '~store/AppStore'; // index.jsx

export const MyComponent = observer(() => {
  const appStore = useAppStore();

  return (
    <>
      <h1>{appStore.test}</h1>
      <button onClick={() => appStore.toggleTest()}>Toggle</button>
    </>
  );
});

Локальное хранилище

Также с помощью Mobx можно создать локальное хранилище для отдельного компонента. Доступ к небу будет только у этого компонента. Это удобно, если логику нескольких useState нужно вынести за область компонента. Также в локальное хранилище можно вынести некоторые методы, чтобы разгрузить тело компонента. Для добавления локального хранилища его нужно инициализировать в компоненте:

const localStore = useLocalStore();

И объявить само хранилище в отдельном файле файле:

import { useLocalObservable } from 'mobx-react-lite';

const createLocalStore = (props) => {
  return {
    hello: props.hello || 'Hello world',
    toggleHello: function () {
      this.hello = this.hello !== 'Hi!' ? 'How are you?' : 'Hi!';
    },
   
  };
};

export const useLocalStore = (props = {}) => {
  return useLocalObservable(() => createLocalStore(props));
};

Подведем итог

На примере видно, как просто обьявить mobx стор и вызывать его в компоненте.

Определенно, преимуществом Mobx по сравнентю с Redux является простота - как в обьявлении так и в поддержании сторов. Цепочка вызова в Mobx простая - обьявляете переменную или метод в сторе и достаете ее из хука в компоненте - не нужно иметь дело ни с какими редисерами и мидлварами.

Так что используйте Mobx и избегайте редукса головного мозга :)

Ссылка на документацию:

https://mobx.js.org/react-integration.html

спасибо

Комментарии (7)


  1. DmitryKazakov8
    13.07.2023 16:44
    +4

    Спасибо за статью в популяризацию MobX, но многое не соответствует действительности.

    1. MobX - не библиотека глобальных сторов, и не библиотека локальных. Это система реактивности, когда можно подписаться на изменение параметров объекта.

    const data = observable({ test: 1 }); 
    autorun(() => console.log(data.test)); 
    data.test = 2; 
    // в консоли 1 и затем 2

    Но использовать этот механизм во фреймворках можно, в том числе для менеджеринга состояния (и в многочисленных других кейсах).

    1. "библиотека, использующая в своей реализации Context API" - нет, MobX не привязан к фреймворкам типа React и не тянет их зависимостью. Но может быть использован во фреймворках, в том числе для передачи его реактивных объектов в React Context. Можно и через другие провайдеры и инджекты.

    2. "Mobx сторы хорошо масштабируются" - масштабируются архитектурные подходы, а MobX просто реактивная обвязка над объектами.

    3. "Mobx стор состоит из двух частей — самого стора с данными и провайдера этого стора" - только первое) Провайдер к фреймворкам - это отдельная история. Лучше написать что "чтобы подключить к React через Context нужно завести кроме стора еще и провайдер".

    4. "Стор в сущности функция‑генератор, которая возвращает объект" - нет, это может быть просто объект как в моем примере выше, но самый эффективный подход - оформлять сторы в виде классов

    class Store { 
      constructor() { makeAutoObservable(this); }
    
      data = 1;
    
      toggle = () => { this.data = 2 } 
    }
    
    const store = new Store();

    Таким образом этот стор сам по себе реактивный и типизированный.

    1. Если делать классами, то соответственно будет биндинг контекста и не придется писать

    <button onClick={() => appStore.toggleTest()}>

    вызывая лишние ререндеры button, потому что анонимная функция будет создаваться новая на каждый рендер. А нужно будет писать

    <button onClick={appStore.toggleTest}>

    сохраняя равенство по ссылкам и оптимизируя перфоманс процесса reconciliation Реакта.


    1. Alexandroppolus
      13.07.2023 16:44

      Насчет примера из п.6 добавлю, что toggleTest надо сделать именно стрелочной функцией в классе, и с соответствующими параметрами (либо без параметров).


      1. DmitryKazakov8
        13.07.2023 16:44

        либо использовать одну из многих либ для автобиндинга, если это не стрелочная функция - в mobx тоже есть autoBind: true параметр, если нужно. Но лучше приучаться писать в методах класса стрелочные функции, так не нужны дополнительные либы и обертки - чисто семантика языка


  1. asakasinsky
    13.07.2023 16:44

    не нужно иметь дело ни с какими редисерами и мидлварами.

    Pipe сквозь middlewares мной использовалось часто, так, что это нифига не плюс.


    1. DmitryKazakov8
      13.07.2023 16:44

      Думаю, имелось в виду именно в контексте Redux про миддлвары. Иначе это нападка на Promise, который имеет паттерн миддлвара, или Express.


  1. nikfarce
    13.07.2023 16:44

    Mobx сторы хорошо масштабируются

    У вас есть личный опыт? Мне очень интересно как там с маштабируемостью у больший веб-приложений, использующий MobX. Поделитесь, пожалуйста)


    1. markelov69
      13.07.2023 16:44
      +2

      C 2016 года юзаю React + MobX по сей день и на огромных вэб-приложениях, реально огромных вообще 0 проблем, всё шикарно работает, быстро и стабильно!
      И вот как им пользоваться - https://codesandbox.io/s/adoring-banach-k8m9ss?file=/src/App.tsx