Por que eu não uso bibliotecas de gerenciamento de estado no React

Por que eu não uso bibliotecas de gerenciamento de estado no React

Original source in English by Fabrizio Beccaceci
Why i no longer use a React state management library
https://medium.com/@ipla/why-i-no-longer-use-a-react-state-management-library-7bdffae54600

Quando comecei a aprender React anos atrás, havia duas coisas que você precisava saber para se considerar um desenvolvedor React: TypeScript e Redux.

Hoje em dia, as coisas mudaram. Se você está aprendendo React agora e se depara com o conceito de gerenciamento de estado, vai se sentir sobrecarregado com uma infinidade de bibliotecas: Redux, Redux Toolkit, MobX, Jotai, Zustand e a lista continua.

Mas antes de nos aprofundarmos, vamos responder à pergunta básica:

O que é uma Biblioteca de Gerenciamento de Estado Global?

Mesmo se você for relativamente novo no React, provavelmente sabe o que é o termo estado. Você cria um com o hook useState, e quando você o atualiza, seu aplicativo renderiza novamente para refletir as mudanças.

No entanto, as coisas ficam complicadas quando você precisa compartilhar alguma informação entre várias partes não conectadas do seu aplicativo React.

Isso é o que frequentemente chamamos de estado global, e é o problema que as bibliotecas de gerenciamento de estado tentam resolver: como tornar o estado compartilhado acessível a qualquer componente.

Vamos ver como o Redux Toolkit lida com isso.

Como o Redux Toolkit Resolve o Estado Global

Primeiro, você cria uma store:

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

export const store = configureStore({
  reducer: {},
});

// Inferindo os tipos de RootState e AppDispatch usando a store
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Este é o local central onde seu estado global irá existir. Em seguida, você envolve seu aplicativo com um provider para tornar a store acessível a todos os componentes:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './app/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Agora, você precisa definir os slices, que são partes individuais do seu estado global. Por exemplo:

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

interface CounterState {
  value: number;
}
const initialState: CounterState = { value: 0 };
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => { state.value += 1; },
    decrement: (state) => { state.value -= 1; },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

Por fim, você conecta este slice à store:

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

export const store = configureStore({
  reducer: { counter: counterReducer },
});

Neste ponto, você pode usar seu estado global nos componentes da seguinte forma:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

export function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <span>{count}</span>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

Embora o Redux Toolkit simplifique significativamente o Redux, ainda tem muito código boilerplate para o que geralmente é uma necessidade simples. E nem sequer falamos sobre ações assíncronas como requisições ao servidor!

Além disso, muitos iniciantes caem na armadilha de colocar todo o seu estado no Redux, levando a stores globais infladas que são mais difíceis de gerenciar.

As Duas Faces do Estado Global

Quando você para pra pensar, a maioria do "estado global" se enquadra em duas categorias:

  1. Estado do Servidor: Dados obtidos de um servidor, como uma lista de clientes em um aplicativo CRM.

  2. Estado da UI Compartilhado: Pequenos pedaços de dados necessários em vários lugares, como o usuário atualmente logado.

Vamos abordar cada um deles separadamente.

A Abordagem Enxuta para Estado Global

Com ferramentas modernas, você pode lidar com estado global sem uma biblioteca dedicada de gerenciamento de estado:

Para Estado do Servidor use TanStack Query

TanStack Query (anteriormente React Query) é feito especificamente para gerenciar estado do servidor. Ele lida com cache, recarregamento, dados obsoletos e muito mais, tudo pronto para uso, e você definitivamente deveria usar isso porque você realmente não vai querer reimplementar tudo isso do zero. Por exemplo, buscar uma lista de clientes é tão simples quanto:

import { useQuery } from '@tanstack/react-query';

function Customers() {
  const { data, error, isLoading } = useQuery(['customers'], fetchCustomers);
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <ul>{data.map((customer) => <li key={customer.id}>{customer.name}</li>)}</ul>;
}

O TanStack Query elimina a necessidade de gerenciar manualmente o estado do servidor e, como o estado que você busca é armazenado em cache, você pode usá-lo exatamente da mesma maneira em todos os componentes e ele será buscado apenas uma vez.

Para Estado Compartilhado use Observables

Para estados menores e específicos do aplicativo, como o usuário logado, eu uso uma implementação leve do padrão Observable:

export class Observable<T> {
  private _value: T;
  private subscribers = new Set<(value: T) => void>();

  constructor(initialValue: T) {
      this._value = initialValue;
    }
    get value() {
      return this._value;
    }
    set value(newValue: T) {
      this._value = newValue;
      this.notify();
    }
    subscribe(callback: (value: T) => void) {
      this.subscribers.add(callback);
      callback(this._value);
      return { unsubscribe: () => this.subscribers.delete(callback) };
    }
    private notify() {
      this.subscribers.forEach((callback) => callback(this._value));
    }
  }
}

Com isso, você pode conectar Observables ao React usando useSyncExternalStore:

import { useSyncExternalStore } from 'react';

export function useObservable<T>(observable: Observable<T>) {
  return useSyncExternalStore(
    (callback) => observable.subscribe(callback).unsubscribe,
    () => observable.value
  );
}

Agora, você pode criar e usar observables desta forma:

const loggedUser = new Observable<User>(user);

loggedUser.value // Para acessar o valor dentro do Observable fora do React

loggedUser.value = someOtherUser // Para definir o valor, tanto fora quanto dentro do React

Dentro de um componente React:

const user = useObservable(loggedUser);

Por que não usar React Context?

Embora o React Context funcione em alguns casos, ele tem desvantagens:

  1. Escopo Limitado: Você não pode acessar o Context fora dos componentes React.

  2. Problemas de Performance: Context dispara uma nova renderização para todos os componentes abaixo dele, até mesmo para os componentes que não usem o valor atualizado.

Usando Observables evitamos ambos problemas!

Conclusão

Gerenciamento de estado global não precisa ser complicado. Combinando TanStack Query e um padrão Observable leve, você pode simplificar seu aplicativo evitando as armadilhas das bibliotecas tradicionais de gerenciamento de estado.

Entre em contato comigo se quiser discutir sobre React, React Native, Nextjs

Bom desenvolvimento! 🎉