Guia de bibliotecas para lidar com gerenciamento de estados em projetos React

Guia de bibliotecas para lidar com gerenciamento de estados em projetos React
RODRIGO SILVA HARDER
RODRIGO SILVA HARDER

Compartilhe

Quando você escolhe trabalhar com React, pode acontecer situações onde você precise compartilhar informações entre componentes. E qual a primeira solução que vem à mente? Isso mesmo, usar props!

Num primeiro momento passar dados via props resolve o problema. Entretanto, à medida que a aplicação cresce, vários componentes podem precisar de uma mesma informação e usar props pode causar problemas de prop drilling.

Isto é, as informações são passadas através de muitos componentes até chegar onde realmente serão usadas. Mas, para toda dor existe um remédio, por isso, neste artigo vamos:

  • Conhecer algumas bibliotecas de gerenciamento de estado, especialmente o Zustand, Recoil, Redux e MobX;
  • Entender a estrutura de cada gerenciador de estado;
  • Explorar exemplos de como usar cada biblioteca;
  • Entender como escolher a biblioteca ideal para seu projeto.

Agora, vem comigo aprender como gerenciar melhor os estados da sua aplicação React.

O problema do prop drilling

Para entender o porquê usar um gerenciador de estado, é necessário compreender melhor o problema causado pela passagem de props em vários componentes, ou mais conhecido como prop drilling.

Lidar com essa situação pode ser desafiador, quanto maior o projeto e mais props são usadas, mais difícil se torna descobrir onde os dados são inicializados, atualizados e usados.

Bom, nada melhor do que um exemplo para identificar com clareza o prop drilling em ação.

Vamos pensar em uma lista de tarefas, que possui a seguinte estrutura:

|_src
    |_componentes
        |_Formulario
        |_ListaDeTarefas
        |_Tarefas
    |_App.jsx

Nesta aplicação teremos os seguintes códigos:

App.jsx:

import { useState } from 'react';
import ListaDeTarefas from './components/ListaDeTarefas';

export default function App() {
  const [tarefas, setTarefas] = useState([
    'Estudar React',
    'Fazer exercício',
    'Ler um livro'
  ]);

  const adicionarTarefa = (novaTarefa) => {
    setTarefas([...tarefas, novaTarefa]);
  };

  return (
    <div>
      <h1>Minha Lista de Tarefas</h1>
      <ListaDeTarefas
        tarefas={tarefas}
        adicionarTarefa={adicionarTarefa} />
    </div>
  );
}

Componente ListaDeTarefas:

import Tarefas from "../Tarefas";
import Formulario from "../Formulario";

export default function ListaDeTarefas({ tarefas, adicionarTarefa }) {
  return (
    <div>
      <Formulario adicionarTarefa={adicionarTarefa} />
      <Tarefas tarefas={tarefas} />
    </div>
  )
}

Componente Formulario:

import { useState } from "react";

export default function Formulario({ adicionarTarefa }) {
  const [novaTarefa, setNovaTarefa] = useState('');

  const submeterTarefa = (e) => {
    e.preventDefault();
    if (novaTarefa.trim()) {
      adicionarTarefa(novaTarefa);
      setNovaTarefa('');
    }
  };

  return (
    <form onSubmit={submeterTarefa}>
      <input
        type="text"
        value={novaTarefa}
        onChange={(e) => setNovaTarefa(e.target.value)}
        placeholder="Adicione uma nova tarefa"
      />
      <button type="submit">Adicionar</button>
    </form>
  );
}

Componente Tarefas:

export default function Tarefas({ tarefas }) {
    return (
        <ul>
            {tarefas.map((tarefa, index) => (
                <li key={index}>{tarefa}</li>
            ))}
        </ul>
    );
}

No exemplo acima, criamos no App.jsx uma lista de tarefas com useState (tarefas) e uma função que adiciona tarefas à lista (adicionarTarefa).

Esses dados são passados para o componente "ListaDeTarefas" que atua como um intermediário, e as repassa para os componentes "Formulario" e "Tarefas", sem usar diretamente as informações contidas nessas props.

Como dito lá no começo desse texto, um prop drilling é exatamente isso, uma sucessão de componentes que recebem as props sem usar seus valores apenas para que essas informações cheguem em níveis mais abaixo na hierarquia da árvore de componentes.

Uma árvore de componentes com vários níveis. As informações do topo estão passando em cada nível para chegar até ao final da hierarquia. Banner promocional da Alura, com um design futurista em tons de azul, apresentando o texto

Solução nativa do React: a Context API

A Context API permite passar dados pela árvore de componentes sem a necessidade de passar manualmente por todos os níveis.

Basicamente ela centraliza essas informações em um estado compartilhado, que pode ser compartilhado com qualquer componente independente do nível em que ele esteja na hierarquia da árvore de componentes.

Uma árvore de componentes com vários níveis e as informações do topo conseguem ser acessadas diretamente pelos níveis mais abaixo na hierarquia.

Isso é apenas a ponta do iceberg. Se quiser explorar mais afundo os conceitos da Context API, vou deixar aqui o curso React: gerencie estados globalmente com Context API do Neilton.

Agora que você entendeu mais sobre a Context API, vamos aplicá-la no exemplo da lista de tarefas que usamos anteriormente.

Inicialmente, é necessário criar um contexto que vai gerenciar os estados e as funções relacionadas às tarefas.

Para isso, precisamos criar uma pasta dentro de "src" chamada "context" e dentro dela um arquivo para armazenar o estado global. Podemos chamar o arquivo de "TarefasContext.jsx".

Vamos usar o método createContext para criar uma fonte de dados compartilhada, e o Provider para compartilhar os dados entre os componentes consumidores.

import { createContext, useState } from 'react';

export const TarefasContext = createContext();

export const TarefasProvider = ({ children }) => {
  const [tarefas, setTarefas] = useState([
    'Estudar React',
    'Fazer exercício',
    'Ler um livro'
  ]);

  const adicionarTarefa = (novaTarefa) => {
    setTarefas([...tarefas, novaTarefa]);
  };

  return (
    <TarefasContext.Provider value={{ tarefas, adicionarTarefa }}>
      {children}
    </TarefasContext.Provider>
  );
};

Em seguida, no arquivo "App.jsx", podemos importar o componente TarefasProvider e englobar todos os outros componentes que são renderizados para que eles tenham acesso ao contexto.

import ListaDeTarefas from "./components/ListaDeTarefas";
import { TarefasProvider } from "./context/TarefasContext";

export default function App() {
  return (
    <TarefasProvider>
      <div>
        <h1>Minha Lista de Tarefas</h1>
        <ListaDeTarefas />
      </div>
    </TarefasProvider>
  );
}

Vale conferir que as props tarefa e adicionarTarefa não são passadas para nenhum componente. Por isso, podemos removê-las do componente "ListaDeTarefas".

import Formulario from "../Formulario";
import Tarefas from "../Tarefas";

export default function ListaDeTarefas() {
  return (
    <div>
      <Formulario />
      <Tarefas />
    </div>
  );
}

Mas como as informações que estão no arquivo "TarefaContext.jsx" vão ser lidas pelos componentes "Tarefas" e "Formulario"? Boa pergunta, o React tem um hook exclusivo, chamado useContext, que permite consumir os dados de um contexto diretamente sem envolver outros componentes.

Dito isso, podemos reescrever os componentes "Tarefas" e "Formulario" da seguinte forma:

//Componente Tarefa

import { useContext } from "react";
import { TarefasContext } from "../../context/TarefasContext";

export default function Tarefas() {
  const { tarefas } = useContext(TarefasContext);
//RESTANTE DO CÓDIGO 
//Componente Formulario

import { useContext, useState } from "react";
import { TarefasContext } from "../../context/TarefasContext";

export default function Formulario() {
  const [novaTarefa, setNovaTarefa] = useState('');
  const { adicionarTarefa } = useContext(TarefasContext);

  const submeterTarefa = (e) => {
    e.preventDefault();
    if (novaTarefa.trim()) {
      adicionarTarefa(novaTarefa);
      setNovaTarefa('');
    }
  }
//RESTANTE DO CÓDIGO

Dessa forma, resolvemos o problema do prop drilling e usamos as informações diretamente onde queremos, sem precisar de intermediários.

Entretanto, nem tudo são flores, por mais que seja uma opção nativa, não precise de instalação e funcione em aplicações de pequeno e médio porte, ainda há alguns problemas nessa abordagem.

Quando você escolhe usar a Context API, pode se deparar com renderizações desnecessárias. Assim que o estado dentro do contexto que foi criado muda, todos os componentes que consomem esse contexto são renderizados novamente, mesmo que eles não precisem, para garantir que todos os locais que dependem daquele estado estejam atualizados, o que reduz o desempenho da aplicação.

Imagine que você mora em um prédio onde toda vez que a campainha de um apartamento em um determinado andar toca, todos os apartamentos daquele andar recebem uma notificação, mesmo que a visita seja apenas para um único morador.

Com certeza, para os demais moradores naquele andar, essa notificação é um esforço desnecessário. As renderizações da Context API, quando algo muda no contexto compartilhado, são muito semelhantes a essa situação.

Contudo, uma opção do React que pode ajudar a diminuir esse problema é usar o react.memo, que permite que você pule a re-renderização de um componente, quando as props que fazem parte dele não mudar.

Basicamente, ele cria uma versão memorizada de um componente. Essa versão pode não ser re-renderizada, desde que as props não sejam alteradas.

Se quiser explorar mais a fundo o react.memo e suas aplicações práticas, você pode consultar a documentação do React.

Mas é importante lembrar que o react.memo é uma otimização de desempenho não uma garantia. Por isso, o React ainda pode renderizar os componentes novamente.

Além disso, aplicações maiores que possuam lógicas de negócio complexas, podem ser difíceis de gerenciar e de rastrear as mudanças de estado ao usar o Context API.

Bibliotecas para gerenciar estados

Justamente por causa dessas limitações que a Context API possui, vamos explorar outras alternativas que podem ser uma ótima opção para gerenciar os estados da sua aplicação.

Redux

Vamos começar pelo Redux, uma biblioteca JavaScript bastante popular de gerenciamento de estado. O Redux segue o padrão Flux, que estabelece um fluxo unidirecional para lidar com os dados.

Caso queira entender melhor como funciona essa estrutura, deixo o artigo Redux: desvendando a arquitetura com Flux do Luiz e o artigo Estados globais: diferenças entre Redux e Context API do Matheus, que podem te ajudar a entender melhor esse conceito.

Mas basicamente a estrutura do Redux é composta por três elementos principais:

  • Actions que descrevem uma mudança no estado;
  • Reducers que processam essa mudança e retornam um novo estado;
  • Store responsáveis por armazenar o estado atual da aplicação.

Além disso, o Redux também conta com middleware, que interceptam as actions antes delas chegarem aos reducers.

São bastante úteis quando queremos lidar com lógica assíncrona, como chamadas de API, log de actions e outras tarefas que você precisa executar entre o despacho de uma action e a atualização do estado.

Um dos middlewares mais conhecidos do Redux é o Redux Thunk, que permite despachar actions assíncronas, e o Redux Saga, que usa generators para lidar com tarefas assíncronas de maneira mais elaborada.

O Redux é ideal para grandes aplicações onde é necessário compartilhar dados entre muitos componentes e garantir que o estado da aplicação seja consistente e previsível.

Por isso é recomendado em situações como a autenticação do(a) usuário(a) ou dados que afetam múltiplos componentes.

Redux Toolkit

O Redux Toolkit ou "RTK" é a abordagem atual para escrever lógica Redux.

Ela é uma nova API com muitos utilitários que simplificam o uso desse gerenciador de estado. Suas principais vantagens são a redução da quantidade de código e uma curva de aprendizagem mais amigável.

Quando o Redux foi criado, a pessoa desenvolvedora tinha que criar actions, reducers e configurar manualmente a store, com funções separadas e muitos detalhes que iam se repetindo e alongando o código.

Com o RTK, todos esses processos foram simplificados com a introdução de funções como o createSlice, que combina a criação de reducers e actions em um mesmo local, e configureStore, responsável por automatizar a configuração da store com suporte embutido a middlewares.

Para mostrar toda a praticidade que o RTK trouxe para o Redux vamos voltar ao exemplo da lista de tarefas. Inicialmente precisamos instalar no terminal essa biblioteca através do comando:

npm install @reduxjs/toolkit react-redux

Agora vamos criar um slice, ou seja, um pedaço do estado, que tem seus reducers, valores e estados iniciais totalmente escopados e separados dos demais.

Para isso, criaremos uma pasta "feature" dentro de "src" e adicionaremos o arquivo "tarefasSlice.js".

import { createSlice } from '@reduxjs/toolkit';

const tarefasSlice = createSlice({
  name: 'tarefas',
  initialState: ['Estudar React', 'Fazer exercício', 'Ler um livro'],
  reducers: {
    adicionarTarefa: (state, action) => {
      state.push(action.payload);  // adiciona nova tarefa à lista
    }
  }
});

export const { adicionarTarefa } = tarefasSlice.actions;
export default tarefasSlice.reducer;

O código acima, define um slice chamado tarefas, que gerencia um estado inicial de tarefas em um array. Com o createSlice, incluímos um reducer chamado adicionarTarefa, que permite adicionar novas tarefas ao estado com state.push(action.payload).

O slice exporta a ação adicionarTarefa e o reducer para integração na store principal do Redux.

Em seguida vamos criar o arquivo "store.js" dentro da pasta "src" usando a função configureStore.

import { configureStore } from '@reduxjs/toolkit';
import tarefasReducer from './features/tarefasSlice';

const store = configureStore({
  reducer: {
    tarefas: tarefasReducer
  }
});

export default store;

Agora vamos envolver os componentes renderizados em "App.jsx" com Provider para que os componentes abaixo dele tenham acesso ao store que armazena o estado global.

import { Provider } from 'react-redux';
import store from './store';
import ListaDeTarefas from './components/ListaDeTarefas';

export default function App() {
  return (
    <Provider store={store}>
      <div>
        <h1>Minha Lista de Tarefas</h1>
        <ListaDeTarefas />
      </div>
    </Provider>
  );
}

O componente "ListaDeTarefas" se mantém da mesma forma como foi escrito com a Context API, sem a necessidade de receber uma propsou ter acesso ao store via algum hook específico.

Por outro lado, precisamos acessar o store nos componentes "Tarefas" e "Formulario" através dos hooks useSelector e useDispatch, responsáveis por ler o estado e enviar ações que alteram o estado. Adicionando os seguintes códigos:

//Componente Tarefas

import { useSelector } from "react-redux";

export default function Tarefas() {
  const tarefas = useSelector((state) => state.tarefas);
//RESTANTE DO CÓDIGO
//Componente Formulario

import { useState } from "react";
import { useDispatch } from "react-redux";
import { adicionarTarefa } from "../../features/tarefasSlice";

export default function Formulario() {
  const [novaTarefa, setNovaTarefa] = useState('');
  const dispatch = useDispatch();

  const submeterTarefa = (e) => {
    e.preventDefault();
    if (novaTarefa.trim()) {
      dispatch(adicionarTarefa(novaTarefa));  // Dispara a ação para adicionar tarefa
      setNovaTarefa('');
    }
  };
//RESTANTE DO CÓDIGO

Agora, seu aplicativo está utilizando o Redux Toolkit para gerenciar a lista de tarefas. O estado das tarefas é centralizado na store, e os componentes "Formulario" e "Tarefas" interagem diretamente com o Redux.

Gif. Personagem senhor Burns do desenho Simpsons batendo os dedos. Com a escrita "Excellent" abaixo.

Zustand

Agora chegou a vez de explorar o Zustand. Esse gerenciador de estado também é baseado na arquitetura Flux, mas sua abordagem é mais descomplicada e minimalista, sem tantas configurações extras e complexidades, como os middlewares do Redux.

Isso faz dele uma boa opção para projetos pequenos e médios, que precisam de uma estrutura ágil e funcional, sem tanta dor de cabeça.

No Redux, você precisa despachar uma action via dispatchers para um reducer que vai examinar o tipo de ação e decidir como o estado deve ser modificado, para só então o estado ser atualizado e a interface reagir a essa mudança.

O Zustand abraça a reatividade ao máximo e permite modificar o estado diretamente, sem toda essa estrutura e sequência de passos.

Com ele, as funções para modificar o estado são criadas diretamente na store e manipulam o estado de forma imediata, sem intermediários, eliminando a necessidade de dispatchers e actions.

Para ficar ainda mais claro essa simplicidade no gerenciamento de estados com Zustand, volto com o exemplo da lista de tarefas.

A princípio vamos instalar essa biblioteca no terminal para garantir que tudo que precisamos esteja disponível para ser aplicado no projeto. Para isso, usamos o comando:

npm install zustand

Assim como no Redux, o Zustand também precisa de um store para armazenar e gerenciar os estados e funções.

Para isso vamos criar o arquivo "useTarefasStore.js" na pasta "src/store". Nele vamos adicionar o seguinte código:

import { create } from "zustand";
const useTarefasStore = create((set) => ({
  tarefas: [
    'Estudar React',
    'Fazer exercício',
    'Ler um livro',
  ],
  adicionarTarefa: (novaTarefa) => set((state) => ({
    tarefas: [...state.tarefas, novaTarefa],
  })),
}));

export default useTarefasStore;

No código acima, temos a definição de uma lista de tarefas e uma função adicionarTarefa bem semelhante ao store que criamos com Redux.

Contudo, essa store está mais enxuta, sem precisar se actions dispatchers e reducers. Tudo isso graças a função set que modifica o estado da store e dispara as atualizações de forma reativa, fazendo com que os componentes do React que dependem desse estado sejam re-renderizados automaticamente quando ele muda.

Agora que temos um estado compartilhado, podemos remover a passagem por propsdo "App.jsx".

import ListaDeTarefas from './components/ListaDeTarefas';

export default function App() {

  return (
    <div>
      <h1>Minha Lista de Tarefas</h1>
      <ListaDeTarefas/>
    </div>
  );
}

É interessante notar que no código acima, não foi necessário encapsular os componentes com nenhum Provider nem nada do tipo.

Isso acontece, pois o Zustand gerencia o estado fora da árvore de componentes, diretamente no escopo global do JavaScript, ou seja, o estado é armazenado de forma independente da renderização do React.

Da mesma forma, podemos manter o mesmo código que utilizamos lá na Context API, pois agora não estamos usando as informações do store via props.

Já no caso dos componentes "Tarefas" e "Formulario", podemos usar o hook personalizado useTarefasStore, importando as informações que queremos usar para ambas as situações:

//Componente Tarefas
import useTarefasStore from "../../store/useTarefasStore";

export default function Tarefas() {
  const {tarefas}=useTarefasStore();
//RESTANTE DO CÓDIGO
//Componente Formulario
import { useState } from "react";
import useTarefasStore from "../../store/useTarefasStore";

export default function Formulario() {
  const [novaTarefa, setNovaTarefa] = useState('');
  const { adicionarTarefa } = useTarefasStore();

  const submeterTarefa = (e) => {
    e.preventDefault();
    if (novaTarefa.trim()) {
      adicionarTarefa(novaTarefa);
      setNovaTarefa('');
    }
  }
//RESTANTE DO CÓDIGO

Prontinho! Alteramos o projeto para usar Zustand no gerenciamento de estados.

Essa biblioteca além de ter um mascote super bonitinho, é uma opção interessante se você busca por uma maneira simples e direta de gerenciar estados, recursos avançados com atualizações de estado sem re-renderizações (transient updates) para aumentar o desempenho da aplicação e controle sobre os estados fora do ambiente React.

Recoil

Agora chegou o momento de conhecermos o Recoil. Diferente do Redux e do Zustand, o Recoil possui uma arquitetura baseada em átomos e seletores, o que permite que o estado seja gerenciado de maneira reativa e modular.

Além disso, essa biblioteca pode ser utilizada de forma similar aos famosos hooks do React como o useStatepor exemplo.

Mas para conseguir aplicar da melhor maneira o Recoil no seu projeto, precisamos conhecer muito bem sua estrutura. Por isso, vem comigo explorar um pouco mais do que são os átomos e seletores que compõem essa biblioteca.

Átomos

Os átomos são unidades de estado e podem ser usados no lugar do componente local do React. Se houver uma mudança no átomo, elas serão propagadas para todos os componentes que dependem desse estado, garantindo consistência e previsibilidade no comportamento da aplicação.

Para criar um átomo, precisamos criar uma pasta chamada state e um arquivo chamado atom.js dentro da pasta "src".

Por exemplo, se eu quiser criar um átomo para gerenciar o estado de uma lista de tarefas eu posso escrever o código da seguinte maneira:

const listaDeTarefasState = atom({
  key: 'ListaDeTarefas',
  default: []
});

No código acima, os átomos criados precisam receber uma chave única para identificação e depuração e um valor padrão.

Para ter acesso a um átomo usamos o hook useRecoilState ou useRecoilValue :

const listaDeTarefas = useRecoilValue(listaDeTarefasState)

Para atualizar o valor da lista, podemos usar o hook useSetRecoilState :

const setListaDeTarefas = useSetRecoilState(listaDeTarefasState)

Seletores

Os seletores são responsáveis por criar valores baseados nos átomos ou até em outros seletores. Em vez de armazenar todos os dados diretamente em átomos (que são os estados primários no Recoil), o seletor pega o valor atual de um átomo (ou de vários átomos) e calcula algo a partir dele, permitindo transformar e utilizar esse dado conforme necessário.

Dessa forma, seguimos o princípio de "armazenar o mínimo estado possível" e derivar outros valores a partir dele, promovendo um código mais eficiente e menos redundante.

Por exemplo, partindo do átomo listaDeTarefasState, podemos aplicar um filtro que vai retornar os valores da lista com base no status das tarefas (completo ou não completo).

const listaDeTarefasFiltradaState = selector({
  key: 'ListaDeTarefasFiltradat',
  get: ({get}) => {
    const filtro = get(listaDeTarefasState);
    const lista = get(listaDeTarefasState);
    switch (filtro) {
      case 'Completo':
        return lista.filter((item) => item.completo);
      case 'Nao Completo':
        return lista.filter((item) => !item.compelto);
      default:
        return list;
    }
  }
})

No código acima, temos um seletor que possui uma chave única e a propriedade get, responsável por derivar um novo valor a partir do estado atual de átomos ou outros seletores.

Neste caso, a propriedade get usa a função get para capturar a lista completa de tarefas e o valor atual do filtro e faz um switch/case para realizar uma filtragem condicional.

Já conhecemos melhor os átomos e seletores do Recoil, então vamos usar mais uma vez o projeto da lista de tarefas para ver como essa biblioteca pode ser implementada?

Inicialmente precisamos instalar o Recoil no terminal com o comando:

npm install recoil

Agora vamos criar o arquivo "atom.jsx" dentro da pasta "src/state" e adicionar o código abaixo:

import { atom } from "recoil";

export const tarefaState = atom({
  key: 'tarefaState',
  default: [
    'Estudar React',
    'Fazer exercício',
    'Ler um livro'
  ]
})

Em seguida, precisamos envolver os componentes que estão sendo renderizados no "App.jsx" com o <RecoilRoot></RecoilRoot> para que todos possam ter acesso ao Recoil.

//RESTANTE DO CÓDIGO
    <RecoilRoot>
      <div>
        <h1>Minha Lista de Tarefas</h1>
        <ListaDeTarefas />
      </div>
    </RecoilRoot>
//RESTANTE DO CÓDIGO

O código do componente "ListaDeTarefas" segue idêntico ao que foi usado com a Context API, Redux e Zustand, enquanto os componentes "Tarefa" e "Formulario" usam os hooks useRecoilValue e useSetRecoilState para acessar as informações e alterar os dados da lista respectivamente.

//Componente Tarefa

import { useRecoilValue } from "recoil";
import { tarefaState } from "../../state/atom";

export default function Tarefas() {
    const tarefas = useRecoilValue(tarefaState);
//RESTANTE DO CÓDIGO
//Componente Formulario

import { useState } from "react";
import { useSetRecoilState } from 'recoil';
import { tarefaState } from "../../state/atom";

export default function Formulario() {
  const [novaTarefa, setNovaTarefa] = useState('');
  const setTarefas = useSetRecoilState(tarefaState);

  const submeterTarefa = (e) => {
    e.preventDefault();
    if (novaTarefa.trim()) {
      setTarefas(tarefasAntigas => [...tarefasAntigas, novaTarefa]);
      setNovaTarefa('');
    }
  }
//RESTANTE DO CÓDIGO

E com isso já são quatro possibilidades de gerenciar o estado da nossa lista de tarefas.

O Recoil é uma boa opção para projetos de médio porte que precisam de atualizações reativas de estado e cenários que envolvem a dependência entre estados, sem a complexidade do soluções como o Redux.

Além disso, essa biblioteca oferece suporte a funcionalidades avançadas como transações atômicas e estados derivados complexos.

MobX

Último, mas não menos importante, temos o MobX, que busca um gerenciamento de estado escalável através da reatividade transparente (TFRP, que em português significa, Programação Reativa Funcional Transparente).

Isso quer dizer que as atualizações acontecem automaticamente sempre que um componente muda. E como isso é possível? Graças ao observable (observável) e ao observer (observador).

Enquanto o observable permite que dados sejam monitorados por outros componentes ou funções, o observer transforma componentes React em "observadores" de estados observáveis, e, quando alguma coisa mudar, esse componente marcado como observer vai ser re-renderizado automaticamente.

Assim como o Redux, o MobX possui um fluxo de unidirecional para atualizar os dados na interface. Sendo assim, ao executar uma ação, ela modifica o estado e consequentemente atualiza as visualizações que forem afetadas. Caso queira saber mais sobre isso, você pode consultar a documentação oficial.

Além disso, no MobX é possível usar decoratos (decoradores), funções especiais do JavaScript que permitem escrever funcionalidades de forma modular e declarativa. São normalmente marcadas pelo sinal de "@" antes da sintaxe da anotação.

Por exemplo se quisermos cria um decorator para um observable, podemos escrever da seguinte maneira @observable.

Mas para usar o decorator junto com o MobX, pode ser necessário fazer algumas configurações do Babel, uma ferramenta que ajuda a converter as funcionalidades do EcmaScript 6 (ES6) em uma versão compatível com os navegadores mais antigos que ainda não suportam essas funcionalidades.

Para isso, precisamos instalar no terminal dentro da pasta do projeto, o seguinte @babel/plugin-proposal-decorators através do comando:

npm install --save-dev @babel/plugin-proposal-decorators

Em seguida, podemos atualizar o arquivo "vite.config.js" para utilizar o plugin do proposal-decorators.

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ["@babel/plugin-proposal-decorators", { version: "2023-11" }],
        ],
      },
    }),
  ],
});

Além disso, para garantir que o ESLint entenda corretamente o código e evite erros de análise ao lidar com as funcionalidades mais recentes do JavaScript, abra o arquivo "eslint.config.js" e dentro do languageOption, logo acima do parserOption adicione a seguinte linha de código:

parser: "babel-eslint",
Tela do Visual Studio Code com o arquivo "eslint.config.js" com o código informando o parser em destaque.

Caso queira mais informações, você pode consultar a documentação do MobX sobre decorators.

Além dos observables e observer ainda temos algumas outras funções que ajudam no gerenciamento de estado, e irei apresentar elas aqui:

  • computed values são usados quando você precisa da combinação de outros dados. Por exemplo, se eu quiser saber o total de tarefas que eu tenho dentro de uma lista;
  • autorun, reaction ou when são usados para qualquer código reagir aos observables. Um exemplo é exibir uma mensagem no console do navegador sempre que um título mudar;
  • actions são usadas para organizar o código e definir quem pode modificar os observables. Por exemplo, definir qual componente pode adicionar um item à uma lista de compras.

Com base em todos esses conhecimentos, vamos escrever uma última vez o código da nossa lista de tarefas?

Como de praxe, vamos começar pela instalação do MobX no terminal através do comando:

npm install mobx mobx-react

A partir daqui, irei usar decorators para me ajudar a escrever os códigos com MobX, então caso queira me acompanhar nessa empreitada, recomendo que você faça as configurações necessárias que mencionei mais acima. Para criar um estado global, precisamos criar um arquivo "tarefasStore.js" dentro da pasta "src/store", com o seguinte código:

import { observable, action } from "mobx";

class TarefasStore {
  @observable accessor tarefas = ['Estudar React', 'Fazer exercício', 'Ler um livro'];

  @action adicionarTarefa = (novaTarefa) => {
    this.tarefas.push(novaTarefa);
  };
}

const tarefasStore = new TarefasStore();
export default tarefasStore;

No código acima, criamos a classe TarefasStore. Nela a lista de tarefas é marcada como um observável através do código @observable accessor tarefas enquanto a função adicionarTarefas é marcada como uma action.

A classe é exportada como uma instância tarefasStore para ser usada em outros componentes da aplicação.

Em seguida, precisamos englobar os componentes que estão sendo renderizados no "App.jsx" com um <Provider></Provider> para que todos os outros componentes abaixo na hierarquia tenham acesso às informações contidas no estado global.

//RESTANTE DO CÓDIGO
    <Provider tarefasStore={tarefasStore}>
      <div>
        <h1>Minha Lista de Tarefas</h1>
        <ListaDeTarefas />
      </div>
    </Provider>
//RESTANTE DO CÓDIGO

Assim como nos outros gerenciadores de estado, aqui não seria diferente. O componente "ListaDeTarefas" se mantém da mesma forma que foi apresentado ao usar a Context API e nos componentes "Tarefas" e "Formulario" precisamos importar o estado global.

//Componente Tarefa

//RESTANTE DO CÓDIGO
    <ul>
      {tarefasStore.tarefas.map((tarefa, index) => (
        <li key={index}>{tarefa}</li>
      ))}
    </ul>
  );
}

export default observer(Tarefas);
//Componente Formulario

//RESTANTE DO CÓDIGO
  const submeterTarefa = (e) => {
    e.preventDefault();
    if (novaTarefa.trim()) {
      tarefasStore.adicionarTarefa(novaTarefa);
      setNovaTarefa('');
    }
  };
//RESTANTE DO CÓDIGO

E com essas mudanças, conseguimos ajustar o projeto para usar o MobX no gerenciamento de estados da lista de tarefas.

Ufa! Foram vários gerenciadores de estado que conhecemos ao longo deste artigo. Mas, o próprio título dessa seção nos traz uma pergunta que você deve estar se fazendo neste exato momento: e agora, qual eu devo escolher para usar no meu projeto?

E a resposta clássica para essa pergunta é: depende! Nenhuma dessas bibliotecas é uma solução definitiva para resolver todos os problemas que podem surgir quando estamos trabalhando com estados em aplicações React. Além disso, com o tempo, outras opções podem surgir e roubar a cena, com soluções diferentes.

Mas calma, não vou filosofar por aqui e sair de fininho. Vamos comparar alguns aspectos dessas ferramentas que podem ser decisivos no momento da sua escolha.

Ao pensar sob o ponto de vista da experiência de desenvolvimento, o Recoil e o Redux, possuem documentações bem completas que podem facilitar a resolução de problemas.

O MobX possui uma comunidade engajada e uma abordagem reativa que simplifica o gerenciamento de estado. Já o Zustand, é mais leve e pode ser uma abordagem interessante em situações que requerem alto desempenho.

Se você algum dia já trabalhou com Redux, mas quer uma opção mais simplificada, o Zustand pode ser uma boa opção.

Mas se você está iniciando no gerenciamento de estado e quer algo mais próximo da familiar estrutura dos hooks, pode preferir o Recoil com seus átomos e seletores. Mas se prefere uma abordagem reativa, recomendo o MobX.

Em termos de tamanho da aplicação, o Zustand é indicado para projetos de pequeno e médio porte, o Recoil é recomendado para aplicativos de médio e grande porte, o MobX é bem versátil e se adapta a qualquer tamanho e o Redux é melhor para projetos maiores e mais complexos.

Já pensando em recursos avançados, é importante saber que o Recoil e o MobX oferecem funcionalidades como estados derivados e observáveis, respectivamente.

O Redux, se destaca pela presença dos middlewares e ferramentas de depuração. Enquanto o Zustand, não apresenta tantos recursos avançados por ser mais leve.

Bom, com certeza a adoção da comunidade pode ser um fator determinante para algumas pessoas optarem por uma biblioteca ou outra.

Neste caso, todos eles são bem aceitos pela comunidade, mesmo o Zustand que está chegando agora e já tem uma grande quantidade de pessoas usuárias.

É isso! Não tem como dizer que uma biblioteca é melhor que a outra, tudo depende da sua necessidade, adaptabilidade, estrutura do projeto e outros fatores que vão te fazer escolher uma ou outra.

Contudo, espero que as discussões que fizemos até aqui te ajudem a escolher a melhor opção para gerenciar os estados da sua aplicação.

Conclusão

Exploramos muitas maneiras de gerenciar estado em projetos React, desde a solução nativa, a Context API, até bibliotecas externas como o Zustand, Redux, Recoil e MobX, cada uma com sua estrutura, sintaxe e recursos. Mesmo com todas as diferenças, conseguimos explorar as potencialidades de cada uma delas para criar um estado global no nosso projeto de lista de tarefas.

Além disso, entendemos melhor o problema que essas bibliotecas resolvem e conversamos sobre aspectos importantes na hora de decidir qual a melhor opção para sua aplicação, destacando o tamanho da aplicação, abordagens, recursos disponíveis, adoção da comunidade, velocidade e quantidade de atualizações.

Principalmente para os três últimos fatores (velocidade, adoção da comunidade e quantidade de atualizações), podemos usar a ferramenta npmtrends que pode ajudar na tomada de decisão, pois permite visualizar esses e outros dados comparando diferentes tecnologias.

Agora te convido a buscar um projeto que você já tenha feito ou esteja desenvolvendo que possua muitas props sendo passadas entre os componentes e experimente utilizar essas bibliotecas para gerenciar os estados da sua aplicação.

Pode ser um exercício super interessante para descobrir mais sobre o uso de cada uma. Caso queira mergulhar ainda mais fundo no React e nos gerenciamentos de estado, você pode consultar os links abaixo.

Até a próxima!

RODRIGO SILVA HARDER
RODRIGO SILVA HARDER

Graduado em Design Gráfico, Técnico em Química e Licenciatura em química, Rodrigo é apaixonado por ensinar e aprender e busca se aventurar em diferentes linguagens de programação. Faço parte da escola semente e atuo como monitor no time de Fórum Ops.

Veja outros artigos sobre Front-end