Aplicações React tendem a se tornar complicadas rapidamente. Props começam a fluir em todas as direções, componentes que precisam dos mesmos dados ficam distantes na árvore, e depurar mudanças de estado vira um jogo de adivinhação. O Redux resolve isso colocando todo o estado da aplicação em um único lugar e impondo um modelo de atualização estrito e previsível.
O que é Redux?
Redux é um contêiner de estado previsível para aplicações JavaScript. Ele centraliza o estado da sua aplicação em um único store, tornando as mudanças de estado rastreáveis e fáceis de reproduzir. O Redux funciona com o React via bindings do react-redux, mas não é exclusivo do React.
Conceitos Fundamentais do Redux
Store
O store é a fonte única de verdade. Ele mantém toda a árvore de estado da sua aplicação. Um único store significa um único lugar para consultar quando algo dá errado.
Actions
Actions são objetos JavaScript simples que descrevem uma intenção de modificar o estado. Toda action deve ter uma propriedade type. Qualquer outra coisa que você adicionar fica a seu critério.
const addAction = {
type: 'ADD_TODO',
payload: {
text: 'Learn Redux',
},
};
Reducers
Reducers são funções puras. Eles recebem o estado anterior e uma action, e retornam um novo estado. Eles nunca mutam o estado existente.
function todoReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
default:
return state;
}
}
Árvore de Estado
A árvore de estado é um objeto JavaScript simples que representa tudo o que sua aplicação conhece. Como os reducers sempre retornam novos objetos em vez de modificar os existentes, as mudanças de estado são previsíveis e passíveis de diff.
Dispatch
Dispatch é o método no store que envia uma action. O reducer do store a processa e produz o próximo estado.
store.dispatch(addAction);
Configurando o Redux em uma Aplicação React
Instalando as Dependências
Instale o Redux e o React Redux:
npm install redux react-redux
Para usuários de TypeScript, instale também os tipos:
npm install --save-dev @types/redux @types/react-redux
Criando o Store
Crie um arquivo store.js e configure o store:
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
Fornecendo o Store para a Aplicação
Envolva seu componente raiz com o Provider do react-redux. Todos os componentes abaixo dele poderão acessar o store.
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')
);
Trabalhando com Actions e Action Creators
Defina os tipos de actions:
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
Crie action creators:
export const addTodo = (text) => ({
type: ADD_TODO,
payload: { text },
});
export const removeTodo = (id) => ({
type: REMOVE_TODO,
payload: { id },
});
Entendendo os Reducers
Crie reducers para lidar com mudanças de estado:
import { ADD_TODO, REMOVE_TODO } from '../actions';
const initialState = {
todos: [],
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
const newTodo = {
id: Date.now(),
text: action.payload.text,
};
return { ...state, todos: [...state.todos, newTodo] };
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
default:
return state;
}
}
export default todoReducer;
Conectando Componentes React ao Redux Store
Usando os Hooks useSelector e useDispatch
useSelector extrai dados do store. useDispatch fornece a função dispatch. Esses dois hooks são tudo o que você precisa na maioria dos componentes.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, removeTodo } from './actions';
function TodoList() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
const handleAddTodo = (text) => {
dispatch(addTodo(text));
};
const handleRemoveTodo = (id) => {
dispatch(removeTodo(id));
};
// Lógica de renderização do componente...
}
Middleware e Actions Assíncronas
O middleware do Redux fica entre o despacho de uma action e o processamento pelo reducer. É aqui que as operações assíncronas pertencem.
Usando Redux Thunk
O Redux Thunk permite que action creators retornem uma função em vez de um objeto de action. Essa função recebe dispatch e pode realizar trabalho assíncrono antes de despachar.
Instale o Redux Thunk:
npm install redux-thunk
Configure o store com middleware:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
Crie um action creator assíncrono:
export const fetchTodos = () => {
return async (dispatch) => {
dispatch({ type: 'FETCH_TODOS_REQUEST' });
try {
const response = await fetch('/api/todos');
const data = await response.json();
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_TODOS_FAILURE', payload: error });
}
};
};
Exemplo: Construindo uma Aplicação de Tarefas com Redux
Configuração do Projeto
Inicialize uma nova aplicação React:
npx create-react-app redux-todo-app
cd redux-todo-app
npm install redux react-redux redux-thunk
Definindo Actions e Action Creators
Em src/actions/index.js:
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';
export const addTodo = (text) => ({
type: ADD_TODO,
payload: { text },
});
export const removeTodo = (id) => ({
type: REMOVE_TODO,
payload: { id },
});
export const fetchTodos = () => {
return async (dispatch) => {
dispatch({ type: FETCH_TODOS_REQUEST });
try {
const response = await fetch('/api/todos');
const data = await response.json();
dispatch({ type: FETCH_TODOS_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: FETCH_TODOS_FAILURE, payload: error });
}
};
};
Criando Reducers
Em src/reducers/todoReducer.js:
import {
ADD_TODO,
REMOVE_TODO,
FETCH_TODOS_REQUEST,
FETCH_TODOS_SUCCESS,
FETCH_TODOS_FAILURE,
} from '../actions';
const initialState = {
todos: [],
loading: false,
error: null,
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TODOS_REQUEST:
return { ...state, loading: true };
case FETCH_TODOS_SUCCESS:
return { ...state, loading: false, todos: action.payload };
case FETCH_TODOS_FAILURE:
return { ...state, loading: false, error: action.payload };
case ADD_TODO:
const newTodo = {
id: Date.now(),
text: action.payload.text,
};
return { ...state, todos: [...state.todos, newTodo] };
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
default:
return state;
}
}
export default todoReducer;
Em src/reducers/index.js:
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
const rootReducer = combineReducers({
todoState: todoReducer,
});
export default rootReducer;
Configurando o Store
Em src/store.js:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
Conectando Componentes
Em src/App.js:
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, removeTodo, fetchTodos } from './actions';
function App() {
const [input, setInput] = useState('');
const { todos, loading, error } = useSelector((state) => state.todoState);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
const handleAddTodo = () => {
if (input.trim() !== '') {
dispatch(addTodo(input));
setInput('');
}
};
const handleRemoveTodo = (id) => {
dispatch(removeTodo(id));
};
if (loading) return <p>Loading todos...</p>;
if (error) return <p>Error fetching todos: {error.message}</p>;
return (
<div>
<h1>Todo List</h1>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add todo..."
/>
<button onClick={handleAddTodo}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}{' '}
<button onClick={() => handleRemoveTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
export default App;
Adicionando Actions Assíncronas
O action creator fetchTodos acima simula a busca de tarefas de uma API. Em um projeto real, substitua a URL do fetch pelo endpoint real da sua API.
Boas Práticas com Redux
- Sempre retorne novos objetos de estado dos reducers. Nunca mute o estado diretamente.
- Normalize a estrutura do seu estado. Estruturas planas são mais fáceis de atualizar do que estruturas profundamente aninhadas.
- Mantenha o estado serializável. Valores não serializáveis quebram o debug por viagem no tempo.
- Escreva funções seletoras para encapsular o acesso ao estado. Isso permite alterar a estrutura do estado depois sem precisar atualizar cada componente.
- Use
combineReducerspara dividir um reducer grande em partes focadas. - Trate operações assíncronas no middleware, não nos reducers.
Conclusão
O Redux impõe uma disciplina que compensa à medida que as aplicações crescem. Um store, um sentido de fluxo de dados, reducers puros: essas restrições tornam os bugs reproduzíveis e as mudanças de estado auditáveis. A aplicação de tarefas acima cobre o ciclo completo, desde as actions passando pelos reducers até os componentes conectados, além da busca assíncrona via Thunk. Esse padrão escala diretamente para aplicações maiores sem mudanças fundamentais.