Hoy quiero contarles cómo se realizó la transición a Mobx en nuestro proyecto, qué ventajas ofrece. También se mostrará un proyecto típico y se darán explicaciones sobre temas clave. Pero primero, introductorio.

¿Por qué necesitas cambiar a algo? De hecho, la respuesta a esta pregunta ya es la mitad de la batalla. A muchos ahora les encanta aplicar nuevas tecnologías solo porque son nuevas. Una buena línea en el currículum, la posibilidad de autodesarrollo, para estar en la tendencia. Es genial cuando puedes seguir adelante.
Y, sin embargo, cada herramienta debe resolver sus propios problemas, y los repelemos de una forma u otra cuando escribimos código comercial.
En nuestro proyecto, hay una serie de widgets donde el usuario ingresa sus datos, interactúa con los formularios. Como regla general, cada widget tiene varias pantallas. Érase una vez, todo funcionó en el viejo motor de plantillas MarkoJS + que requería jQuery en el cliente. La interacción con los formularios se escribió en un estilo imperativo, si ... de lo contrario, devoluciones de llamada y esto es todo lo mismo que ya parece ser en el pasado.
Entonces llegó el momento de
reaccionar . La lógica de negocios en el cliente se estaba volviendo más gruesa, había muchas opciones de interacción, el código imperativo se convirtió en un desastre complicado. El código de reacción declarativo resultó ser mucho más conveniente. Finalmente, fue posible concentrarse en la lógica en lugar de la presentación, reutilizar componentes y distribuir fácilmente tareas para desarrollar nuevas funciones entre diferentes empleados.
Pero la aplicación en React puro con el tiempo se basa en límites tangibles. Por supuesto, estamos aburridos de escribir this.setState para todos y pensar en su asincronía, pero arrojar datos y devoluciones de llamada a través del grosor de los componentes lo hace particularmente difícil. En resumen, ha llegado el momento de separar completamente los datos y la presentación. No se trata de cómo puede hacer esto en React puro, pero en los marcos de la industria que implementan la arquitectura Flux de las aplicaciones front-end han sido populares últimamente.
Según el número de artículos y referencias en vacantes,
Redux es el más famoso entre nosotros. En realidad, ya traje mi mano para instalarlo en nuestro proyecto y comenzar el desarrollo, como en el último momento (¡y esto es literalmente!) El diablo atravesó el Habr, y luego hubo una discusión sobre el tema "¿Redux o Mobx?" Aquí está este artículo:
habr.com/en/post/459706 . Después de leerlo, así como todos los comentarios debajo, me di cuenta de que todavía usaría
Mobx .
Así que de nuevo. La respuesta a la pregunta más importante: ¿por qué todo esto? - se ve así: es hora de separar la presentación y los datos, me gustaría construir la gestión de datos en un estilo declarativo (así como renderizado), sin polinización cruzada de devoluciones de llamada y atributos reenviados.
Ahora estamos listos para proceder.
1. Sobre la aplicación
Necesitamos construir en el frente un diseñador de pantallas y formas, que luego puedan ser barajadas rápidamente, conectadas entre sí siguiendo los requisitos cambiantes del negocio. Esto inevitablemente nos lleva a lo siguiente: crear una colección de componentes completamente aislados, así como algún componente básico que corresponda a cada uno de nuestros widgets (de hecho, estos son SPA separados creados cada vez bajo un nuevo caso de negocios en la aplicación general).
Los ejemplos mostrarán una versión truncada de uno de estos gadgets. Para no acumular código adicional, deje que sea una forma de tres campos y botones de entrada.
2. Datos
Mobx no es esencialmente un marco, es solo una biblioteca. El manual establece explícitamente que no organiza sus datos directamente. Usted mismo debe crear una organización de este tipo. Por cierto, usamos
Mobx 4 porque la versión 5 usa el tipo de datos Sybmol, que, desafortunadamente, no es compatible con todos los navegadores.
Por lo tanto, todos los datos se asignan a entidades separadas. Nuestra aplicación apunta a un conjunto de dos carpetas:
-
componentes donde ponemos toda la vista
-
tiendas , que contendrán datos, así como la lógica de trabajar con ellos.
Por ejemplo, un componente de entrada de datos típico para nosotros consta de dos archivos:
Input.js y
InputStore.js . El primer archivo es un estúpido componente React que es estrictamente responsable de mostrar, el segundo son los datos de este componente, las reglas del usuario (
onClick ,
onChange , etc.)
Antes de pasar directamente a los ejemplos, debemos resolver otro problema importante.
3. Gestión
Bueno, tenemos componentes completamente autónomos de View-Store, pero ¿cómo nos unimos en una aplicación completa? Para la visualización, tendremos el componente raíz de
App.js , y para administrar los flujos de datos, el almacenamiento principal es
mainStore.js . El principio es simple: mainStore sabe todo acerca de todos los repositorios de todos los componentes necesarios (se mostrará a continuación cómo se logra esto). Otros repositorios no saben nada sobre el mundo (bueno, habrá una excepción: los diccionarios). Por lo tanto, tenemos la garantía de saber a dónde van nuestros datos y dónde interceptarlos.
mainStore declarativamente, a través del cambio de partes de su estado, puede controlar el resto de los componentes. En la siguiente figura,
Acciones y
Estado se refieren a almacenes de componentes, y los
valores calculados se refieren a
mainStore :

Comencemos a escribir el código. El archivo principal de la aplicación 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";
Aquí puedes ver el concepto básico de Mobx. Los datos (almacenes) están disponibles en cualquier lugar de la aplicación a través del mecanismo
Proveedor . Envolvemos nuestra aplicación enumerando las instalaciones de almacenamiento necesarias. Para usar el
proveedor, conectamos el
módulo mobx-react . Para que
mainStore de la tienda de control principal tenga acceso a todos los demás datos desde el principio, inicializamos las tiendas secundarias dentro de
mainStore :
Ahora
App.js , el esqueleto de nuestra aplicación.
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> ); } }
Hay otros dos conceptos básicos de Mobx:
inyectar y observar.
Inject solo implementa el almacén necesario en la aplicación. Las diferentes partes de nuestra aplicación utilizan diferentes repositorios, que enumeramos en
inyectar , separados por comas. Naturalmente, las tiendas conectables se deben enumerar inicialmente en el
Proveedor . Los repositorios están disponibles en el componente a través de
this.props.yourStoreName .
observador : el decorador indica que nuestro componente se suscribirá a los datos que se modifican con Mobx. Los datos han cambiado: se ha producido una reacción en el componente (se mostrará a continuación). Por lo tanto, no hay suscripciones especiales y devoluciones de llamada: ¡Mobx ofrece los cambios en sí!
Volveremos a administrar toda la aplicación en
mainStore , pero por ahora haremos los componentes. Tenemos tres tipos de ellos:
Fio ,
correo electrónico ,
botón . Deje que el primero y el tercero sean universales y el
correo electrónico personalizado. Comencemos con él.
La pantalla es el componente React tonto habitual:
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> ); } }
Conectamos el componente externo de validación, y es importante hacerlo después de que el elemento ya esté incluido en el diseño. Por lo tanto, el método de la tienda se llama en
componentDidMount .
Ahora el repositorio en sí:
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();
Vale la pena prestar atención a dos nuevas entidades.
observable : un objeto, cuyo cambio de campos es monitoreado por Mobx (y envía señales al
observador , que está suscrito a nuestro almacenamiento específico).
acción : este decorador debe envolver cualquier controlador que cambie el estado de la aplicación y / o cause efectos secundarios. Aquí cambiamos el valor del
valor en los
parámetros @observable .
¡Eso es todo, nuestro componente simple está listo! Puede rastrear los datos del usuario y registrarlos. Más adelante veremos cómo se
suscribe el repositorio central de
mainStore para cambiar estos datos.
Ahora un componente típico de
Fio . Su diferencia con la anterior es que vamos a utilizar componentes de este tipo una cantidad ilimitada de veces en una aplicación. Esto impone algunos requisitos adicionales en la tienda de componentes. Además de eso, haremos más pistas sobre los caracteres de entrada utilizando el excelente servicio
DaData . Pantalla:
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() { 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";
Aquí hay algo nuevo: no accedemos directamente al estado del componente, sino a través de
get :
get(FioStore.items, name)
El hecho es que el número de instancias de componentes es ilimitado, y el repositorio es uno para todos los componentes de este tipo. Por lo tanto, durante el registro, ingresamos los parámetros de cada instancia en
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(() => { 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 = []; } } }); }) } @observable items = new Map([]); @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Fio"})) return false;
El estado de nuestro componente genérico se inicializa de la siguiente manera:
@observable items = new Map([]);
Sería más conveniente trabajar con un objeto JS normal, sin embargo, no se "tocará" al cambiar los valores de sus campos, porque los campos se agregan dinámicamente cuando se agregan nuevos componentes a la página. Recibiendo pistas de DaData sacamos por separado.
El componente del botón es similar, pero no hay consejos:
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() {} @observable items = new Map([]) @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Button"})) return false;
El componente del
botón está envuelto por el componente HOC de
ButtonArea . Tenga en cuenta que el componente anterior incluye su propio conjunto de tiendas y el más joven. En las cadenas de componentes anidados no es necesario reenviar ningún parámetro ni devolución de llamada. Todo lo que se necesita para el funcionamiento de un componente en particular se agrega directamente a él.
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> ); } }
Entonces, tenemos todos los componentes listos. El asunto queda con el gerente de
mainStore. Primero, todo el código de almacenamiento:
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 } }; componentsMap = { userData : [ ["name", "fio"], ["surname", "fio"], ["email", "email"], ["send_data", "button"] ] }; @observable listenerBlocks = {}; @action fillBlocks = () => { for (let i in this.componentsMap) { const pageBlock = this.componentsMap[i];
Algunas entidades clave más.
computed es un decorador para funciones que rastrean cambios en nuestro
observable . Una ventaja importante de Mobx es que solo rastrea los datos que se calculan y luego se devuelven como resultado. La reacción y, como consecuencia, el rediseño del DOM virual ocurre solo cuando es necesario.
reacción : una herramienta para organizar los efectos secundarios basados en un estado cambiado. Tiene dos funciones: la primera calculada, que devuelve el estado calculado, la segunda con los efectos que deberían seguir a los cambios de estado. En nuestro ejemplo, la
reacción se aplica dos veces. En el primero, observamos el estado de los campos y concluimos si todo el formulario es correcto, y también registramos el valor de cada campo. En el segundo, hacemos clic en un botón (más precisamente, si hay un signo "botón presionado"), enviamos datos al servidor. El objeto de datos se muestra en la consola del navegador. Como
mainStore conoce todos los repositorios, inmediatamente después de procesar el clic de un botón, podemos permitirnos desactivar la bandera en un estilo imperativo:
get(this.ButtonStore.items, "send_data").isClicked = false;
Puede discutir qué tan aceptable es la presencia de tal "imperativo", pero en cualquier caso, el control se lleva a cabo solo en una dirección: de
mainStore a
ButtonStore .
La ejecución automática se usa donde queremos ejecutar algunas acciones directamente, no como una reacción para almacenar cambios. En nuestro ejemplo, se inicia una función auxiliar, además de rellenar previamente los campos del formulario con datos del diccionario.
Por lo tanto, la secuencia de acciones que tenemos es la siguiente. Los componentes rastrean los eventos del usuario y cambian su estado.
mainStore a través de
cálculo calcula el resultado basado solo en aquellos estados que han cambiado. Diferentes
calculados buscan cambios en diferentes estados en diferentes repositorios. Además, a través de la
reacción, basada en resultados
calculados ,
realizamos acciones con
observables , así como también realizamos efectos secundarios (por ejemplo, hacemos una solicitud AJAX).
Obsevables se suscribe a componentes secundarios, que se
vuelven a dibujar si es necesario. Un flujo de datos unidireccional con control total sobre dónde y qué cambios.
Puede probar el ejemplo y el código usted mismo. Enlace al repositorio:
github.com/botyaslonim/mobx-habr .
Luego, como de costumbre:
npm i ,
npm ejecuta local . En la carpeta
pública , archivo
index.html . Las sugerencias DaData funcionan en mi cuenta gratuita, por lo tanto, probablemente pueden caer en algunos puntos debido al efecto habr.
¡Estaré encantado de cualquier comentario constructivo y sugerencia sobre el trabajo de las aplicaciones en
Mobx!En conclusión, diré que la biblioteca simplificó enormemente el trabajo con datos. Para aplicaciones pequeñas y medianas, definitivamente será una herramienta muy conveniente para olvidarse de las propiedades de los componentes y las devoluciones de llamada y concentrarse directamente en la lógica empresarial.