Pular para conteúdo
Victor Magalhães
vhfmag@protonmail.com
Disclaimers Aqui não há lugar para o ódio. Este espaço está em defesa das mulheres, da população negra, indígena, pobre, LBGTTIQ, imigrante, muçulmana, judia, refugiada e todas as pessoas sob ataque. #SomosTodasAntifascistas

Apoia o atual presidente? Você não é bem vindo, fascista. Saia 👋🏾

Espécies estão sendo extintas; cidades, afundadas; já está faltando comida. Mas ainda dá pra fazer algo contra a catástrofe climática:

Uma introdução a MobX

Entendendo os principais conceitos dessa biblioteca de gerenciamento de estado para web apps

O MobX é uma biblioteca de gerenciamento de estado para Javascript frequentemente utilizada com React. Ela tem uma curva de aprendizado menos abrupta do que a sua alternativa mais popular, o Redux, apesar de (de acordo com minha experiência na Cubos) más práticas emergirem com mais frequência com seu uso. Nesse artigo, usaremos alguns exemplos de código em Typescript para explicar os principais conceitos, boas práticas e alguns gotchas.

O que é MobX

O MobX aplica conceitos de programação funcional reativa em Javascript de forma transparente, escondendo detalhes complexos de implementação por trás de objetos Javascript puros. Tirando o jargão, isso significa que ele permite que partes de sua aplicação sejam notificadas de mudanças de estado sem mudar o paradigma de programação do seu projeto. Para determinar o que é observado e por quem, a biblioteca se utiliza de dois conceitos de base: observáveis e reações.

Observáveis e reações

Observáveis são porções de estado (variáveis, listas, objetos, campos de uma classe, etc) que notificam ouvintes (as reações) quando há mudanças. Observáveis são, em geral, definidos usando a função observable, exportada pelo módulo mobx. Há várias maneiras de declarar reações, uma delas sendo usar a função autorun. Segue exemplo:

import { observable, autorun } from "mobx";

const lista = observable([1, 2, 3]);

autorun(() => {
	const soma = lista.reduce((acc, el) => acc + el, 0);
	console.log(soma);
});
// printa 6

lista.push(4);
// printa 10

Eis o que a biblioteca faz, por debaixo dos panos: roda a função passada para autorun uma vez, registra todos os observáveis utilizados dentro da função e a executa todas as vezes que algum desses observáveis mudar. O uso acima mostrado de observáveis é, apesar de possível, raro na prática. Com frequência, usamos classes que contém campos observáveis. Segue um exemplo mais próximo do uso prático:

class TaskStore {
	@observable
	public tasks: Task[];

	constructor() {
		try {
			this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
		} catch {
			this.tasks = [];
		}

		autorun(() => localStorage.setItem("tasks", JSON.stringify(this.tasks)));
	}
}

O exemplo acima ilustra um dos usos mais frequentes de autorun na prática: persistir estado. Além disso, usa-se observable como um decorador, o que só é possível dentro de classes.

Observáveis: valores a que reações podem se inscrever para ser notificadas de mudanças

Reações: ações a serem executadas sempre que determinados observáveis mudarem

Valores derivados

Até agora, nós discutimos observáveis e reações. No entanto, reagir a mudanças no estado nem sempre é suficiente: por vezes, precisamos usar uma parte do estado para calcular outra. Por exemplo:

import { observable } from "mobx";

interface ExamResult {
	student: string;
	grade: number;
}

class ExamStore {
	@observable
	public results: ExamResult[] = [];

	public averageGrade() {
		return this.results.reduce((acc, el) => acc + el, 0) / this.results.length;
	}
}

const store = new ExamStore();
store.results.push({ student: "Victor", grade: 7 });
store.results.push({ student: "Clara", grade: 8 });
store.results.push({ student: "Dygufa", grade: 9 });

console.log(store.averageGrade());
document.querySelector("avg").innerText = `Média da turma: ${store.averageGrade()}`;

Nesse exemplo, averageGrade é chamado duas vezes, recalculando a média desnecessariamente. Isso poderia ser otimizado, já que enquanto results não mudar, a média permanecerá a mesma (para quem é familiar com Redux, nesse contexto se usariam seletores). E é justamente para isso que serve o computed, mais uma função da biblioteca. O exemplo acima ficaria assim, quando reescrito para utilizar computed:

import { observable, computed } from "mobx";

interface ExamResult {
	student: string;
	grade: number;
}

class ExamStore {
	@observable
	public results: ExamResult[] = [];

	@computed
	public get averageGrade() {
		return this.results.reduce((acc, el) => acc + el, 0) / this.results.length;
	}
}

const store = new ExamStore();
store.results.push({ student: "Victor", grade: 7 });
store.results.push({ student: "Clara", grade: 8 });
store.results.push({ student: "Dygufa", grade: 9 });

console.log(store.averageGrade);
document.querySelector("avg").innerText = `Média da turma: ${store.averageGrade}`;

As mudanças foram:

  1. O decorador @computed foi adicionado ao método averageGrade
  2. O método foi convertido em getter - isso é um requisito para usar computed
  3. Por ser um getter, averageGrade passa a ser utilizado como um campo, não um método (e por isso não é mais chamado)

Com essas 3 mudanças, o MobX otimiza o cálculo da média, fazendo-o apenas quando necessário. Além disso, averageGrade também é um observável, o que permite que reações se inscrevam a ele e reajam a mudanças.

Valores derivados: observáveis derivados a partir de outros observáveis que são automaticamente re-calculados sempre que os observáveis de que depende mudam

Dica: sempre que possível, use computed para derivar informações a partir do estado: isso evita que computações custosas se repitam desnecessariamente

Observadores

Antes de introduzir outros conceitos de MobX, gostaria de tornar nossos exemplos mais práticos, usando o MobX em uma aplicação React: