Сегодня я хочу рассказать вам о том, как на нашем проекте состоялся переход на Mobx, какие преимущества это даёт. Также будет показан типовой проект и даны пояснения по основным вопросам. Но сначала вводные.

image

Почему вообще надо на что-то переходить? На самом деле, ответ на этот вопрос — уже половина дела. Многие сейчас любят применять новые технологии только потому, что они новые. Хорошая строчка в резюме, возможность саморазвития, быть в тренде. Здорово, когда можно просто идти вперёд.

И всё же, каждый инструмент должен решать свои задачи, и от них мы так или иначе отталкиваемся, когда пишем коммерческий код.

У нас в проекте существует некоторое количество виджетов, куда пользователь вводит свои данные, взаимодействует с формами. Как правило, в каждом виджете несколько экранов. Когда-то давно это всё работало на старом добром шаблонизаторе MarkoJS + обязательный jQuery на клиенте. Взаимодействие с формами писалось в императивном стиле, if… else, коллбэки и вот это всё самое то, что уже, кажется, осталось в прошлом.

Потом настало время React. Бизнес-логика на клиенте всё утолщалась, вариантов взаимодействий становилось много, императивный код превращался в сложно устроенную кашу. Декларативный react-код оказался гораздо удобнее. Можно было, наконец, сконцентрироваться на логике, а не представлении, переиспользовать компоненты и легко распределять задачи по разработке новых фич между разными сотрудниками.

Но и приложение на чистом React со временем упирается в ощутимые границы. Конечно, нам надоедает писать this.setState на каждый чих и думать про его асинхронность, но особенные трудности доставляет пробрасывание данных и коллбэков через толщу компонентов. Короче, настаёт момент окончательно разделить данные и представление. Не вопрос, можно исхитриться сделать это на чистом React, но в индустрии в последнее время популярны фреймворки, реализующие Flux-архитектуру фронт-енд приложения.

У нас, если судить по количеству статей и упоминаниям в вакансиях, наиболее известен Redux. Собственно, я уже занёс руку, чтобы инсталлировать его в наш проект и начать разработку, как в самый последний момент (и это буквально!) чёрт дёрнул полистать Хабр, а тут как-раз шло обсуждение темы «Redux или Mobx?» Вот эта статья: habr.com/ru/post/459706. Прочитав её, а также все комментарии под ней, я понял, что всё-таки буду использовать Mobx.

Итак, ещё раз. Ответ на самый главный вопрос – зачем всё это? – выглядит так: настало время разделить представление и данные, управление данными хотелось бы построить в декларативном стиле (как и отрисовку), никакого перекрёстного опыления коллбэками и пробрасываемыми атрибутами.

Теперь мы готовы приступить.

1. О приложении


Нам требуется на фронте построить некий конструктор экранов и форм, которые потом можно было бы быстро тасовать, соединять друг с другом вслед за меняющимися требованиями бизнеса. Это неизбежно подвигает нас к следующему: создать коллекцию абсолютно изолированных компонентов, а также некий основной компонент, соответствующий каждому из наших виджетов (по сути это отдельные SPA, создаваемые каждый раз под новый бизнес-кейс в общем приложении).

В примерах будет показана урезанная версия одного из таких мини-приложений. Чтобы не громоздить лишний код, пусть это будет форма из трёх полей ввода и кнопки.

2. Данные


Mobx по сути не является фреймворком, это всего лишь библиотека. В руководстве прямо сказано, что она не организует ваши данные напрямую. Вы сами должны придумать такую организацию. Кстати, мы используем Mobx 4, потому что версия 5 использует тип данных Sybmol, который, к сожалению, поддерживается не всеми браузерами.

Итак, все данные выделяются в отдельные сущности. Наше приложение стремится к набору из двух папок:
components, куда мы положим все view
stores, где будут содержаться данные, а также логика работы с ними.
Например, типовой компонент для ввода данных у нас состоит из двух файлов: Input.js и InputStore.js. Первый файл – это глупый компонент React, отвечающий строго за отображение, второй – данные этого компонента, правила работы с пользователем (onClick, onChange, etc...)

Прежде чем мы перейдём непосредственно к примерам, нужно решить ещё один важный вопрос.

3. Управление


Хорошо, у нас полностью автономные компоненты View-Store, но как у нас всё это срастается в целое приложение? Для отображения у нас будет корневой компонент App.js, а для управления потоками данных основное хранилище mainStore.js. Принцип простой: mainStore знает всё обо всех хранилищах всех нужных компонентов (ниже будет показано, как это достигается). Другие хранилища ничего не знают об окружающем мире вообще (ну, будет одно исключение — словари). Таким образом, мы гарантированно знаем, куда идут наши данные и где их перехватывать.

image

mainStore декларативно, через изменение частей своего state может управлять остальными компонентами. На следующем рисунке Actions и State относится к хранилищам компонентов, а Computed values относится к mainStore:

image

Начнём писать код. Основной файл приложения index.js:

import React from "react";
import ReactDOM from "react-dom";
import {Provider} from "mobx-react";
import App from "./components/App";
import mainStore from "./stores/mainStore";
import optionsStore from "./stores/optionsStore";
// для IE11
require("es6-object-assign").polyfill();
require( "./static/less/main.less");

const stores = {
    mainStore,
    optionsStore,
    ButtonStore : mainStore.ButtonStore,    
    FioStore : mainStore.FioStore,
    EmailStore : mainStore.EmailStore
};

ReactDOM.render((
    <Provider {...stores}>
        <App />
    </Provider>
), document.getElementById('reactContainer'));

Здесь видна основная концепция Mobx. Данные (stores) доступны в любом месте приложения через механизм Provider. Мы оборачиваем наше приложение, перечисляя необходимые в работе хранилища. Для использования Provider подключаем модуль mobx-react. Для того, чтобы основное управляющее хранилище mainStore со старта имело доступ ко всем остальным данным, делаем инициализацию дочерних хранилищ внутри mainStore:

// mainStore.js

import optionsStore from "./optionsStore";
import ButtonStore from "./ButtonStore";
import FioStore from "./FioStore";
import EmailStore from "./EmailStore";
....

class mainStore {
    constructor() {
        /**
         * Инициализация дочерних хранилищ
         */
        this.ButtonStore = new ButtonStore();      
        this.FioStore = new FioStore();
        this.EmailStore = new EmailStore();       

    ...


Теперь App.js, скелет нашего приложения

import React from "react";
import {observer, inject} from "mobx-react";
import ButtonArea from "./ButtonArea";
import Email from "./Email";
import Fio from "./Fio";
import l10n from "../../../l10n/localization.js";

@inject("mainStore")
@observer
export default class App extends React.Component {
    constructor(props) {
        super(props);       
    };

    render() { 
        const mainStore = this.props.mainStore;
    
        return (
            <div className="container">
                <Fio
                    label={l10n.ru.profile.name}
                    name={"name"}
                    value={mainStore.userData.name}
                    daData={true}                
                />
                <Fio
                    label={l10n.ru.profile.surname}
                    name={"surname"}
                    value={mainStore.userData.surname}
                    daData={true}                  
                />
               <Email
                    label={l10n.ru.profile.email}
                    name={"email"}
                    value={mainStore.userData.email}                     
                />
                <ButtonArea />                
            </div>            
        );
    }
}

Тут ещё две основные концепции Mobx – inject и observer.
inject внедряет только необходимый store в приложении. Разные части нашего приложения используют разные хранилища, которые мы перечисляем в inject через запятую. Естественно, подключаемые хранилища должны быть изначально перечислены в Provider. Хранилища доступны в компоненте через this.props.yourStoreName.
observer – декоратор указывает, что наш компонент будет подписан на данные, которые изменяются с помощью Mobx. Данные изменились – в компоненте возникла реакция (ниже будет показано, как). Таким образом, никаких специальных подписок и коллбэков – Mobx доставляет изменения сам!

К управлению всем приложением в mainStore мы ещё вернёмся, а пока сделаем компоненты. У нас их три вида – Fio, Email, Button. Пусть первый и третий будут универсальными, а Email – кастомный. С него и начнём.

За отображение отвечает обычный глупый React-компонент:

Email.js
import React from "react";
import {inject, observer} from 'mobx-react';

@inject("EmailStore")
@observer
export default class Email extends React.Component {

    constructor(props) {
        super(props);        
    }; 

    componentDidMount = () => {        
        this.props.EmailStore.validate(this.props.name);
    };

    componentWillUnmount = () => {
        this.props.EmailStore.unmount(this.props.name);
    };

    render() {         
        const name = this.props.name;
        const EmailStore = this.props.EmailStore;
        const params = EmailStore.params;         
        let status = "form-group email ";
        if (params.isCorrect && params.onceValidated) status += "valid";
        if (params.isWrong && params.onceValidated) status += "error";    

        return (            
            <div className={status}>           
                <label htmlFor={name}>{this.props.label}</label>
                <input
                    type="email"                 
                    disabled={this.props.disabled}
                    name={name}
                    id={name}
                    value={params.value}
                    onChange={(e) => EmailStore.bindData(e, name)}                                              
                />                
            </div>
        );
    }
}                

Мы подключаем внешний компонент валидации, и важно сделать это после того, как элемент уже включён в вёрстку. Поэтому метод из store вызывается в componentDidMount.

Теперь само хранилище:

EmailStore.js
import {action, observable} from 'mobx';
import reactTriggerChange from "react-trigger-change";
import Validators from "../../../helpers/Validators";
import {
    getTarget
} from "../../../helpers/elementaries";

export default class EmailStore {

    @observable params = {
        value : "",  
        disabled : null,
        isCorrect : null,
        isWrong : null,
        onceValidated : null,
        prevalidated : null
    }
   
    /**
     * Ввод пользовательских данных  
     */
    @action bindData = (e, name) => {      
        this.params.value = getTarget(e).value;        
    };    
    
    /**
     * Валидация поля
     */
    @action validate = (name) => {
        const callbacks = {
            success : (formatedValue) => {                                 
                this.params.value = formatedValue;
                this.params.isCorrect = true;
                this.params.isWrong = false;    
                this.params.onceValidated = true;
            },
            fail : (formatedValue) => {                                          
                this.params.value = formatedValue;
                this.params.isCorrect = false;
                this.params.isWrong = true;                    
            }
        };
        const options = {
            type : "email"
        };
        const element = document.getElementById(name);
        new Validators(element, options, callbacks).init();
        // превалидация данных поля
        reactTriggerChange(element);
        this.params.prevalidated = true;
    };   

}


Здесь стоит обратить внимание на две новых сущности.

observable – объект, любое изменение полей которого отслеживает Mobx (и передаёт сигналы в observer, который подписан на наше конкретное хранилище).
action – этим декоратором должен обёртываться любой хэндлер, который меняет state приложения и/или вызывает сайд-эффекты. Здесь мы меняем значение value в @observable params.

Всё, наш простой компонент готов! Он умеет отслеживать пользовательские данные и записывать их. Позже мы увидим, как центральное хранилище mainStore подписывается на изменение этих данных.

Теперь типовой компонент Fio. Его отличие от предыдущего в том, что мы собираемся использовать компоненты этого типа неограниченное количество раз в одном приложении. Это накладывает некоторые дополнительные требования на store компонента. В довесок, сделаем ещё подсказки по вводимым символам с помощью прекрасного сервиса DaData. Отображение:

Fio.js
import React from "react";
import {inject, observer} from 'mobx-react';
import {get} from 'mobx';

@inject("FioStore")
@observer
export default class Fio extends React.Component {

    constructor(props) {
        super(props);        
    };  

    componentDidMount = () => {
        /**
         * Обязательная регистрация компонента с параметрами вызова
         */
        this.props.FioStore.registration(this.props);
    };

    componentWillUnmount = () => {
        this.props.FioStore.unmount(this.props.name);
    };

    render() {

        /**
         * Подсказки берутся соответственно типу запрашиваемых данных
         * Для DaData:
         * data.surname - Фамилия
         * data.name - Имя        
         * https://dadata.ru/api/suggest/name
         */
        const FioStore = this.props.FioStore;
        const name = this.props.name;
        const item = get(FioStore.items, name);   
        if (item && item.isCorrect && item.onceValidated && !item.prevalidated) status = "valid";
        if (item && item.isWrong && item.onceValidated) status = "error";
        // до регистрации в store 
        let value = this.props.value;
        if (item) value = item.value;       

        return (            
            <div className="form-group fio">                
                <label htlmfor={name}>{this.props.label}</label>          
                <input
                    type="text"                                               
                    disabled={this.props.disabled}
                    name={name}
                    id={name}
                    value={value}                     
                    onChange={(e) => FioStore.bindData(e, name)}                                                                
                />                
                {(item && item.suggestions && item.suggestions.length > 0) && <div className="hint-container" id={"hint-container-" + item.id}>{item.suggestions.map((suggestion, i) => {
                    return (
                        <div className={"suggestion-item fs-" + i} key={i} value={suggestion.data[name]} onClick={(e) => FioStore.setSuggestion(e, name)}>
                            <span className="suggestion-text">{suggestion.data[name]}</span>
                        </div>)
                })}</div>}
            </div>
        );
    }
}      

Здесь есть кое-что новое: мы обращаемся к state компонента не напрямую, а через get:
get(FioStore.items, name)

Дело в том, что количество экземпляров компонента не ограничено, а хранилище одно на все компоненты этого типа. Поэтому при регистрации мы заносим параметры каждого экземпляра в Map:

FioStore.js
import {action, autorun, observable, get, set} from 'mobx';
import reactTriggerChange from "react-trigger-change";
import Validators from "../../../helpers/Validators";
import {
    getDaData, 
    blockValidate
} from "../../../helpers/functions";
import {
    getAttrValue,
    scrollToElement,
    getTarget
} from "../../../helpers/elementaries";

export default class FioStore {

    constructor() { 
        autorun(() => {
            /**
             * Клик по любому полю для закрытия окна подсказок. Клик по самому полю подсказок обрабатывается отдельно в setSuggestion()
             */
            const self = this;
            $("body").click((e) => {
                if (e.target.className !== "suggestion-item" && e.target.className !== "suggestion-text") {
                    const items = self.items.entries(); 
                    for (var [key, value] of items) {
                        value.suggestions = [];                       
                    }                                             
                }
            });
        })
    }

    /**
     * В объект items записываются данные каждого из экзепляров компонента Fio при их создании
     */
    @observable items = new Map([]);

    /**
     * Регистрация экземпляра компонента
     */
    @action registration = (params) => {        
        const nameExists = get(this.items, params.name);      
        if (!blockValidate({params, nameExists, type: "Fio"})) return false; 
        // расширяем items новым объектом
        const value = {
            value : params.value, 
            disabled : params.disabled,
            isCorrect : null,
            isWrong : null,
            suggestions : [],        
            daData : params.daData,
            startValidation : true,
            // элемент был однажды положительно валидирован
            onceValidated : false,
            // элемент был провалидирован при открытии
            prevalidated : false
        };
        set(this.items, params.name, value);         
        this.validate(params.name);  
    };

    /**
     * Открепление экземпляра компонента
     */
    @action unmount = (name) => {
        this.items.delete(name);
    };

    /**
     * Ввод пользовательских данных
     * Может сопровождаться подсказками
     */
    @action bindData = (e, name) => {
        const value = getTarget(e).value;
        const item = get(this.items, name);
        /**
         * Запрашиваем подсказку в сервисе DaData
         */
        if (item.daData && !item.startValidation) {
            getDaData({value, type: "fio", name})
                .then((result) => {
                    item.suggestions = result.suggestions;                    
                })
                .catch((error) => {console.log(error)})
        } else {
            item.startValidation = false;
            item.value = value;
        }
    };

    /**
     * Выбор подсказки
     */
    @action setSuggestion = (e, name) => {
        if (e) e.preventDefault();       
        get(this.items, name).value = getAttrValue(e);
        // закрываем окно с подсказками
        get(this.items, name).suggestions = [];
        get(this.items, name).isCorrect = true; 
        get(this.items, name).isWrong = false;
    };

    /**
     * Валидация поля
     */
    @action validate = (name) => {
        const callbacks = {
            success : (formatedValue) => {        
                get(this.items, name).value = formatedValue;
                get(this.items, name).isCorrect = true;
                get(this.items, name).isWrong = false;      
                get(this.items, name).onceValidated = true;
            },
            fail : (formatedValue) => {                         
                get(this.items, name).value = formatedValue;
                get(this.items, name).isCorrect = false;
                get(this.items, name).isWrong = true;             
            }
        };
        const options = {
            type : "fio"
        };
        const element = document.getElementById(name);
        new Validators(element, options, callbacks).init();
        // превалидация данных поля
        reactTriggerChange(element);            
        get(this.items, name).prevalidated = true;
    };   

}

State нашего универсального компонента инициализируется так:

@observable items = new Map([]);

С обычным объектом JS было бы удобнее работать, однако он не будет «прослушиваться» при изменении значений его полей, т.к. поля добавляются динамически при добавлении новых компонентов на странице. Получение подсказок DaData мы выносим отдельно.

Похожим образом выглядит компонент кнопок, только отсутствуют подсказки:

Button.js
import React from "react";
import {inject, observer} from 'mobx-react';

@inject("ButtonStore")
@observer
export default class CustomButton extends React.Component {

    constructor(props) {
        super(props);        
    }; 

    componentDidMount = () => {
        /**
         * Обязательная регистрация компонента с параметрами вызова
         */
        this.props.ButtonStore.registration(this.props);
    };

    componentWillUnmount = () => {
        this.props.ButtonStore.unmount(this.props.name);
    };

    render() {
        const name = this.props.name;   
        return (            
            <div className="form-group button">
                <button                  
                    disabled={this.props.disabled}
                    onClick={(e) => this.props.ButtonStore.bindClick(e, name)}
                    name={name}
                    id={name}
                >{this.props.text}</button>               
            </div>
        );
    }
}


ButtonStore.js
import {action, observable, get, set} from 'mobx';
import {blockValidate} from "../../../helpers/functions";

export default class ButtonStore {

    constructor() {}

    /**
     * В объект items записываются данные каждого из экзепляров компонента Button при их создании
     */
    @observable items = new Map([])    

    /**
     * Регистрация экземпляра компонента
     */
    @action registration = (params) => {        
        const nameExists = get(this.items, params.name);      
        if (!blockValidate({params, nameExists, type: "Button"})) return false;
        // расширяем items новым объектом
        const value = {           
            disabled : params.disabled,       
            isClicked : false     
        };
        set(this.items, params.name, value);      
    };

    /**
     * Открепление экземпляра компонента
     */
    @action unmount = (name) => {
        this.items.delete(name);
    };

    /**
     * Нажатие на кнопку
     */
    @action bindClick = (e, name) => {
        e.preventDefault();       
        get(this.items, name).isClicked = true;       
    };

}


Компонент кнопки Button обёрнут HOC-компонентом ButtonArea. Обратите внимание, что старший компонент включает свой набор stores, а младший свой. В цепочках вложенных компонентов нет необходимости пробрасывать какие-либо параметры и коллбэки. Всё, что нужно для работы конкретного компонента, добавляется непосредственно в нём же.

ButtonArea.js
import React from "react";
import {inject, observer} from 'mobx-react';
import l10n from "../../../l10n/localization.js";
import Button from "./Button";

@inject("mainStore", "optionsStore")
@observer
export default class ButtonArea extends React.Component {

    constructor(props) {
        super(props);        
    };      

    render() {
        return (            
            <div className="button-container">                
                <p>{this.props.optionsStore.dict.buttonsHeading}</p>
                <Button 
                    name={"send_data"}  
                    disabled={this.props.mainStore.buttons.sendData.disabled ? true : false}
                    text={l10n.ru.common.continue}                                         
                /> 
            </div>
        );
    }
}


Итак, у нас готовы все компоненты. Дело осталось за управляющим mainStore. Сначала весь код хранилища:

mainStore.js
import {observable, computed, autorun, reaction, get, action} from 'mobx';
import optionsStore from "./optionsStore";
import ButtonStore from "./ButtonStore";
import FioStore from "./FioStore";
import EmailStore from "./EmailStore";
import {
    fetchOrdinary, 
    sendStats
} from "../../../helpers/functions";
import l10n from "../../../l10n/localization.js";

class mainStore {

    constructor() {

        /**
         * Инициализация дочерних хранилищ
         */
        this.ButtonStore = new ButtonStore();       
        this.FioStore = new FioStore();
        this.EmailStore = new EmailStore();        

        autorun(() => {     
            this.fillBlocks();         
            this.fillData();
        });
        

        /**
         * Показ кнопки отправки первоначальных данных
         */
        reaction(
            () => this.dataInput,
            (result) => {
                let isIncorrect = false;
                for (let i in result) {
                    for (let j in result[i]) {
                        const res = result[i][j];              
                        if (!res.isCorrect) isIncorrect = true;                 
                        this.userData[j] = res.value;   
                    }
                };
                if (!isIncorrect) {                 
                    this.buttons.sendData.disabled = false
                } else {                  
                    this.buttons.sendData.disabled = true
                };
            }
        );

        /**
         * Кнопка отправки первоначальных данных
         */
        reaction(
            () => this.sendDataButton,
            (result) => {
                if (result) {
                    if (result.isClicked) {
                        get(this.ButtonStore.items, "send_data").isClicked = false;
                        const authRequestSuccess = () => {
                            console.log("request is success!")
                        };
                        const authRequestFail = () => {
                            console.log("request is fail!")
                        };
                        const request = {
                            method : "send_userdata",
                            params : {
                                name : this.userData.name,                               
                                surname : this.userData.surname,
                                email : this.userData.email
                            }
                        };
                        console.log("Request body is:");
                        console.log(request);
                        fetchOrdinary(
                            optionsStore.OPTIONS.sendIdentUrl,
                            JSON.stringify(request),
                            {
                                success: authRequestSuccess,
                                fail: authRequestFail
                            }
                        );
                    }
                }
            }
        );       

    }

    @observable userData = {
        name : "",      
        surname : "",       
        email : ""
    };

    @observable buttons = {      
        sendData : {
          disabled : true
        }
    };  
    
    /**
     * Схема компонентов на страницах
     * @key - имя страницы
     * @value - массив с последовательным перечислением компонентов (name, type), которые в этом порядке включаются в вёрстку на странице
     */
    componentsMap = {
        userData : [
            ["name", "fio"],           
            ["surname", "fio"], 
            ["email", "email"], 
            ["send_data", "button"]
        ]
    };       

    /**
     * Коллекции компонентов для работы listener'ов различных stores
     */
    @observable listenerBlocks = {};

    /**
     * Заполнение коллекций компонентов
     */
    @action fillBlocks = () => {
        for (let i in this.componentsMap) {
            const pageBlock = this.componentsMap[i];
            // преобразуем в объект типов компонентов (key) с массивами их имён (value)
            const blocks = {};
            pageBlock.forEach((item, i) => {
                const _name = item[0];
                const _type = item[1];
                if (!blocks[_type]) {
                    blocks[_type] = [_name]
                } else {
                    blocks[_type].push(_name)
                }                
            })
            this.listenerBlocks[i] = blocks;
        }      
    };

    /**
     * Предзаполнение полей ввода
     */
    @action fillData = () => {      
        if (optionsStore.preset) {
            // проверки, чтобы избежать undefined, что поломает неконтролируемый компонент
            if (optionsStore.preset.name) this.userData.name = optionsStore.preset.name;
            if (optionsStore.preset.surname) this.userData.surname = optionsStore.preset.surname;           
        }
    };   

    /**
     * Listener для компонентов страницы
     */
    @computed get dataInput() {
        const mappedResult = {               
            fio : {},
            email : {
                email : {}
            }
        };  
        
        if (this.FioStore && this.FioStore.items) {    
            this.listenerBlocks.userData.fio.forEach((item) => {
                const i = get(this.FioStore.items, item);
                if (i) {
                    mappedResult.fio[item] = {
                        isCorrect : i.isCorrect,                       
                        value : i.value
                    }
                }
            })
        }

        if (this.EmailStore && this.EmailStore.params) {
            mappedResult.email.email = {
                isCorrect : this.EmailStore.params.isCorrect,                 
                prevalidated : this.EmailStore.params.prevalidated,
                value : this.EmailStore.params.value
            }   
        }
       
        return mappedResult
    } 


    /**
     * Listener нажатия кнопки отправления данных
     */
    @computed get sendDataButton() {
        let result = {};
        const i = get(this.ButtonStore.items, "send_data");
        if (i) {
            result = {
                isClicked : i.isClicked
            }
        }
        return result
    }   

}

export default new mainStore();


Ещё несколько ключевых сущностей.

computed – декоратор для функций, отслеживающих изменения в наших observable. Важным преимуществом Mobx является то, что отслеживаются только те данные, которые вычисляются в computed и потом возвращаются в качестве результата. Реакция и, как следствие, перерисовка virual DOM происходит только тогда, когда это необходимо.
reacion – инструмент для организации сайд-эффектов на основе изменившегося состояния. Он принимает две функции: первая computed, возвращающая вычисленное состояние, вторая с эффектами, которые должны последовать вслед за изменениями state. В нашем примере reaction применяется два раза. В первом мы смотрим состояние полей и делаем вывод о том, корректна ли вся форма, а также записываем значение каждого поля. Во втором мы по клику на кнопку (точнее, при наличии признака «кнопка нажата») отправляем данные на сервер. Объект с данными выводится в консоль браузера. Поскольку mainStore знает все хранилища, мы сразу после обработки клика кнопки можем позволить себе в императивном стиле отключить признак:

get(this.ButtonStore.items, "send_data").isClicked = false;

Можно обсудить, насколько допустимым является наличие такой «императивщины», но в любом случае управление происходит только в одну сторону — от mainStore к ButtonStore.
autorun используется там, где мы хотим запустить какие-то действия директивно, не в качестве реакции на изменение store. В нашем примере запускается одна вспомогательная функция, а так же предзаполнение полей формы данными из словаря.

Таким образом, последовательность выполнения действий у нас следующая. Компоненты отслеживают пользовательские события и изменяют своё состояние. mainStore через computed вычисляет результат, основанный только на тех state, которые изменились. Разные computed смотрят за изменением разных состояний в разных хранилищах. Далее через reaction мы на основе результатов computed выполняем действия с observables, а также выполняем сайд-эффекты (например, делаем AJAX-запрос). На obsevables подписаны дочерние компоненты, которые при необходимости перерисовываются. Однонаправленный поток данных с полным контролем над тем, где и что меняется.

Попробовать пример и код можно самому. Ссылка на репозиторий: github.com/botyaslonim/mobx-habr.
Дальше как обычно: npm i, npm run local. В папке public файл index.html. Подсказки DaData работают на моём бесплатном аккаунте, поэтому, вероятно, могут в некоторые моменты падать из-за хабра-эффекта.

Буду рад любым конструктивным замечаниям и предложениям по работе приложений на Mobx!

В заключение скажу, что библиотека очень упростила работу с данными. Для маленьких и средних приложений она точно будет очень удобным инструментом, чтобы забыть о свойствах компонентов и коллбэках и сконцентрироваться непосредственно на бизнес-логике.

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


  1. JustDont
    14.10.2019 12:11
    +2

    Забавно, что хотя Mobx идёт с готовой связкой именно к реакту, связка — это очень костыльное место всей этой системы (вы просто почитайте issues, там быстро можно дочитаться до кошмаров на ночь). Сама же Mobx мне вообще очень нравится, очень плохо, что больше никто так не делает. RxJS почему-то десятки клонов, а вот реактивная библиотека с фокусом именно на propagation of change, а не на потоки событий — всего одна.

    PS: А, ну и да. Статью подтверждаю своим опытом — сам тоже рефакторил лапшекод на коллбеках на MobX, и результатом был крайне доволен.


    1. vintage
      14.10.2019 14:05

      1. JustDont
        14.10.2019 15:20

        О, спасибо. Поиграюсь с CellX, если случай представится.


        1. vintage
          14.10.2019 16:04

          Насколько я помню там используется менее продвинутый алгоритм, который я описывал тут: https://habr.com/ru/post/235121/


          Riim поправь, если я не прав.


          1. Riim
            14.10.2019 16:18

            Начиная с v1.8 алгоритм тот же, что и в mobx. А на счёт менее продвинутого я бы поспорил, да в старом алгоритме есть один мелкий нерешаемый недостаток, но зато он минимум в 3 раза быстрее. В три раза медленнее — это лучшее, что я смог получить с нового алгоритма избавившись всего лишь-то от одного недостатка с которым вполне нормально жилось. Довольно сомнительное улучшение.


            1. vintage
              14.10.2019 16:28

              Корректность всё же важнее производительности, да и производительность ты тогда измерял на не слишком реалистичных сценариях типа глубины в тысячи зависимостей. Или есть какие-то новые более адекватные бенчмарки?


              1. Riim
                14.10.2019 16:57

                Корректность всё же важнее производительности

                С корректностью результатов в старом алгоритме всё отлично. Недостаток в лишнем вычислении при определённых условиях, которое в реальном приложении случалось крайне редко и совершенно не мешало.


                Или есть какие-то новые более адекватные бенчмарки?

                Не понимаю почему ты не веришь в адекватность такого бенчмарка. Ну вот уменьшу я глубину (число слоёв) одновременно увеличив количество ячеек в каждом слое и что дальше? Если количество вычисляемых ячеек по которым пройдёт сигнал будет тем же, то и результат будет абсолютно тем же.
                Все бенчмарки для такой библиотеки, скорей всего, можно разделить на два типа: 1 — скорость создания экземпляра ячейки, 2 — скорость прохождения сигнала по вычисляемым ячейкам. Остальное практически не имеет какого-либо смысла. Что ты предлагаешь ещё замерить? Скорость чтения не вычисляемой ячейки? Зачем?


                1. vintage
                  14.10.2019 18:33

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


                  Потому что от топологии очень сильно зависит эффективность разных алгоритмов. И надобности в сильно глубоких деревьях мне встречать не приходилось.


                  Что можно замерять:


                  1. Трекинг большого числа зависимостей (фильтр списка товаров).
                  2. Инвалидация большого числа зависимых (подсветка текущего элемента в большом списке).


                  1. JustDont
                    14.10.2019 19:02

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

                    ЗЫ: Лично мне вот, кстати, от хорошей реактивной библиотеки за эти годы экспериментов и возни стало надо следующее:
                    а) Чтоб было про стейт, а не потоки событий. В гробу я видел потоки событий;
                    б) Чтоб можно было без шума и пыли динамически фигачить зависимости и менять/убирать их;
                    в) Чтоб не было тормозов, когда зависимости начинают исчисляться в сотнях;
                    г) чтоб можно было в свойствах делать Symbol (что MobX в это не умел — для меня в свое время стало очень неприятным сюрпризом, из-за которого качество кода заметно просело).

                    Зачем надо г)? Чтоб можно было на полученные от бека данные вешать реактивные поля, и потом не париться тем, что перед JSON.stringify обратно на сервер эти реактивные поля нужно будет вычищать.


                    1. Riim
                      14.10.2019 20:04

                      Про глубину в очередной раз ответил чуть раньше.


                      Чтоб можно было на полученные от бека данные вешать реактивные поля, и потом не париться тем, что перед JSON.stringify обратно на сервер эти реактивные поля нужно будет вычищать.

                      я для подобного использую декоратор NonEnumerable.


                    1. vintage
                      14.10.2019 20:20

                      Чтоб не было тормозов, когда зависимости начинают исчисляться в сотнях;

                      У меня на текущем проекте их до 3k, а зависимых так вообще до 500к :-)


                      Чтоб можно было на полученные от бека данные вешать реактивные поля, и потом не париться тем, что перед JSON.stringify обратно на сервер эти реактивные поля нужно будет вычищать.

                      Их можно просто объявлять неитерируемыми.


                      1. JustDont
                        14.10.2019 20:21

                        Их можно просто объявлять неитерируемыми.

                        Нде, спасибо. Я так и думал, что где-то я тогда протупил, написав код для чистки.


                  1. Riim
                    14.10.2019 19:56

                    но когда стреляло — отрывало руки по локоть

                    можно пример? Я находил у себя в приложении пару мест с таким лишним вычислением, там всё гладко было. Я именно намеренно искал, никаких проблем не возникало.


                    И надобности в сильно глубоких деревьях мне встречать не приходилось

                    вычисляемой ячейке где-то в середине цепочки вообще без разницы сколько там в глубину над или под ней. Твоё предложение менять число зависимостей ещё хоть как-то обосновано (ответил ниже), но я хоть убей не понимаю, что ты так вцепился в глубину дерева. Это же просто способ увеличить число ячеек в тесте.


                    Трекинг большого числа зависимостей
                    Инвалидация большого числа зависимых

                    я вижу у тебя для хранения родительских и дочерних ячеек используется Set, has на котором, при достаточно большом количестве айтемов, будет заметно быстрей, чем indexOf на массиве (https://jsperf.com/array-indexof-vs-set-has/). Наверно, в таком кейсе твой вариант действительно будет быстрее. Я же выбрал массив, тк. при малом числе айтемов уже indexOf быстрее, плюс нативный for-of тогда ещё рано было использовать, а Set#forEach был заметно медленнее обычного цикла. Малое число зависимостей — это 99.9% случаев. Мой бенчмарк показывает как ведут себя библиотеки в этих 99.9%, ты же предлагаешь мне бенчмарк под 0.1% заявляя, что он будет более адекватным. Мне кажется ты всё же не прав.


                    1. vintage
                      14.10.2019 20:33

                      можно пример?

                      Показ страницы зависит через несколько атомов от урла, а на странице идёт вычисление на основе параметра из урла с меньшей глубиной зависимостей. Страница предполагает, что раз она открыта, то параметр в урле есть. Но когда уходишь — параметр тоже стирается. В итоге вместо параметра приходит балалайка и в лучшем случае начинаются тормоза из-за кучи эксепшенов и ненужных вычислений, а в худшем — что-нибудь ломается капитально.


                      не понимаю, что ты так вцепился в глубину дерева. Это же просто способ увеличить число ячеек в тесте.

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


                      я вижу у тебя для хранения родительских и дочерних ячеек используется Set, has на котором, при достаточно большом количестве айтемов, будет заметно быстрей, чем indexOf на массиве

                      В новой реализации уже используются массивы, но без indexOf.


                      1. Riim
                        14.10.2019 22:11

                        а в худшем — что-нибудь ломается капитально

                        ok, на самом деле я немного о другом случае описанном здесь. А то о чём говоришь ты похоже из этой серии. Странно, но со мной такого ни разу так и не случилось, может мне очень повезло, а может что-то в моём фреймворке не даёт этому происходить. В любом случае хорошо, что ничего подобного больше не случится ни с $mol_atom, ни с cellx.


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

                        так я и не спорю про разное число зависимостей, я про то, что замена большей глубины на большую ширину без изменения числа зависимостей ничего не даст. Так зачем ты это требуешь?


                        но без indexOf

                        выглядит довольно запутанно, плюс отталкивает slaves с разными типами значений (в masters как я понял тоже самое) и лишние итерации при переборе masters (if( !master ) continue). Set#has конечно чуть медленнее, но неужели вся эта дополнительная возня действительно даёт результат? Можешь поверхностно объяснить идею?


                        1. vintage
                          14.10.2019 22:30

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

                          Разные реализации по разному справляются с разным числом зависимостей. Ну грубо говоря indexOf на большом числе зависимостей может начать тормозить.


                          Можешь поверхностно объяснить идею?

                          Идея в том, чтобы


                          • трекинг происходил как можно быстрее
                          • требовалось как можно меньше памяти
                          • можно было трекать по позиции, что необходимо для квантования вычислений

                          То есть в процессе работы некоторые подписки могут задублироваться, но это не страшно — пропустить их не долго.


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


                          1. Riim
                            14.10.2019 22:43

                            Разные реализации по разному справляются с разным числом зависимостей. Ну грубо говоря indexOf на большом числе зависимостей может начать тормозить.

                            так и я о том же. Глубину то ты зачем уменьшить хочешь? Ты не понимаешь, что увеличивать число зависимостей можно не трогая глубину?


                            трекинг происходил как можно быстрее

                            ну это понятно, я про идею алгоритма спрашивал.


                            1. vintage
                              14.10.2019 22:54

                              Ну да, просто тестировать имеет смысл на реалистичных сценариях.


                              Ну там долго расписывать, какая именно часть его не понятна?


                              1. Riim
                                14.10.2019 23:40

                                Ну да, просто тестировать имеет смысл на реалистичных сценариях.

                                ну там вполне реалистично, 1-2 зависимости, можно увеличить до 1-3, но не более, это будет похоже на 99% ячеек в реальном приложении. А про глубину нет смысла думать, как я уже говорил, ячейке пофиг на какой она глубине, на скорости её обработки никак не отражается, это просто способ сделать их много.


                                Ну там долго расписывать, какая именно часть его не понятна?

                                в общих чертах вроде понял, вопрос уже другой: это реально даёт результат в плане ускорения или это пока просто эксперимент? Чем больше понимаю алгоритм, тем большее ощущение, что это никак не должно быть быстрее использования Set и тем более массива при малом числе зависимостей.


                                1. vintage
                                  15.10.2019 06:22

                                  На текущем проекте у меня до 3к зависимостей, при глубине не более пары десятков.


                                  Оно сделано так не столько для ускорения, сколько для возможности квантования.
                                  Ну и что может быть быстрее прямой записи в массив по смещению?


    1. MaZaAa
      14.10.2019 14:18

      По поводу костылей, этот просто автор пишет так, как во времена динозавров писали с Provider и Inject, все работает изумительно если писать нормально.
      codesandbox.io/s/goofy-hamilton-b9jxv
      codesandbox.io/s/frosty-dust-2mk5b


      1. botyaslonim Автор
        14.10.2019 14:31

        Ну скорее как в туториалах на mobx.js.org советуют )
        Это лишь второй проект на этой библиотеке, наверняка чего-то не знаю ещё


        1. MaZaAa
          14.10.2019 14:37

          Вот она, глобальная проблема, доки и туториалы устаревают и большинство людей знакомятся с инструментом и начинают использовать его стремно( Но ничего, век живи, век учись, теперь вы знаете как можно его использовать максимально эффективно и с минимальным кол-вом кода, и без props hell и hoc hell


          1. botyaslonim Автор
            14.10.2019 14:39

            Я как бы не вижу, где тут props hell в Provider. Он в нативном React, да. А в Mobx мы говорим только о способах подключения нужного store. Но это всё-равно не старый добрый props hell


            1. MaZaAa
              14.10.2019 21:04

              Только это стремный и некрасивый способ, получать store через пропсы, если в этом нету реальной необходимости(получать store через пропсы). Кстати, я надеюсь в для локальных стейтов компонентов вы тоже MobX используете, или setState?


              1. botyaslonim Автор
                14.10.2019 23:53

                Да, конечно, про setState забыли навсегда. В примерах видно


              1. Druu
                15.10.2019 15:52

                Только это стремный и некрасивый способ, получать store через пропсы

                А какие еще варианты в реакте есть? Речь не о глобальном стейте, конечно, а, допустим, такая задача — у нас есть компонент Х, в нем список элементов E, для E при этом уже есть свой стейт и своя локлаьная огика. С-но, стейт Х содержит список стейтов E, как передать каждый из стейтов E в нужный E без пропсов?


                1. MaZaAa
                  15.10.2019 15:56

                  Не, так я говорю именно против inject который стор в пропсы пробрасывает, когда стор можно просто подключить через Import и получать все бонусы от IDE, такие как автокомплит и т.п. Но я не против пропсов в целом разумеется.


                  1. Druu
                    15.10.2019 16:55

                    Не, так я говорю именно против inject который стор в пропсы пробрасывает, когда стор можно просто подключить через Import

                    Я же указал, что речь не о глобальном сторе, речь о локальных сторах — которые привязаны к инстансу компонента, внутри него создаются и с ним же уничтожаются. Т.е. импортировать вы можете конструктор, но не сам стор. Если надо оркестрировать такие сторы из внешнего компонента, то никак в реакте кроме прокидывания в пропсы это не сделать. Ну разве что какое-то жуткое обмазывание провайдерами и хоками.


                    1. MaZaAa
                      15.10.2019 18:58

                      Сделайте реальный пример в codesandbox, а то я не понимаю из ваших слов где тут закопана какая-то великая проблема. Лучше всего просто посмотреть в код работающий, в котором с какой-то целью сделано то, что вы имеете в виду. И если там будет какой-то ад, может я подскажу как можно это превратить во что-то нормальное.


                      1. Druu
                        18.10.2019 10:46

                        Я про этот кейз:


                        если нужны разные инстансы одного и того же стора, то 2 варианта 1) context 2) new Store() и прокидывание в пропсы.

                        :)


      1. JustDont
        14.10.2019 15:13

        По поводу костылей — это я про внутреннюю реализацию mobx-react, а не про статью автора. Автор всё вполне нормально пишет.

        Я хоть и не страдаю DI в любом месте проекта, но глупо отрицать, что DI в среднем даёт куда более пригодный к переиспользованию код. Скажем, в том, чем занимался я, я в некоторых read-only случаях вместо настоящих сторов передавал простые POJO такой же структуры, где мне реактивность заведомо не требовалась, и гонять зря весь движок MobX было не надо.

        Ну и передать стор — это мягко говоря не props hell. Это вы настоящего props hell не видели.


        1. MaZaAa
          14.10.2019 15:27

          React сам по себе вообще не реактивный, от слова совсем. Поэтому чтобы реактивную технологию скрестить с не реактивной, без этого ни как. Таким образом мы наконец-то получаем реактивный React. Тем более все это работает быстро, четко и стабильно, с минимальным количеством перерендера, а рендериться только то, что действительно поменялось. Спасибо MobX.

          Как по мне, так DI это вырви глаз и лишний код, все можно делать компактнее и красивее. Provider использую только когда нужен Context API.


          1. JustDont
            14.10.2019 15:33

            Поэтому чтобы реактивную технологию скрестить с не реактивной, без этого ни как.

            DI и реактивность — это вообще две ортогональные друг другу вещи.


            1. MaZaAa
              14.10.2019 15:36

              И к чему это?? Я проводил какую-то связь DI с реактивностью?


              1. JustDont
                14.10.2019 15:41

                А, вы про кривизну. Как по мне, кривизна там из-за того, что ЖЦ реактовских компонентов — треш и угар. К «совсем не реактивным» веб-компонентам, например, MobX крепится очень небольшим количеством кода без каких-либо ужасов.


                1. MaZaAa
                  14.10.2019 15:45

                  А что не так с жизненным циклом реакт компонентов? в реакте он крепиться тупо @observer или observer(...), куда уж меньше кода)


                  1. JustDont
                    14.10.2019 16:25

                    Вы пробовали посмотреть, что там внутри этого? Я — пробовал.


                    1. MaZaAa
                      14.10.2019 16:52

                      Да, я смотрел, но как я говорил выше это не так важно, потому что в итоге работает быстро и стабильно) Этого более чем достаточно.


          1. vintage
            14.10.2019 16:11

            React сам по себе вообще не реактивный, от слова совсем. Поэтому чтобы реактивную технологию скрестить с не реактивной, без этого ни как.

            Ну вот тут как-то без костылей обошлись: https://github.com/eigenmethod/mol/tree/master/jsx/view


            1. MaZaAa
              14.10.2019 16:57

              onclick
              серьезно? не camelCase и не under_score, печально)
              onclick={ event => this.change( event ) }
              вместо того чтобы передавать функцию по ссылке, давайте ее каждый раз по новой создавать, круто)


              1. vintage
                14.10.2019 18:37

                серьезно? не camelCase и не under_score, печально)

                Серьёзно: https://developer.mozilla.org/ru/docs/Web/API/GlobalEventHandlers/onclick


                вместо того чтобы передавать функцию по ссылке, давайте ее каждый раз по новой создавать, круто)

                Это не так дорого, как вам кажется.


  1. MaZaAa
    14.10.2019 12:43

    А если вы не будете использовать Proveder и inject, тогда вам не надо будет через пропсы все это делать, а просто будете делать import вашего стора/сторов, то вообще будет сказка


    1. fljot
      14.10.2019 13:15

      а просто будете делать import вашего стора/сторов, то вообще будет сказка


      не учите плохому, это фактически тот же самый
      MyThing.getInstance()


      Для прямого связывания сторов (моделей) есть DI через конструктор (poor man's DI ручками через конструктор),
      для связывания UI компонент с ними есть DI или на пропсах, или сырой реактовый контекст, или нормальные IoC абстракции над контекстом (InversifyJS)


      1. MaZaAa
        14.10.2019 13:24

        Так то, я наоборот даю хороший совет, по сокращению кода и уход от props hell и HoC hell. Вы просто никогда не работали с MobX и не понимаете о чем говорите видно, я с MobX с 2016 года много проектов написал в продакшен, все работают замечательно и там нет ни единого Provide и Inject


        1. fljot
          14.10.2019 13:31

          Нет, мы работаем с MobX и InversifyJS для жирной модульной бизнес-логики со сложными графами связанных объектов в своих ограниченных контекстах (модули), а React всё это рендерит, инжектя в себя необходимые вью-модели (сторы) из тех контекстов, которые ему доступны в том или ином поддереве.
          А ваш совет подходит для TODO-app на нескольких синглтонах.


          1. MaZaAa
            14.10.2019 13:36

            Пустые слова да и только. К сожалению с таким типом мышления большинство людей(( Никогда сами ничего не проверяют, ни попробуют… Только бросаются словами которые где-то, когда-то услышали или вычитали, и принимают все это на веру и правду… Вот пожалуйста, удивляйтесь и меняйте свое представление о мире codesandbox.io/s/goofy-hamilton-b9jxv


            1. fljot
              14.10.2019 13:43

              Вы меня не удивили, но огорчили. Вы оперируете глобальными (типа статическими) переменными и отказываетесь думать в модульность.
              Это троллинг?


              1. MaZaAa
                14.10.2019 13:49

                MobX позволяет делать сторы глобальными и локальными, когда для твоего компонента нужно что-то взять из глобального стора, ты просто берешь это напрямую. В чем собственно проблема?? Вы так же можете брать значения этого стора и прокидывать в пропсы компоненту. Есть такое понятие умный и глупый компонент. Вот глупый компонент просто принимает пропсы, а умный решает откуда что взять, кому и когда что пропихнуть в пропсы. В чем проблема? Как это убивает модульность? Вот посмотрите ещё сюда codesandbox.io/s/frosty-dust-2mk5b


                1. vintage
                  14.10.2019 14:17

                  Есть такое понятие умный и глупый компонент.

                  Эти понятия не масштабируются. И являются по сути костылями к Реакту, не позволяющему управлять состоянием компонент извне.


                  1. MaZaAa
                    14.10.2019 14:25

                    Ну конечно, меняешь состояния MobX сторов и управляешь как угодно из вне.


                  1. strannik_k
                    14.10.2019 14:46

                    А вот тут я согласен, что это костыли. Компоненты — это view. Писать view, который не рендерит, а реализует логику — ну не бред ли)
                    Надо логику к div-у добавить, через атрибуты можно было бы сделать ее добавление.


          1. strannik_k
            14.10.2019 14:28

            И что у вас там такого, из-за чего нельзя просто через import передать нужный стор? В чем причина, почему так будет плохо?

            Для меня пока Provider и inject просто создают иллюзию, что между стором и остальными компонентами нет зависимости. А зависимость то никуда не делась.


            1. botyaslonim Автор
              14.10.2019 14:29

              А какая есть зависимость? Store сам по себе, он может принимать вызов коллбэков от компонента (action), а может не принимать. Это как напишете


              1. strannik_k
                15.10.2019 02:46

                Store нет, а вот компонент то зависим от стора.


                1. botyaslonim Автор
                  15.10.2019 09:55
                  +1

                  Ну так принципиально мы делаем глупые компоненты, а все данные и методы выводим в их хранилища. То есть компонент вообще всегда будет зависеть от какого-то store. Вот нужный мы и подключаем тем или иным способом. Через Provider я сразу определяю, какие конкретно stores у меня будут задействованы на этом SPA. Так даже удобнее и логичнее, чем через import. Потому что взаимосвязь между именем инжекта и именем файла хранилища определяется ровно в одном месте.

                  Вот смотрите. У нашего SPA есть index.js, там написано import Store from /path1. Далее в компоненте мы просто inject(«storeName») и не думаем, откуда он пришёл сверху и какому файлу соответствует. Увели этот компонент на другой проект, в компоненте ничего не меняем вообще, просто в index.js нового проекта пишем import Store from /path2.

                  Эти наши изменения никак не затрагивают сам компонент. А вот если писать в компоненте import Store from ./path, то просто так Store мы не поменяем. Надо будет копировать компонент в другую папку, где по такому же относительному пути лежит другой Store. Или, ттт, писать бизнес-логику подключения в самом компоненте.

                  Поэтому через Provider выходит универсальнее


                  1. bgnx
                    15.10.2019 11:58

                    А используя inject тайпскрипт или флоу покажет ошибку если не был прокинут стор через Provider? Преимущество импортов в том что они статически тайпчекаются и ошибки будут видны сразу а вот все эти инжекты и провайдеры (и прочие di) под большим сомнением (причем если и тайпчекаются то скорее всего нужно будет заимпортить тип чтобы передать дженерик инжекту и тогда смысл в этом вообще пропадает если все равно связь через импорты)


                    1. botyaslonim Автор
                      15.10.2019 12:08

                      Если не был прокинут через Provider, приложение падает с ошибкой сразу


                      1. bgnx
                        15.10.2019 12:51

                        Когда падает? В рантайме? В момент рендера нужного компонента? То есть для того чтобы задетектить ошибку непрокинутого стора нужно будет запустить приложение, наклацать нужное состояние чтобы отрендерился нужный компонент и только тогда в консоли увидеть ошибку отсутствующего стора? Нет уж, спасибо, я лучше буду импорты юзать с которыми тайпскрипт сразу в редакторе подсветит ошибку в момент копирования файла с компонентом в проект


                        1. MaZaAa
                          15.10.2019 13:39

                          И не только TS, просто IDE подсветит красным с обычным JS


            1. vintage
              14.10.2019 14:31

              И что у вас там такого, из-за чего нельзя просто через import передать нужный стор?

              В 2k19 приходится объяснять что не так с глобальными переменными...


              1. strannik_k
                14.10.2019 14:57

                При чем тут глобальные переменные?
                Передача через Provider или через import, делает стор локальным или глобальным?


                1. vintage
                  14.10.2019 15:13

                  При том, что то, что находится за import — глобальные переменные. Просто вынесенные в отдельный файл.


                  1. MaZaAa
                    14.10.2019 15:21

                    Мдаааа, научи «Умного» молиться, он и лоб расшибет. Мы тут не рассматриваем потенциальные ошибки в сферическом вакууме, которые может допустить только тот, кто только из яслей вышел. export default const someState; перезапишите его, удачи.


                    1. vintage
                      14.10.2019 16:24

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


                      1. MaZaAa
                        14.10.2019 17:06

                        Вы не знаете походу что такое глобальные переменные и те переменные которые доступны только внутри контекста где их импортируют ЯВНО и они НЕ внезапные. Откуда вы вообще взяли глобальные переменные в этой статье или в комментариях к ней??? В JS на клиенте глобальные переменные это только var(если не объявлена внутри замыкания конечно) и window.globalVar. Все остальное всегда внутри замыкания и никуда просто так наружу никогда не выходит. Учитывая то, что уже уже годами люди пишут на es6+ с Importy/require и используют только cont и let, поэтому глобальные переменные это только window.someVar. Зачем вы в заблуждение вводите людей? Просто непонятно откуда-то берете что-то и пишете. А ведь кто-то может тьфу тьфу, взять и поверить этому и подумать что вы что-то в тему написали.


                        1. vintage
                          14.10.2019 18:46
                          +2

                          То, о чём вы говорите — это глобальный скоуп. Разумеется, всё, что лежит в глобальном скоупе является глобальным (то есть в единственном экземпляре в приложении). Но это далеко не единственное место. Если вас смущает термин "переменная", то можете изменить его на термин "стейт". Суть не изменится. Далее в этом терминологическом споре я не участвую.


                      1. babylon
                        15.10.2019 13:50

                        Тут ещё такой момент.Меняя вложенность модуля извне глобальный указатель внезапно может показать другой scope:) А так было бы удобно


                  1. strannik_k
                    14.10.2019 15:24

                    Я вас правильно понимаю, вы хотите сказать, что если мы передаем стор через Provider, то он перестает быть глобальной переменной?


                    1. vintage
                      14.10.2019 16:25

                      Да, он становится свойством контекста окружения.


                      1. strannik_k
                        15.10.2019 01:55

                        Теперь понял о чем вы, а то каким то бредом казалось. Не подумал хорошенько над сообщением fljot.
                        Ок, теперь вроде вижу пользу от такого вида внедрения зависимостей. Это для случаев, когда в рантайме в компоненте планируется использовать разные экземпляры классов, передаваемых в него.
                        Возвращаясь к ситуации со сторами, не так часто используется несколько экземпляров одного и того же стора. Я думаю, использование для сторов Provider и inject все-таки чаще будет избыточным.

                        Может еще есть что-нибудь полезное в таком виде DI?


                        1. vintage
                          15.10.2019 06:25

                          Этот апи у Реакта и правда кривой. Попробуйте reactive-di.


            1. fljot
              14.10.2019 14:34

              «нужный стор» понятие относительное, относительно контекста использования. Почитайте про концепцию Dependency Injection и, например, про реактовый контекст.


    1. botyaslonim Автор
      14.10.2019 13:25

      Мне правда тоже не очень потянет смысл ухода от Provide и inject. В смысле, будет ли тогда Mobx работать так, как заявлено (то есть гарантировано доставлять изменения ровно в тот момент, когда они произошли)? Если да, то какой большой смысл в создании этих сущностей?


      1. MaZaAa
        14.10.2019 13:29

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

        Вот codesandbox.io/s/goofy-hamilton-b9jxv поиграйтесь, проверьте что все пашет


    1. ipanic
      14.10.2019 14:39

      Хорошо, а если нужно переиспользовать компонент с разными источниками данных?


      1. botyaslonim Автор
        14.10.2019 14:41

        Разные источники данных может поставлять mainStore (в моём примере), то есть через инициализирующие компонент props приходит то, что нужно в данный момент


        1. ipanic
          14.10.2019 14:50

          Это был вопрос к MaZaAa
          С inject я могу прибить стор к компоненту и отрендерить его. А могу просто прокинуть через пропсы значения из родительского


          1. MaZaAa
            14.10.2019 14:56

            Вот вам ответ, ни один Provider и Inject не пострадал)
            codesandbox.io/s/magical-golick-b1675

            Provider и Consumer стоить использовать только в рамках Context API


            1. ipanic
              14.10.2019 15:07

              Ну в общем-то сразу вылезли пропсы вместо прямого импорта.
              Вопросов больше нет


              1. MaZaAa
                14.10.2019 15:14

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


      1. MaZaAa
        14.10.2019 14:49

        у тебя может быть 1000000 сторов mobx и все он отдельный класс, подключай только нужные


        1. VolCh
          18.10.2019 06:57

          В одном месте нужен один инстанс сьора, в другом — другой. Контекст эту проблему решает через разных провайдеров. Импорт — разные файлы нужно создавать, так?


          1. MaZaAa
            18.10.2019 10:18

            если нужны разные инстансы одного и того же стора, то 2 варианта 1) context 2) new Store() и прокидывание в пропсы. Если вложенные компоненты должны иметь доступ к этому стору, то конечно лучше context api. Но опять же, это только для случаев аля несколько форм на одной страницы у которых должны быть разны экземпляры одного и того же стора, т.е. обычно это редкие случаи и на многих проектов в этом вообще надобности никогда даже не возникает


            1. vintage
              18.10.2019 14:19

              Ну да, многие проекты так и не вырастают из пелёнок. А некоторые даже до прода не успевают дойти, пока фронтендеры играются с экшен криэйтерами, комбинаторами редьюсеров сторов и компонентами разной степени глупости.


    1. Balek
      18.10.2019 08:28

      Подключение стора через import сделает невозможным SSR.


      1. MaZaAa
        18.10.2019 11:22

        Можно SSR делать через puppeter, 1 раз написал и это универсальное решения для всех проектов, не важно Angualr, Vue, React и т.п. И не надо специально под него подстраиваться когда разрабатываешь, краулеры и SEO довольны.


        1. Balek
          18.10.2019 11:43

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


          1. MaZaAa
            18.10.2019 12:37

            1) Под всем этим, вы намекаете, что DI это нормально? Или что?
            2) Что именно вы называете костылем в виде жирного браузера?
            3) Нормальная архитектура в вашем понимании это какая? Больше конкретики пожалуйста, какой стэк технологий и т.д.


            1. Balek
              18.10.2019 12:55

              Я сам предпочитаю обычный импорт из-за простоты, но в данном случае нужно признать, что это может вызвать проблемы. Лучше доставать стор из контекса реакта.


              Костылем я назвал puppeter.


              Разговор был не о готовой архитектуре, а о том, что мы строим под приложение. Но если хотите пример хорошей архитектуры, где уже решены эти вопросы, посмотрите фреймворк DerbyJS.


              1. MaZaAa
                18.10.2019 13:54

                Почему костыль?
                Пробежался по всем страницам и положил их в кэш. И бэкенд их отдает из кэша, отсюда максимальная производительность.
                Когда меняется контент у какой-то страницы, просто рендеришь ее ещё раз и ложишь в кэш.
                Отсюда вывод, никакое DI и прочее гуано использовать не нужно.
                Нет смысла делать рендер на каждый запрос и отдавать максимально свежие данные ради SEO.


                1. Balek
                  18.10.2019 14:13

                  Именно поэтому и костыль. То что вы описали, это костыль для костыля. Например, страниц может быть запредельно много. Да и SSR нужен не только для SEO.


  1. vintage
    14.10.2019 14:28

    в индустрии в последнее время популярны фреймворки, реализующие Flux-архитектуру фронт-енд приложения

    Это какие такие фреймворки кроме Реакта и его клонов? FLUX не масштабируется, так как предполагает один глобальный цикл обработки данных. Вот даже вам для этого пришлось делать две одинаковые иерархии: для сторов и для шаблонов.


  1. inoyakaigor
    14.10.2019 15:05

    этим декоратором должен обёртываться любой хэндлер

    Я бы сказал, что не должен, а может. Если не обернуть в @ action, то при каждом изменении observable переменной внутри функции будет производится перерендеринг зависимых от этих переменных компонентов. С использованием этого декоратора перерендер будет произведён один раз в конце. То есть для функций меняющих одну переменную его можно и не использовать


    1. botyaslonim Автор
      15.10.2019 10:00

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


  1. pterodaktil
    16.10.2019 13:18

    mobx-react[-lite] уже позволяет избавиться от переполнения главного стора и создания каши, используя useLocalStore хук, а создание компонент через классы уже почти ушли во времена динозавров с приходом хуков


    1. botyaslonim Автор
      16.10.2019 13:19

      Ага, прям уже и ушли )


    1. MaZaAa
      16.10.2019 14:19

      хахаахах, ну вы конечно приколист, вообще-то MobX сторы в виде классов намного удобнее во всех отношениях нежели useLocalStore. Работа с данными отдельно, view отдельно, вместо каши где все подряд внутри функции намешано.


    1. strannik_k
      16.10.2019 20:09
      +2

      Еще не успели уйти, но могут, к сожалению. На нынешнем проекте используем хуки. Лично я не в восторге от них. Будет возможность, в следующем проекте вернусь к классам.


      1. botyaslonim Автор
        17.10.2019 00:59

        Они вообще хоть что-то лучше делают, хуки?


        1. MaZaAa
          17.10.2019 10:27

          Все что будет сказано имеет отношения именно к связке React + MobX. т.е. когда реакт именно реактивный.
          В целом имея опыт написания проекта полностью на функциональных компонентах с хуками, могу сказать, что в следующий раз я хуки использовать не буду, слишком у них много недостатков, а их достоинства можно так же подтянуть в классы и делать не extend React.Component а extend MyEnchancedComponent в котором уже можно расширить функционал как удобно. Тот же useEffect в классах реализовать элементарно. И те же кастомные хуки тоже делать не проблема и использовать в классах. Вот пожалуйста codesandbox.io/s/pedantic-silence-xu0jd как раз на примере излюбленного онлайн статуса пользователя который в примерах реакта.


          1. vintage
            17.10.2019 17:38

            Скорее всего ваше решение сломается, так как Реакт трекает состояния хуков основываясь на порядке обращения к ним при рендеринге. А так как конструкторы компонент отрабатывают при рендеринге родительского компонента, то любая динамика в нём сломает ваши хуки капитально. Весёлой отладки.


            1. MaZaAa
              17.10.2019 17:43

              Омг, о чем речь вообще? Как обычно просто набор слов, что ничего не работает, все сломается, а где конкретика??? Берешь codesandbox, и пишешь код где все сломано, вот тогда и поговорим. Вы как обычно увидели фразу вырванную из контекста, не посмотрели код, а сразу же начали строчить комментарий.

              codesandbox.io/s/epic-wave-0zt26 — ещё есть вопросы?


              1. vintage
                17.10.2019 18:10

                Предлагаю вам почитать этот доклад, чтобы понять как реакт трекает хуки: https://habr.com/ru/post/413791/
                Он не про хуки, но принцип тот же.


                Также можете поразмышлять над тем, почему Реакт запрещает использовать хуки в циклах и условиях, — причина всё та же.


                Придумывать пример, на котором у вас всё рухнет, у меня нет ни времени, ни желания. Я вас предупредил. Далее уже вы сами решайте, делать ли ставку на опасное решение, или подождать, пока грабли ударят вам по лицу.


                1. MaZaAa
                  17.10.2019 18:54

                  Какое опасное решение?? Вы что за чушь вообще порите… Речь тут вообще не о хуках, а о их АНАЛОГЕ в классовых компонентах. Как обычно пустые слова, не подкрепленные кодом.

                  codesandbox.io/s/epic-wave-0zt26

                  P.S. Конечно у вас нету желания, что-либо доказать, потому что, вы как обычно не правы и сядите в лужу. Это и так понятно.


                  1. vintage
                    17.10.2019 19:13

                    Ага, не обратил внимание, что вы не реактовые хуки используете, а свои.


                    А желания во многом нет из-за манеры вашего поведения. Подумайте над этим.


                    1. MaZaAa
                      17.10.2019 19:55
                      +2

                      Так вы не обращаете внимания ни на что, выдираете слова «триггеры» для вас просто из контекста и пытаетесь сделать какие-то утверждения, которые ни чем не подкрепляете, от слова совсем. Прежде чем что-то писать, прочитайте о чем речь, а если лень, то не встревайте вовсе, а то вводите людей которые читают в заблуждение. Если вы не заметили, я кидаю постоянно код и подкрепляю им свои слова. Любой человек может на него посмотреть, поэкспериментировать с ним, проверить и опровергнуть. А вы просто говорите «Это все фигня, работать не будет, тормозит по страшному, дебажить нереально, вот в $mol вообще все летает и работает идеально». Подумайте над этим.


                      1. babylon
                        17.10.2019 21:34

                        vintage просто тонко троллит, а вы все по-серьезному тапками кодом кидайтесь:))) Вы реально думаете, что это изменит ситуацию с реактом или молом?
                        мол лучше реакта. И это суровая реальность. Возвращайтесь в нее по скорее. Старайтесь видеть картину целиком, а не частями. Но это уже особенность работы вашего мозга. Человек видит мозгом… или не видит. Вы не видите. Подумайте почему. Любые фреймворки (мол и реакт не исключение) вообще трудно улучшать. Их проще создать по другому. Можете сами попробовать, а уже потом заниматься критиканством. А пока вам до Карловского как до луны. И не только вам.


                        1. MaZaAa
                          17.10.2019 22:01

                          Ага, вы из той же секты что и он :D А это не троллинг, это бред сивой кобылы.


                        1. VolCh
                          18.10.2019 07:24

                          Лучше по каким параметрам и для кого? Только не говорите "по всем и для всех". Тут грамотного разработчика на реакт на зп чуть выше рынка сложно найти, а как найти на Mol?


                          1. vintage
                            18.10.2019 07:46

                            Ищите просто грамотного разработчика, а он уже быстро освоит нужный инструмент, будь то Реакт с его 100500 библиотек-костылей, Ангуляр с тоннами бойлерплейта или Мол с необычным синтаксисом.


                            1. VolCh
                              18.10.2019 11:39

                              Мы предпочитаем исключать ситуацию когда разработчику не нравится стэк, когда он просто его терпит за "чуть выше рынка".


                              1. vintage
                                18.10.2019 14:24

                                Вы хотите сказать, что всем "реакт разработчикам" нравится Реакт?


        1. strannik_k
          17.10.2019 13:48

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

          Они якобы должны делать лучше то, ради чего появились.
          reactjs.org/docs/hooks-intro.html
          Вкратце опишу свои мысли по этому.
          1) It’s hard to reuse stateful logic between components
          В доках написано «React doesn’t offer a way to “attach” reusable behavior to a component (for example, connecting it to a store).» Как бы это проблема реакта, а не классов. На классах такое сделать можно. Например, в соседнем комментарии MaZaAa предложил вариант решения. Но так как он не встроенный, как хуки, то и выглядит громоздко.
          Миксины были близки к удобному и качественному решению проблемы. Но смешивание в таком случае — это ошибка. Данные и методы в миксине не стоило смешивать с компонентом, а надо было делать ее отдельный объектом, который имеет такой же жизненный цикл, как и компонент. Каждый такой объект имел бы свои данные, а не общие, мог бы взаимодействовать с другими такими же объектами и компонентом. Было бы еще очень полезно, если бы их через атрибуты можно было бы добавлять, тогда ради небольших фич не пришлось бы создавать компоненты, как сейчас приходится.

          2) Complex components become hard to understand
          То, что было в классе, поместили в функцию и поменяли API. Не знаю как другим, но для меня лучше не стало.

          3) Classes confuse both people and machines
          В таком виде данное утверждение очень спорное и даже ошибочное.
          Не зря же в JS добавили классы, хотя их функционал можно на прототипах сделать.
          Кому-то удобней классами делать, кому-то функциями. Нормально два варианта оставить, но вот пропагандировать только один, это глупо. Не тот случай.


          1. MaZaAa
            17.10.2019 14:29

            На классах такое сделать можно. Например, в соседнем комментарии MaZaAa предложил вариант решения. Но так как он не встроенный, как хуки, то и выглядит громоздко.

            А почему громоздко?
            Использование в одну строчку
            this.userState = useUserOnlineStatus(this, props.userId);

            Код самого «хука» такой же по сути как и для функциональных компонентов
            
            const useUserOnlineStatus = (context, userId) => {
              const userState = new UserState();
            
              context.useEffect(() => {
                const handleStatusChange = status => {
                  userState.online = status;
                };
            
                onlineSubscriber.subscribe(userId, handleStatusChange);
            
                return () => {
                  onlineSubscriber.unSubscribe(userId, handleStatusChange);
                };
              });
            
              return userState;
            };
            


            1. strannik_k
              17.10.2019 15:16

              Это я неудачно выразился.
              Я имел ввиду, что в вашем решении надо еще BaseComponent реализовать. А в случае хуков уже встроенное стандартное решение. А так, я бы не сказал, что ваше решение уступает хукам.


              1. MaZaAa
                17.10.2019 15:29

                Ну главное что используя классовые компоненты можно получать все достоинства ООП плюс достоинства хуков, а так же никаких проблем связанных с контекстом не будет, как в функциональных компонентах. Таким образом надобность в функциональных компонентах отпадает вообще. И все удобства которые есть там, точно так же ложаться на классовые компоненты.


        1. VolCh
          18.10.2019 06:46

          То, что явно лучше на уровне компонента: не надо дублировать логику, которая нужна на каждый рендер, в didUpdate/didMount, чуть меньше кода при работе со стейтом, если в компоненте нет или мало "глобальных" изменений стейта, изменений нескольких ключей стейта одновременно, а вот строчки типа


          setLoading(false)
          setOptions(options)
          setSelected(null)
          setTooltipMessage("Select option")

          вместо одного setState выглядят, скажем так на любителя.


          Главный плюс на уровне проекта — простое переиспользование. Но с главным минусом (для меня) — не работают с классами — на большом проекте, где в основном классы, толку от этого мало, если не ставить целью переписать всё на функции с правилами типа "изменения файлов с классами компонентов пройдёт ревью только если компоненты переписываются на функции"


          1. Druu
            18.10.2019 08:29
            +1

            То, что явно лучше на уровне компонента: не надо дублировать логику, которая нужна на каждый рендер, в didUpdate/didMount, чуть меньше кода при работе со стейтом, если в компоненте нет или мало "глобальных" изменений стейта, изменений нескольких ключей стейта одновременно

            Так это же все можно и для классов сделать. Например, никто не мешал добавить life-cycle метод, который будет работать как didUpdate+didMount.


            Главный плюс на уровне проекта — простое переиспользование.

            С-но в классы точно так же можно выносить куски работы со стейтом. Единственное что делают хуки — позволяют получить автоматом нужный инстанс сервиса. Но, во-первых, это снижает контроль (т.к. нельзя самом вмешаться в этот процесс), а, во-вторых, сделано через одно место как раз из-за того, что не через классы.


          1. strannik_k
            18.10.2019 12:32

            Переиспользование логики хуки делают не лучше, чем классы.
            Вы видимо не прочитали соседние комментарии в которых MaZaAa привел
            пример реализации тех же хуков на классах, а также я пишу о том,
            как можно было еще реализовать простое переиспользование