React + MobX, lições aprendidas

Caio Vaccaro
7 min readJun 3, 2018

--

Introdução

Faz alguns meses que meu desafio tem sido montar e desenvolver o Front-end de um produto. Quando comecei, só me falaram que tinham decidido usar React, mas fora isso eu podia construir como quisesse.

Image from Adweek article.

Redux?

Dentre várias escolhas que tive que fazer, uma delas era se usava Redux ou não, e como lidaria com o dados na aplicação. Eu já tinha familiaridade com Redux e algumas implementações Flux, assim como o seu ecossistema, mas uma coisa que tem me incomodado um pouco com o Redux é que existem cada vez mais bibliotecas e dependências para cada tipo de problema/situação que você encontra e como a maior parte do conhecimento disponível online se baseia nos boilerplates mais famosos, você acaba sendo forçado a plugar diversas libs no projeto sem nem saber se precisa de fato delas. 😞

MobX

Comecei a olhar alternativas, e encontrei o MobX. A princípio achei bem diferente, tem uma certa curva de aprendizado, mas passa rápido. É interessante que eles tem um apoio bom de desenvolvedores/empresas, a princípio não são movidos por interesse corporativo e a comunidade é relativamente boa (não se compara ao redux, claro). Além de tudo, é muito fácil começar a usar o MobX. Mas, eu passei por algumas pegadinhas que eu gostaria de saber quando comecei, então resolvi escrever algumas dicas.

Antes de explicar como o MobX funciona, como o contexto é React vale dizer brevemente que também tive que escolher se usaria o Create React App ou não. Eu precisava de server-side-rendering, que eles não suportam, e entre as recomendações do próprio Facebook para SSR, tem o Next.js e o Razzle. Gosto bastante do Next e todo o ecossistema da Zeit, mas de uma forma ou de outra você acaba ficando preso nele. O Razzle, por definição é muito flexível. É apenas uma base para o projeto, praticamente tudo é customizável. Como eu ainda não sabia muito o que vinha pela frente no meu desafio, fui com ele (vale dar uma olhada). Para usar o React (via Razzle) com MobX, só precisei instalar e importar no projeto o mobx e o mobx-react.

Como funciona

Vamos ver alguns conceitos básicos do MobX.

O foco da biblioteca é gestão escalável de state (da mesma forma que o Redux, não precisa ser com React) de aplicações. E essa escalabilidade é alcançada por uma reatividade transparente (o nome chique é TFRP, programação funcional reativa transparente). Isso significa que tudo que for relacionado ao estado da aplicação deve ser derivado automaticamente (ou automagicamente). Vai ficar mais claro já já.

Observables

A base para isso acontecer é o uso de observables. De forma bem simples, um observable adiciona a uma estrutura de dado existente, a possibilidade de ser “observado” por alguém. É semelhante ao design pattern de Pub/Sub ou Mediator, onde uma parte A pede para ser avisada quando algo acontecer na parte B, porém aqui, além disso acontecer automaticamente (sem precisar “subscrever”), o que é observado é o valor em si, ao invés de callbacks criados por você.

É opcional o uso de decorators no MobX, é só uma forma de escrever um pouco menos de código e manter a sua estrutura atual. Todo decorator tem uma função correspondente. Para habilitar decorators, talvez você precise alterar a configuração do babel.

Um exemplo é usar o decorator @observable e @observer:

import {observable} from "mobx";
import {observer} from "mobx-react";

@observable name = "Caio";
@observer
class Hello extends React.Component {
render() {
return (
<p>Oi, {this.props.name}!</p>
)
}
}
ReactDOM.render(<Hello name={name} />, document.getElementById('root'));

Veja que sem precisar escrever nada específico, o observador reagirá sozinho quando o observable name mudar seu valor. Mesmo que você tenha muitos observables complexos, o MobX internamente registra somente o que está sendo usado por você no método render.

Bacana, né? Bem fácil e direto. 👌

Exemplo anterior sem decorator:

let name = observable("Caio");

const Hello = observer(({name}) => {
return(
<p>Oi, {this.props.name}!</p>
)
});

ReactDOM.render(<Hello name={name} />, document.getElementById('root'));

Você encontra mais exemplos de com/sem decorator na documentação.

Computed values

De início pode parecer um pouco desnecessário usar essa feature, mas ela é bem poderosa. Apesar de ser bem fácil criar um observable e um observer, o MobX oferece várias ferramentas para você manter bem clara a distinção entre a camada de dados e a de interface. Computed values são valores derivados de um ou mais observables, que você também quer usar.

import {observable, computed} from "mobx";
import {observer} from "mobx-react";

@observable firstName = "Caio";
@observable lastName = "Vaccaro";
// getter, imagina que estamos numa classe!
@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}

@observer
class Hello extends React.Component {
render() {
return (
<p>Oi, {this.props.fullName}!</p>
)
}
}

ReactDOM.render(<Hello fullName={fullName} />, document.getElementById('root'));

Você pode usar isso para várias coisas, por exemplo montar um objeto para um request AJAX:

@observable someSearchFilter = observable.map();
@observable anotherSearchFilter = observable.map();

@computed get queryObject() {
return {
filterOne: Array.from(this.someSearchFilter.keys()).join(','),
filterTwo: Array.from(this.anotherSearchFilter.keys()).join(',')
}
}

Aqui usei um observable.map, você pode saber mais sobre os métodos disponíveis aqui.

Ao invés de juntar pedaços nos componentes ou em outros módulos, se pergunte se você pode fazer isso com computed values, se sim, use!

Custom reactions

Não são só componentes React que podem reagir aos observables, qualquer código pode. Existem 3 funções para isso, autorun, reaction e when.

Continuando o exemplo anterior, se você quiser de fato fazer uma chamada AJAX sempre que atualizarem um filtro de busca:

import {observable, computed, autorun} from "mobx";
import {observer} from "mobx-react";

@observable someSearchFilter = observable.map();
@observable anotherSearchFilter = observable.map();

@computed get queryObject() {
return {
filterOne: Array.from(this.someSearchFilter.keys()).join(','),
filterTwo: Array.from(this.anotherSearchFilter.keys()).join(',')
}
}

autorun(() => {
// É claro que no caso real você vai usar um throttle, etc
yourAjaxFunction(this.queryObject);
});

Ok! Parece que é isso né? Já é suficiente para brincar um pouco e ter sua aplicação funcionando. 🚀

Actions

Apesar de não ser obrigatório, eu aconselho muito que você use actions. Por padrão, o MobX vai fazer o seu trabalho, todo observer vai reagir aos observables que estão usando (somente estes) quando eles forem alterados, mas não existe nada que impeça os observables de serem alterados de qualquer lugar, inclusive no seu próprio componente!

@observable firstName = “Caio”;
@observable lastName = “Vaccaro”;
@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@observer
class Hello extends React.Component {
render() {
return (
<p>Oi, {this.props.fullName}!</p>
)
}
}
// isso funciona… mas não é muito legal, ok?
firstName = “Fernando”;

É totalmente opcional usar actions, mas além de ajudar com a performance, ajuda você a estruturar seu código.

import {observable, computed, autorun, action} from "mobx";
import {observer} from "mobx-react";

@observable someSearchFilter = observable.map();
@observable anotherSearchFilter = observable.map();

@computed get queryObject() {
return {
filterOne: Array.from(this.someSearchFilter.keys()).join(','),
filterTwo: Array.from(this.anotherSearchFilter.keys()).join(',')
}
}

@action
addFilter = (observableName, item) => {
this[observableName].set(item.id, item);
}

@action
replaceFilter = (observableName, items) => {
this[observableName].replace(items);
}

autorun(() => {
yourAjaxFunction(this.queryObject);
});

@observer
class YourComponent extends React.Component {
render() {
return(
// Em algum lugar no seu render…
<Checkbox
onClick={addFilter
.bind(null, someSearchFilter, item)}
value={item}
/>
// Em outro lugar…
<button
onClick={replaceFilter
.bind(null, someSearchFilter, items)}
>Aplicar</button>
}
}

Ok! Agora podemos ir para as dicas. Recapitulando:

  • Para dados que são mutáveis e são usados em um ou mais componentes, ou que causam qualquer código javascript rodar com frequência, use um observable.
  • Se algum dado que você precisa é uma combinação de outros dados, use computed values.
  • Para componentes react reagirem aos observables, use observer.
  • Para qualquer código javascript reagir aos observables, use autorun, reaction ou when.
  • Para organizar melhor seu código e manter um padrão de quem pode modificar os observables, use as actions.

Primeira dica: Use Domain Stores.

Separe seus observables e a lógica relacionada a eles em Stores por domínio (por exemplo usuário, busca, feed, etc).

class ExampleStore {
@observable someSearchFilter = observable.map();
@observable anotherSearchFilter = observable.map();

@action
addFilter = (observableName, item) => {
this[observableName].set(item.id, item);
}
}

Para usar as stores em seus componentes, use um Provider e inject.

import {Provider, inject, observer} from "mobx-react";

<Provider store={new ExampleStore()}>
<App />
</Provider>

@inject('store')
@observer
class Hello extends React.Component {
render() {
return (
<p>Oi, {this.props.store.fullName}!</p>
)
}
}

Segunda dica: Use enforceActions.

Usando a opção enforceActions, o MobX só vai observar mudanças em observables que aconteçam como consequencia de uma action declarada.

import { configure } from "mobx";

configure({
enforceActions: "strict"
});

class ExampleStore {
@observable firstName = "Caio";
@observable lastName = "Vaccaro";

@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}

@action
setFirstName = (name) => { this.firstName = name };
}

@inject('store')
@observer
class Hello extends React.Component {
render() {
// Throw. Não pode fazer isso, um erro será gerado.
this
.props.store.firstName = 'Gustavo';
// Isso sim…
this
.props.store.setFirstName('Gustavo');

return (
<p>Oi, {this.props.store.fullName}!</p>
)
}
}

Terceira dica: Só exponha suas actions.

Escolha uma forma de só permitir o acesso as actions de suas Stores e somente leitura dos observables, seja exportando elas para outro objeto ou deixando claro que elas são públicas. Algumas actions serão internas da store e não devem ser usadas por componentes, outras sim. Existem muitas formas de fazer isso, na própria documentação do MobX eles deixam claro que a biblioteca não opina sobre como você quer lidar com os eventos e o que dispara as actions. Você pode usar alguma implementação Flux ou qualquer outra biblioteca para criar esse canal de comunicação. Uma vez que uma ação do usuário aconteça e dispare uma action interna da sua store, o MobX cuida do resto de forma reativa.

Conclusão

Tanto o MobX quanto o Redux são bons e cumprem o que prometem. Apesar de existirem diversos benchmarks dizendo que em alguns casos de uso o MobX supera o Redux em performance, eu não tenho como negar nem afirmar se um é melhor que o outro. Por enquanto parecem apenas estilos diferentes. Acredito que o ideal é, se você for usar Redux, saiba a razão de cada biblioteca existir e tente usar o mínimo possível delas, deixando o código o mais direto possível. Se for usar MobX, separe bem dados e lógica da interface, e faça tudo seguindo um padrão simples e redundante.

Se você quer saber mais, esses dois links bastam (em inglês):

Esses são bons exemplos de projetos com MobX:

E claro, nem tudo é Mobx, nem React, o importante é saber usar as ferramentas quando convém. 😉

Um desafio remoto possibilita morar em lugares assim.

--

--

Caio Vaccaro
Caio Vaccaro

Responses (2)