Redux é uma biblioteca de gerenciamento de estado para aplicações JavaScript e TypeScript. Ela resolve um problema específico: à medida que as aplicações crescem, diferentes partes do sistema precisam compartilhar e modificar os mesmos dados, e fazer isso por meio do estado local de componentes ou de props profundamente aninhadas se torna inviável. O Redux centraliza esses dados em um único store e impõe um ciclo de atualização estrito.

Por Que o Redux Importa

Sem uma solução centralizada de estado, você acaba com dados espalhados pelos componentes, emissores de eventos disparando em ordem imprevisível e cadeias de props com três ou quatro níveis de profundidade. O Redux resolve isso tornando o fluxo de dados explícito. As mudanças de estado só ocorrem por meio de actions despachadas, e essas mudanças são tratadas por funções puras chamadas reducers.

O resultado é que as transições de estado são rastreáveis, os bugs são reproduzíveis, e adicionar novos recursos não exige desembaraçar a lógica de estado existente.

Princípios Fundamentais do Redux

O Redux é construído em torno de três princípios:

  1. Fonte Única da Verdade: Todo o estado da aplicação vive em um único objeto JavaScript, o store. Um único lugar para consultar ao depurar.

  2. O Estado é Somente Leitura: A única forma de alterar o estado é despachar uma action, um objeto simples que descreve o que aconteceu. Isso impede que atualizações apareçam em lugares inesperados.

  3. As Mudanças São Feitas com Funções Puras: Os reducers recebem o estado atual e uma action, e retornam o próximo estado sem mutar o original. As mesmas entradas sempre produzem a mesma saída.

Quando Usar Redux?

O Redux é uma boa escolha quando múltiplos componentes precisam acessar os mesmos dados, quando você precisa de depuração com viagem no tempo ou log de auditoria de mudanças de estado, ou quando a aplicação é complexa o suficiente para que o estado imprevisível se torne um problema real de manutenção.

Para aplicações pequenas, o Redux costuma ser excessivo. O estado local de componentes e o React Context cobrem a maioria dos casos nessa escala. O custo de boilerplate do Redux se justifica quando a lógica de estado é genuinamente complexa.

Primeiros Passos com Redux

1. Instale o Redux e as Bibliotecas de Suporte

npm install redux react-redux

Com TypeScript:

npm install --save-dev @types/redux @types/react-redux

2. Crie Suas Actions

As actions são objetos simples com um campo type e dados opcionais:

// actions.ts
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export function increment() {
  return { type: INCREMENT };
}

export function decrement() {
  return { type: DECREMENT };
}

3. Defina um Reducer

O reducer é uma função pura que retorna um novo objeto de estado sem mutar o original:

// reducer.ts
import { INCREMENT, DECREMENT } from "./actions";

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export function counterReducer(
  state: CounterState = initialState,
  action: { type: string }
): CounterState {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

4. Crie o Store

// store.ts
import { createStore } from "redux";
import { counterReducer } from "./reducer";

export const store = createStore(counterReducer);

5. Integre com o React

O componente Provider disponibiliza o store para qualquer componente aninhado:

// index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";

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

Um Exemplo Completo

Aqui está um exemplo funcional em TypeScript cobrindo actions, reducer, store e um componente React conectado.

Passo 1: Estrutura do Projeto

src
├── components
│   └── Counter.tsx
├── redux
│   ├── actions.ts
│   ├── reducer.ts
│   └── store.ts
└── App.tsx

Passo 2: Crie os Tipos de Action e os Action Creators

// src/redux/actions.ts
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

interface IncrementAction {
  type: typeof INCREMENT;
}

interface DecrementAction {
  type: typeof DECREMENT;
}

export type CounterActionTypes = IncrementAction | DecrementAction;

export function increment(): CounterActionTypes {
  return { type: INCREMENT };
}

export function decrement(): CounterActionTypes {
  return { type: DECREMENT };
}

As interfaces TypeScript para cada tipo de action garantem que o código permaneça com tipagem segura em todo o reducer e em qualquer componente que despache actions.

Passo 3: Defina o Reducer

// src/redux/reducer.ts
import { INCREMENT, DECREMENT, CounterActionTypes } from "./actions";

export interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export function counterReducer(
  state = initialState,
  action: CounterActionTypes
): CounterState {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

O counterReducer impõe um único ponto de modificação de estado: apenas essa função altera o estado, e o faz retornando um novo objeto.

Passo 4: Crie o Store

// src/redux/store.ts
import { createStore } from "redux";
import { counterReducer } from "./reducer";

export const store = createStore(counterReducer);

Para aplicações mais complexas, applyMiddleware adiciona middlewares como Redux Thunk ou Redux Saga para tratar operações assíncronas.

Passo 5: Construa um Componente Counter

// src/components/Counter.tsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "../redux/actions";
import { CounterState } from "../redux/reducer";

export function Counter() {
  const dispatch = useDispatch();
  const value = useSelector((state: CounterState) => state.value);

  return (
    <div>
      <h1>Redux Counter</h1>
      <p>Value: {value}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

useSelector recupera o valor atual do contador do store. useDispatch envia as actions.

Passo 6: Conecte Tudo no App

// src/App.tsx
import React from "react";
import { Counter } from "./components/Counter";

function App() {
  return <Counter />;
}

export default App;

Executando a Aplicação

npm install
npm start

Abra o navegador e você verá o contador. Os botões de incrementar e decrementar atualizam o estado via Redux, com cada mudança rastreada no store.

Boas Práticas para Escalar com Redux

  1. Use Middleware para Efeitos Colaterais: Para operações assíncronas como chamadas a APIs, adicione redux-thunk ou redux-saga. Eles fornecem padrões estruturados para efeitos colaterais em vez de despachar actions de dentro dos componentes.

  2. Normalize o Estado para Entidades Complexas: Se você estiver armazenando arrays de itens ou dados profundamente aninhados, normalize-os. Bibliotecas como normalizr achatam estruturas complexas, tornando atualizações e buscas mais eficientes.

  3. Mantenha os Reducers Focados: Cada reducer deve tratar uma fatia do estado. Um reducer que gerencia usuários, pedidos e notificações está fazendo trabalho demais.

  4. Combine Reducers: Use combineReducers conforme a aplicação cresce. Cada reducer gerencia sua fatia, e combineReducers monta a árvore de estado completa.

  5. Aproveite o DevTools: A extensão Redux DevTools fornece depuração com viagem no tempo e visibilidade total sobre cada mudança de estado. Vale a pena configurá-la desde o primeiro dia.

  6. Use TypeScript de Forma Eficaz: Actions, reducers e selectors tipados previnem uma classe inteira de erros em tempo de execução. O investimento inicial em tipagem se paga rapidamente em lógicas de estado complexas.

Armadilhas Comuns e Como Evitá-las

  1. Usar Redux em Excesso: Nem todo estado pertence ao Redux. Flags específicas de UI, como o estado aberto/fechado de um modal, estados de hover ou valores de formulários locais, geralmente ficam melhor no estado do componente. O Redux é para dados que múltiplos componentes compartilham.

  2. Mutar o Estado nos Reducers: A mutação direta causa bugs sutis. Sempre retorne um novo objeto de estado usando operadores spread. Evite state.value++ ou qualquer reatribuição direta.

  3. Ignorar a Performance: Re-renders excessivos degradam a performance em aplicações grandes. Selectors memoizados de bibliotecas como Reselect evitam re-renders desnecessários ao computar dados derivados de forma eficiente.

  4. Negligenciar a Organização do Código: À medida que o código cresce, mantenha actions, reducers e tipos claramente separados. Uma organização confusa desacelera novos colaboradores e dificulta a depuração.

Além do Básico

Com os fundamentos sólidos, algumas ferramentas estendem as capacidades do Redux:

  • Redux Observable gerencia fluxos assíncronos complexos usando observables do RxJS.
  • Reselect cria selectors composáveis e memoizados para dados derivados.
  • Redux Toolkit reduz significativamente o boilerplate e é hoje a abordagem recomendada para novos projetos Redux.
  • Tipos utilitários do TypeScript como ReturnType e Extract simplificam as definições de tipo em configurações complexas de actions e reducers.

Considerações Finais sobre Redux

O Redux continua sendo uma escolha confiável para gerenciamento de estado previsível em grandes aplicações JavaScript e TypeScript. A verbosidade é real, e o custo do boilerplate é real, mas o retorno também é: as mudanças de estado são rastreáveis, os bugs são reproduzíveis, e o código permanece gerenciável à medida que cresce.

Seja iniciando um novo projeto ou refatorando um existente, a estrutura do Redux recompensa a disciplina. O DevTools, por si só, economizou mais horas de depuração do que o custo de configuração na maioria dos projetos em que trabalhei.