Observando os estados de uma aplicação React com a biblioteca MobX
Se você já trabalhou com gerenciamento de estado em aplicações React, sabe que pode ser uma tarefa desafiadora, especialmente quando o projeto começa a crescer e os requisitos para lidar com o estado começam a se tornar mais complexos, como nos casos em que temos:
- O compartilhamento de estados entre os componentes;
- A interdependência de estados, onde, por exemplo, o cálculo de um valor total, depende da alteração de uma lista de itens;
- A sincronização do estado local com dados externos;
Para que tudo isso funcione adequadamente, é necessário que o código esteja organizado, desacoplado e permita fácil manutenção e escalabilidade.
Por isso, vamos conhecer o MobX, uma biblioteca de gerenciamento de estado que vai nos ajudar a observar os estados da nossa aplicação e evitar as dores de cabeça causadas por um emaranhado de props
passadas de um componente para outro.
Então, se você quer saber como o MobX pode te ajudar na prática, vem comigo que neste artigo vamos:
- Entender os conceitos por detrás do MobX;
- Conhecer a estrutura e as principais partes que compõem essa biblioteca;
- Implementar os conceitos do MobX na prática;
- Saber porque o MobX pode ser uma boa escolha para o seu projeto.
Porque usar MobX?
Antes de mais nada, vamos entender porque devemos usar o MobX em um projeto React.
O MobX transforma os componentes React em observadores de estados e quando um desses estados muda as atualizações acontecem automaticamente.
Para simplificar podemos pensar em uma sala com lâmpadas automáticas (componentes), conectadas a sensores de movimento (MobX).
Quando uma pessoa se move na sala (o estado muda), os sensores detectam a alteração e acendem a luz imediatamente.
Isso torna este gerenciador de estado extremamente reativo e reduz a necessidade de escrever códigos complexos para atualizar a interface, já que a biblioteca lida com as dependências com mais eficiência.
Dito isso, apenas os componentes que dependem diretamente das mudanças de estado são re-renderizados, aprimorando o desempenho da aplicação.
Outro ponto positivo do MobX é uma curva de aprendizagem mais suave, graças ao minimalismo de sua API, que reduz a complexidade de conceitos como reducers e actions obrigatórias, como no Redux.
Assim, como o Zustand e o Redux, o MobX também faz uso de uma store, mas sua diferença está na organização do código, que ocorre de forma modular e sem a imposição de uma estrutura rígida, o que garante maior flexibilidade e autonomia para a pessoa desenvolvedora.
E essa liberdade é tão interessante que o MobX permite inclusive o uso de decorators para tornar o código mais legível e declarativo.
Além disso, o MobX é uma excelente opção para projetos de diferentes tamanho, pois possibilita maior facilidade na manutenção e escalabilidade da aplicação.
Acho que eu já despertei a sua curiosidade para aprender mais sobre o MobX e considerar ele para te ajudar a gerenciar os estados da sua aplicação!
Agora que tal aprender mais sobre essa biblioteca?
Como funciona o fluxo de gerenciamento de estado do MobX?
O MobX atualiza os componentes automaticamente sempre que o estado monitorado muda.
Isso acontece, por conta do conceito de Reatividade Transparente (Transparent Functional Reactive Programming, ou TFRP) que é baseado em dois elementos principais: os observadores (observer) e os observáveis (observables).
- Observables (observáveis): são os dados que podem ser monitorados. Por exemplo, o estado de uma lista de um carrinho de compras.
- Observers (observadores): transforma componentes em "observadores" de estados observáveis. Por exemplo, o próprio componente "CarrinhoDeCompras".
Como funciona o fluxo de dados para que essas alterações aconteçam?
Assim como o Redux, o MobX adota um fluxo unidirecional para atualizar os dados na interface, seguindo a sequência abaixo:
- Um evento dispara uma ação (action);
- Esta ação modifica o estado da minha aplicação (state);
- Após a modificação do estado os valores derivados são atualizados imediatamente (computed values);
- E as reações são acionadas (reactions).
Através deste fluxo, surgem novos conceitos para complementar os observer e observable, que são: as actions e valores derivados. Bora entender cada um deles:
As actions são qualquer parte do código que altera o estado observável (observable). Elas ajudam a organizar o código e geralmente são disparadas a partir de componentes. Um exemplo seria adicionar um item a uma lista de compras.
Já os valores derivados, são gerados a partir de outros dados. O MobX segue o princípio: "tudo aquilo que deriva do estado deve ser derivado automaticamente". Ou seja, uma vez configurados, esses valores são atualizados automaticamente sempre que os dados nos quais se baseiam forem alterados. São considerados valores derivados os computed values e as reactions.
Os computed values são usados para combinar dados existentes e produzir um novo valor, como é o caso do cálculo do valor total dos itens em um carrinho de compras.
Por outro lado, as reactions são aplicadas para executar efeitos colaterais (side effects) com base em mudanças no estado observável, como, exibir uma mensagem no console do navegador quando um item é adicionado ao carrinho, para realizar a depuração do código.
Agora, que tal colocar esses conceitos em prática? Para isso, vamos escrever o código do carrinho de compras que usamos de exemplo.
Explorando os conceitos de MobX na prática
Para criar os observables, actions, reactions e computed values vamos precisar criar uma pasta store e um arquivo chamado CarrinhoStore.js que ficarão dentro da pasta "src". É aqui que todas as informações relacionadas ao estado irão ficar.
Um carrinho de compras nada mais é do que um array de produtos, por isso criaremos um observable que vai tornar essa lista um estado observado.
class CarrinhoStore {
@observable accessor itens = [];
}
Neste código criamos a classe CarrinhoStore
e dentro dela adicionamos um estado observável (observable) chamado itens
que guardam um array vazio com a ajuda dos decorators
Aqui já surge uma estrutura bem interessante para analisarmos. Criamos uma classe chamada "CarrinhoStore" e usamos decorators para declarar o estado observável.
O MobX não impõe uma estrutura rígida, pois ele é uma biblioteca de gerenciamento de estado não opinativa.
Contudo, é bem comum utilizar o paradigma da Programação Orientada à Objetos (POO), pela facilidade em detectar API's e por se beneficiar das otimizações dos navegadores por conta da previsibilidade e compartilhamento de métodos.
Caso queira conferir, você pode acessar a documentação oficial.
Além disso, se você não tiver familiaridade com este assunto e quiser aprender mais, você pode consultar o curso de JavaScript: classes e heranças no desenvolvimento de aplicações com orientação a objetos.
Além do MobX não ser opinativo, eu também comentei que ele permitia utilizar decorators para escrever funcionalidades de maneira declarativa e modular.
Como é o caso do @observable
. Mas essa praticidade vem acompanhada de uma configuração especial que você pode consultar no artigo Guia de bibliotecas para lidar com gerenciamento de estados em projetos React na seção sobre MobX e na documentação do MobX sobre decorators.
Mas caso você não queira usar *decorators para escrever o código, você pode optar por usar a própria API do MobX (makeObservable
). Nesse caso, o código ficaria assim:
class CarrinhoStore {
itens = [];
constructor() {
makeObservable(this, {
itens: observable,
});
}
}
Criamos a varíavel itens
, com um array vazio, dentro da classe CarrinhoStore
e definimos um construtor para configurar as propriedades desde o momento em que o objeto é instanciado.
No construtor, utilizamos a API do MobX (makeObservable
) para que ele identifique se um valor é uma observable
, action
, reaction
ou computed value
, especificamente para esta situação definimos a variável itens
como um observable
.
Para adicionar produtos em uma lista, precisamos ter esses produtos. Por isso, dentro da classe CarrinhoStore
, vamos configurar a seguinte lista de objetos:
produtos = [
{ id: 1, nome: "Produto 1", preco: 10 },
{ id: 2, nome: "Produto 2", preco: 20 },
{ id: 3, nome: "Produto 3", preco: 30 },
];
Definimos um conjunto com três objetos que possuem um "id", um "nome" e um "preço".
Note que ele não é associado a nenhuma das partes do MobX, mas vai ser importante para algumas ações futuras, por isso ele se encontra na store.
Um carrinho de compras possui a possibilidade de adicionar e remover itens. Essas duas funcionalidades podem ser criadas através de ações (actions). Então vamos criar alguns métodos dentro da nossa classe CarrinhoStore
.
@action adicionarItem = (produtoId) => {
const itemExistente = this.itens.find((item) => item.id === produtoId);
if (itemExistente) {
itemExistente.quantidade += 1;
} else {
const produto = this.produtos.find((produto) => produto.id === produtoId);
if (produto) {
this.itens.push({ ...produto, quantidade: 1 });
}
}
};
@action removerItem = (index) => {
const item = this.itens[index];
if (item.quantidade > 1) {
item.quantidade -= 1;
} else {
this.itens.splice(index, 1);
}
};
Criamos as ações:
adicionarItem
que aplica uma lógica condicional onde, se um produto estiver no carrinho, o valor deste produto é incrementado em uma unidade e se o valor não estiver no carrinho ele faz a adição do produto.- E
removerItem
, responsável por buscar a posição do item e decrementar em uma unidade caso a quantidade seja maior que 1 ou remover o item da lista se a quantidade for igual a um.
Até aqui os itens já são adicionados na lista, mas todo carrinho de compras precisa mostrar o valor final da compra e como isso pode ser feito?
Isso pode ser feito por meio do computed value, que vai multiplicar o preço de cada produto pela quantidade que foi adicionado ao carrinho e vai fazer a soma de todos esses valores para obter um novo valor que é acessado como uma variável convencional através do getter
.
@computed get total() {
return this.itens.reduce((acc, item) => acc + item.preco * item.quantidade, 0);
}
Para garantir que todos os produtos sejam adicionados na lista, podemos exibir uma mensagem no console com a quantidade total de produtos que estão no array.
Isso pode ser muito interessante para nos ajudar a depurar o código e identificar problemas.
E a melhor opção para lidar com os efeitos colaterais de uma ação são as reações (reactions).
constructor() {
reaction(
() => this.itens.map(item => item.quantidade),
(quantidades) => {
const totalItens = quantidades.reduce((acc, quantidade) => acc + quantidade, 0);
console.log(`Quantidade total de itens no carrinho: ${totalItens}`);
}
);
}
Para escrever uma reaction, usamos duas funções: uma para definir o que observar e outra para agir quando algo mudar.
Essa separação ajuda a monitorar apenas o necessário, reagir a mudanças importantes e evitar problemas desnecessários. Além disso, torna a lógica de observação mais flexível e reutilizável.
No caso do código acima, a primeira função (() => this.itens.map(item => item.quantidade)
) observa as quantidades dos itens no carrinho.
Já a segunda função é chamada assim que uma alteração na quantidade é detectada, ela captura o valor da nova quantidade e exibe uma mensagem no console com este valor.
Além destas duas funções obrigatórias, também podemos utilizar alguns parâmetros opcionais, como:
fireImmediately
: um valor booleano, responsável por definir se a função de reação deve ser executada imediatamente na primeira vez que o reaction for criado. O valor padrão éfalse
;delay
: adiciona um atraso (em milissegundos) antes de executar a função de reação. Isso pode ser útil para evitar execuções muito rápidas em caso de alterações frequentes;equals
: personaliza como o MobX deve comparar os valores retornados pela função de observação. Por padrão, o MobX usa uma comparação simples, mas você pode substituir por sua própria lógica.
Caso queira, você pode consultar a documentação para entender melhor como funcionam as reações e os valores opcionais que podem ser utilizados.
Para implementar uma reaction utilizei um constructor para configurar uma reação no momento em que uma classe é instanciada.
Isso acontece, pois a reaction não define ou modifica diretamente propriedades ou métodos da classe, mas sim monitora mudanças de estado e executa efeitos colaterais.
Os decorators são declarativos e associados a elementos individuais de uma classe, enquanto as reactions são dinâmicas e permitem monitorar combinações de estados e agir de forma condicional.
Além disso, ele é configurado externamente para maior flexibilidade, algo que não se alinha bem com a natureza estática dos decorators.
Prontinho! Com a classe pronta, a instanciamos e exportamos para usá-la nos componentes React.
export default new CarrinhoStore();
Já temos o estado observável, mas ainda não temos ninguém para observar o estado e dizer pro MobX quando aplicar as modificações.
É aqui que entram os componentes, eles vão espionar o estado e sinalizar toda vez que houver uma mudança.
Como podemos definir um componente como um observer?
Há algumas formas como:
- Consumindo observables diretamente de escopos externos: usamos observer para transformar o componente React em um "observador" de um observable declarado fora do componente;
- Utilizando React Context para compartilhar observables: o Context é utilizado para compartilhar um observable com uma árvore de componentes, simplificando a injeção de estado;
- Criando estado local observável com
useLocalObservable
: usamos um hook para criar um estado observável diretamente no componente, ideal para estados locais simples; - Passando
observables
comoprops
: o observable é passado como propriedade, permitindo que o componente consuma e reaja às mudanças; - Criando
observables
comuseState
eobservable
: combina o React state com MobX para gerenciar observáveis em estado local; - Utilizando classes observáveis como
props
ou estados: uma classe contendo o observable é instanciada e consumida diretamente, seja como prop ou como estado inicial.
Caso queira exemplos de como implementar cada uma destas formas para definir o estado em componentes observadores, consulte as documentações referentes ao Uso do estado externo em componentes observadores e Uso do estado observável local em componentes observadores.
Vamos consumir os observables criados na store diretamente de escopos externos.
Isto significa que, o componente que criaremos vai utilizar a classe CarrinhoStore
diretamente do escopo externo. O código ficará assim:
import { observer } from "mobx-react";
import carrinhoStore from "../../store/CarrinhoStore";
const Carrinho = observer(() => {
return (
<div>
<h2>Produtos</h2>
<div>
{carrinhoStore.produtos.map((produto) => (
<button key={produto.id} onClick={() => carrinhoStore.adicionarItem(produto.id)}>
Adicionar {produto.nome} - R${produto.preco}
</button>
))}
</div>
<h3>Carrinho</h3>
<ul>
{carrinhoStore.itens.map((item, index) => (
<li key={index}>
{item.nome} - R${item.preco} x {item.quantidade}
<button onClick={() => carrinhoStore.removerItem(index)}>
Remover
</button>
</li>
))}
</ul>
<p>Total: R${carrinhoStore.total}</p>
</div>
);
});
export default Carrinho;
Neste código, temos um componente retornando três partes principais:
- Um botão para cada produto configurado na classe
CarrinhoStore
totalizando três botões e aplica o métodoadicionarItem
; - Um array com a lista de itens, o preço, a quantidade e um botão de "Remover" que possui o método
removerItem
; - Um parágrafo que recebe o método
total
da classeCarrinhoStore
.
Com a estrutura pronta, envolvemos o componente Carrinho
com um observer
da biblioteca mobx-react
, que nada mais é do que um High Order Component (HoC), mas o que é esse tal de HoC? Ele é uma função que recebe um componente como entrada e retorna um novo componente como saída, adicionando funcionalidades ou modificando o comportamento do componente original.
Suponha que você tem um componente simples e quer adicionar alguma funcionalidade extra — como autenticação, logging, ou reatividade — sem misturar essa lógica diretamente no componente.
É aqui que os HoC brilham, eles te ajudam a encapsular essa lógica e "envolve" o componente original, resultando em uma versão aprimorada dele.
No caso do carrinho de compras, o observer
transforma o componente Carrinho
em um componente reativo.
A partir disso, ele passa a "observar" e reagir às mudanças nos estados gerenciados pelo MobX.
Caso queira ver outros exemplos e explorar mais os High Order Components, consulte a documentação oficial.
Agora com tudo pronto, basta aplicar o componente "Carrinho" no arquivo "App.jsx", e teremos o seguinte resultado:
Com o carrinho de compras pronto, vamos analisar como a aplicação lida com as mudanças de estado.
- Um evento de clique no botão de produto, dispara a ação que adiciona um item ao carrinho de compras;
- Essa ação modifica o estado observável de um array vazio para um array com um elemento;
- À medida que são inseridos novos itens ao array, o valor total da compra é alterado;
- Simultaneamente, ao adicionar um item no array uma reação é acionada e exibe uma mensagem no console do navegador com a quantidade total de itens.
Mais exemplos do uso do MobX para gerenciamento de estados
Aplicamos o MobX em um exemplo simples de carrinho de compras, mas ele pode ser usado em muitas outras aplicações, como em formulários dinâmicos, dashboards interativos que exibem gráficos dinâmicos, filtros e tabelas de dados, chats de mensagem em tempo real, feed de notícias com atualizações em tempo real, atualizações dinâmicas de preços de ações, entre outros.
Com tantas possibilidades, que tal praticar o que aprendemos em outros projetos? Assim, você reforça seus conhecimentos e aprofunda o que exploramos sobre o MobX. Topa o desafio?
Conclusão
Chegamos ao final de mais um artigo, e com esse texto exploramos uma das muitas bibliotecas que nos ajudam a gerenciar os estados de aplicações React, o MobX.
Aprofundamos nossos conhecimentos nos conceitos fundamentais do MobX, que incluem: os observables, observer, action, reaction e computed values, e aplicamos os conceitos na criação de um carrinho de compras.
Além disso, usamos decorators e Programação Orientada a Objetos (POO) para nos ajudar a construir nossa store.
Falamos sobre as vantagens do MobX para gerenciar estados em projetos variados e exploramos exemplos para você praticar e ampliar seus conhecimentos.
Espero que esta jornada tenha sido tão incrível para você quanto foi para mim e nos vemos em uma próxima oportunidade!