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:
-
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.
-
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.
-
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
-
Use Middleware para Efeitos Colaterais: Para operações assíncronas como chamadas a APIs, adicione
redux-thunkouredux-saga. Eles fornecem padrões estruturados para efeitos colaterais em vez de despachar actions de dentro dos componentes. -
Normalize o Estado para Entidades Complexas: Se você estiver armazenando arrays de itens ou dados profundamente aninhados, normalize-os. Bibliotecas como
normalizrachatam estruturas complexas, tornando atualizações e buscas mais eficientes. -
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.
-
Combine Reducers: Use
combineReducersconforme a aplicação cresce. Cada reducer gerencia sua fatia, ecombineReducersmonta a árvore de estado completa. -
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.
-
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
-
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.
-
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. -
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.
-
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
ReturnTypeeExtractsimplificam 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.