Hoje, quero falar sobre como a transição para a Mobx ocorreu em nosso projeto, quais são as vantagens que ela oferece. Um projeto típico também será mostrado e explicações sobre os principais problemas. Mas primeiro, introdutório.

Por que você precisa mudar para alguma coisa? De fato, a resposta a essa pergunta já é metade da batalha. Muitos agora adoram aplicar novas tecnologias apenas porque são novas. Uma boa linha no currículo, a possibilidade de autodesenvolvimento, estar na tendência. É ótimo quando você pode simplesmente seguir em frente.
E, no entanto, cada ferramenta deve resolver seus próprios problemas, e nós os repelimos de uma maneira ou de outra quando escrevemos código comercial.
Em nosso projeto, há vários widgets nos quais o usuário digita seus dados, interage com os formulários. Como regra, cada widget tem várias telas. Era uma vez, tudo funcionava no bom e velho mecanismo de modelagem MarkoJS + que exigia o jQuery no cliente. A interação com os formulários foi escrita em um estilo imperativo, se ... mais, retornos de chamada e isso é tudo a mesma coisa que já parece estar no passado.
Então chegou a hora de
reagir . A lógica de negócios do cliente estava ficando mais espessa, havia muitas opções de interação, o código imperativo se transformou em uma bagunça complicada. O código de reação declarativo acabou sendo muito mais conveniente. Finalmente, foi possível concentrar-se na lógica, e não na apresentação, reutilizar componentes e distribuir tarefas com facilidade para o desenvolvimento de novos recursos entre diferentes funcionários.
Mas a aplicação no React puro ao longo do tempo repousa em limites tangíveis. É claro que estamos entediados em escrever this.setState para todos e pensar em sua assincronia, mas jogar dados e retornos de chamada através da espessura dos componentes torna isso particularmente difícil. Em suma, chegou o momento de separar completamente os dados e a apresentação. Não se trata de como você pode fazer isso no React puro, mas nas estruturas do setor que implementam a arquitetura Flux de aplicativos front-end têm sido populares ultimamente.
De acordo com o número de artigos e referências em vagas,
Redux é o mais famoso entre nós. Na verdade, eu já trouxe minha mão para instalá-lo em nosso projeto e iniciar o desenvolvimento, como no último momento (e isso é literalmente!) O diabo puxou o Habr, e então houve apenas uma discussão sobre o tópico "Redux ou Mobx?" Aqui está este artigo:
habr.com/en/post/459706 . Depois de ler, assim como todos os comentários, percebi que ainda usaria o
Mobx .
Então de novo. A resposta para a pergunta mais importante - por que tudo isso? - é assim: é hora de separar a apresentação e os dados. Gostaria de criar o gerenciamento de dados em estilo declarativo (como desenho), sem polinização cruzada de retornos de chamada e atributos encaminhados.
Agora estamos prontos para prosseguir.
1. Sobre a aplicação
Precisamos criar na frente um designer de telas e formulários, que poderiam ser rapidamente embaralhados, conectados entre si, seguindo os requisitos variáveis dos negócios. Isso inevitavelmente nos leva ao seguinte: criar uma coleção de componentes completamente isolados, bem como algum componente básico que corresponda a cada um dos nossos widgets (na verdade, esses são SPAs separados, criados sempre a partir de um novo caso de negócios no aplicativo geral).
Os exemplos mostrarão uma versão truncada de um desses gadgets. Para não acumular código extra, seja uma forma de três campos e botões de entrada.
2. Dados
Mobx não é essencialmente uma estrutura, é apenas uma biblioteca. O manual declara explicitamente que não organiza seus dados diretamente. Você mesmo deve criar uma organização desse tipo. A propósito, usamos o
Mobx 4 porque a versão 5 usa o tipo de dados Sybmol, que, infelizmente, não é suportado por todos os navegadores.
Portanto, todos os dados são alocados para entidades separadas. Nossa aplicação visa um conjunto de duas pastas:
-
componentes onde colocamos todas as visualizações
-
armazena , que conterá dados, bem como a lógica de trabalhar com eles.
Por exemplo, um componente de entrada de dados típico para nós consiste em dois arquivos:
Input.js e
InputStore.js . O primeiro arquivo é um estúpido componente React que é estritamente responsável pela exibição; o segundo são os dados desse componente, as regras do usuário (
onClick ,
onChange , etc ...)
Antes de irmos diretamente aos exemplos, precisamos resolver outra questão importante.
3. Gerenciamento
Bem, temos componentes completamente autônomos da View-Store, mas como nos reunimos em um aplicativo inteiro? Para exibição, teremos o componente raiz do
App.js e, para gerenciar fluxos de dados, o armazenamento principal é
mainStore.js . O princípio é simples: o mainStore sabe tudo sobre todos os repositórios de todos os componentes necessários (será mostrado abaixo como isso é alcançado). Outros repositórios não sabem nada sobre o mundo (bem, haverá uma exceção - dicionários). Assim, temos a garantia de saber para onde nossos dados estão indo e para onde interceptá-los.
A mainStore declarativamente, através da alteração de partes de seu estado, pode controlar o restante dos componentes. Na figura a seguir,
Ações e
Estado se referem a armazenamentos de componentes e
Valores computados se referem a
mainStore :

Vamos começar a escrever o código. O arquivo principal do aplicativo 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";
Aqui você pode ver o conceito básico da Mobx. Os dados (lojas) estão disponíveis em qualquer lugar do aplicativo por meio do mecanismo
Provider . Envolvemos nosso aplicativo listando as instalações de armazenamento necessárias. Para usar o
provedor, conectamos o
módulo mobx-react . Para que o
mainStore do armazenamento de controle principal tenha acesso a todos os outros dados desde o início, inicializamos os armazenamentos filhos no
mainStore :
Agora
App.js , o esqueleto do nosso aplicativo
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> ); } }
Existem mais dois conceitos básicos da Mobx -
injetar e observar.
injetar apenas implementa o armazenamento necessário no aplicativo. Diferentes partes de nosso aplicativo usam repositórios diferentes, que listamos por
injeção , separados por vírgulas. Naturalmente, as lojas conectáveis devem ser listadas inicialmente no
provedor . Os repositórios estão disponíveis no componente através de
this.props.yourStoreName .
observador - o decorador indica que nosso componente será inscrito nos dados que são modificados usando o Mobx. Os dados foram alterados - ocorreu uma reação no componente (será mostrado abaixo). Portanto, não há assinaturas e retornos de chamada especiais - o Mobx entrega as alterações em si!
Voltaremos a gerenciar todo o aplicativo na
mainStore , mas por enquanto faremos os componentes. Temos três tipos deles -
Fio ,
Email ,
Button . Permita que o primeiro e o terceiro sejam universais e o
Email - personalizado. Vamos começar com ele.
O display é o habitual componente React burro:
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 o componente externo da validação, e é importante fazer isso depois que o elemento já estiver incluído no layout. Portanto, o método da loja é chamado em
componentDidMount .
Agora o próprio repositório:
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 a pena prestar atenção em duas novas entidades.
observável - um objeto, qualquer alteração de campo é monitorada pelo Mobx (e envia sinais para o
observador , que é inscrito em nosso armazenamento específico).
ação - esse decorador deve envolver qualquer manipulador que altere o estado do aplicativo e / ou cause efeitos colaterais. Aqui,
alteramos o valor do
valor nos
params @observable .
É isso aí, nosso componente simples está pronto! Ele pode rastrear os dados do usuário e gravá-los. Mais tarde, veremos como o repositório central do
mainStore se inscreve para alterar esses dados.
Agora, um componente típico do
Fio . Sua diferença em relação à anterior é que vamos usar componentes desse tipo um número ilimitado de vezes em um aplicativo. Isso impõe alguns requisitos adicionais no armazenamento de componentes. Além disso, faremos mais dicas sobre os caracteres de entrada usando o excelente serviço
DaData . Exibição:
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";
Há algo novo aqui: não acessamos o estado do componente diretamente, mas através de
get :
get(FioStore.items, name)
O fato é que o número de instâncias de componentes é ilimitado e o repositório é um para todos os componentes desse tipo. Portanto, durante o registro, inserimos os parâmetros de cada instância no
Mapa :
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;
O estado do nosso componente genérico é inicializado da seguinte maneira:
@observable items = new Map([]);
Seria mais conveniente trabalhar com um objeto JS comum, no entanto, ele não será "tocado" ao alterar os valores de seus campos, porque os campos são adicionados dinamicamente quando novos componentes são adicionados à página. Ao receber dicas do DaData, retiramos separadamente.
O componente do botão é semelhante, mas não há dicas:
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;
O componente do
Button é envolvido pelo componente HOC do
ButtonArea . Observe que o componente mais antigo inclui seu próprio conjunto de lojas e o mais novo. Nas cadeias de componentes aninhados, não há necessidade de encaminhar nenhum parâmetro e retorno de chamada. Tudo o que é necessário para a operação de um componente específico é adicionado diretamente a ele.
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> ); } }
Então, temos todos os componentes prontos. O assunto é deixado para o gerente da
mainStore. Primeiro, todo o código de armazenamento:
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];
Mais algumas entidades importantes.
computado é um decorador para funções que rastreiam mudanças em nosso
observável . Uma vantagem importante do Mobx é que ele rastreia apenas os dados calculados e retornados como resultado. A reação e, como conseqüência, o redesenho do DOM viral ocorrem apenas quando necessário.
reação - uma ferramenta para organizar efeitos colaterais com base em um estado alterado. São necessárias duas funções: a primeira calculada, retornando o estado calculado, a segunda com efeitos que devem seguir as alterações de estado. No nosso exemplo, a
reação é aplicada duas vezes. No primeiro, examinamos o estado dos campos e concluímos se todo o formulário está correto e também registramos o valor de cada campo. No segundo, clicamos em um botão (mais precisamente, se houver um sinal de "botão pressionado"), enviamos dados para o servidor. O objeto de dados é exibido no console do navegador. Como o
mainStore conhece todos os repositórios, imediatamente após o processamento de um botão, podemos desativar a sinalização em um estilo imperativo:
get(this.ButtonStore.items, "send_data").isClicked = false;
Você pode discutir o quão aceitável é a presença desse "imperativo", mas, em qualquer caso, o controle ocorre apenas em uma direção - do
mainStore ao
ButtonStore .
A execução automática é usada onde queremos executar algumas ações diretamente, não como uma reação para armazenar alterações. No nosso exemplo, uma função auxiliar é iniciada, além de pré-preencher os campos do formulário com os dados do dicionário.
Assim, a sequência de ações que temos é a seguinte. Os componentes rastreiam eventos do usuário e alteram seu estado.
O mainStore por meio do cálculo calcula o resultado com base apenas nos estados que foram alterados. Diferentes
computados procuram alterações em diferentes estados em diferentes repositórios. Além disso, por meio da
reação, com base nos resultados
computados , realizamos ações com
observáveis , além de efeitos colaterais (por exemplo, fazemos uma solicitação AJAX).
O Obsevables assina componentes filho, que são redesenhados, se necessário. Um fluxo de dados unidirecional com controle total sobre onde e o que muda.
Você pode experimentar o exemplo e o código. Link para o repositório:
github.com/botyaslonim/mobx-habr .
Então, como de costume:
npm i ,
npm execute local . Na pasta
pública , arquivo
index.html . As dicas do DaData funcionam na minha conta gratuita; portanto, elas provavelmente podem cair em alguns pontos devido ao efeito habr.
Ficarei feliz em quaisquer comentários e sugestões construtivas sobre o trabalho de aplicativos no
Mobx!Concluindo, direi que a biblioteca simplificou bastante o trabalho com dados. Para aplicativos de pequeno e médio porte, será definitivamente uma ferramenta muito conveniente para esquecer as propriedades de componentes e retornos de chamada e se concentrar diretamente na lógica de negócios.