<h2>Entendendo Redux Toolkit: Fundamentos e Filosofia</h2>
<p>Redux é uma biblioteca de gerenciamento de estado previsível para aplicações JavaScript. Porém, a configuração tradicional do Redux é verbose e exige muito código boilerplate. O Redux Toolkit (RTK) foi criado para simplificar drasticamente essa experiência, fornecendo utilitários que encapsulam as melhores práticas de forma elegante e moderna.</p>
<p>A filosofia do Redux Toolkit é simples: <strong>reduzir a complexidade sem perder o poder</strong>. Enquanto o Redux "puro" exigia que você criasse actions, action creators, reducers e middwares separadamente, o RTK consolida tudo isso em uma API intuitiva chamada <strong>Slices</strong>. Além disso, o toolkit já vem configurado com Redux Thunk (para ações assíncronas) e Immer (para mutações imutáveis de forma legível).</p>
<h2>Slices: O Coração do Redux Toolkit</h2>
<h3>O que é um Slice?</h3>
<p>Um Slice é um objeto que contém a lógica de reducer, actions e initial state tudo junto em um único lugar. É como ter um mini-domínio de estado com suas próprias regras e ações. Internamente, o <code>createSlice</code> utiliza Immer, permitindo que você escreva código que parece estar mutando o estado, mas na verdade mantém imutabilidade.</p>
<h3>Criando seu Primeiro Slice</h3>
<p>Vamos criar um exemplo prático de um slice para gerenciar um carrinho de compras:</p>
<pre><code class="language-typescript">import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
totalPrice: number;
}
const initialState: CartState = {
items: [],
totalPrice: 0,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
const existingItem = state.items.find(
(item) => item.id === action.payload.id
);
if (existingItem) {
existingItem.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
state.totalPrice += action.payload.price * action.payload.quantity;
},
removeItem: (state, action: PayloadAction<string>) => {
const item = state.items.find((item) => item.id === action.payload);
if (item) {
state.totalPrice -= item.price * item.quantity;
state.items = state.items.filter((item) => item.id !== action.payload);
}
},
clearCart: (state) => {
state.items = [];
state.totalPrice = 0;
},
},
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;</code></pre>
<p>Observe como o código parece estar mutando <code>state.items</code> e <code>state.totalPrice</code> diretamente. Graças ao Immer, isso é seguro e resultará em um novo estado imutável. Não há necessidade de espalhamento (<code>...</code>) ou cópias manuais. As actions são geradas automaticamente, e o reducer está pronto para ser integrado.</p>
<h3>Tipagem Robusta com TypeScript</h3>
<p>Note que utilizei <code>PayloadAction<CartItem></code> para tipar o payload das actions. Isso garante que TypeScript saiba exatamente qual tipo de dados está sendo passado. Se você tentar passar dados incompatíveis, o compilador avisará imediatamente. Essa tipagem rigorosa previne bugs em tempo de desenvolvimento, não apenas em runtime.</p>
<h2>Thunks Tipados: Ações Assíncronas com Segurança</h2>
<h3>O Problema das Ações Assíncronas</h3>
<p>Redux é síncrono por padrão: um reducer recebe uma action e retorna um novo estado. Mas aplicações reais precisam fazer requisições HTTP, consultar databases ou executar operações demoradas. Para isso, usamos Thunks — funções que retornam outras funções, permitindo lógica assíncrona antes de disparar uma action.</p>
<h3>Criando um Thunk Tipado com <code>createAsyncThunk</code></h3>
<p>O <code>createAsyncThunk</code> do RTK fornece uma maneira tipada e inteligente de lidar com ações assíncronas. Ele automaticamente cria actions para pending, fulfilled e rejected:</p>
<pre><code class="language-typescript">import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
interface User {
id: string;
name: string;
email: string;
}
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
loading: false,
error: null,
};
// Thunk tipado: recebe um userId como argumento
export const fetchUser = createAsyncThunk<
User, // Tipo de retorno (fulfilled)
string, // Tipo do argumento
{
rejectValue: string; // Tipo do erro
}
>(
'user/fetchUser',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(/api/users/${userId});
if (!response.ok) {
return rejectWithValue('Falha ao buscar usuário');
}
return await response.json();
} catch (error) {
return rejectWithValue('Erro de rede');
}
}
);
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload; // payload é tipado como User
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? 'Erro desconhecido'; // payload é tipado como string
});
},
});
export default userSlice.reducer;</code></pre>
<p><strong>Explicação dos generics</strong>: O primeiro genérico (<code>User</code>) define o tipo do <code>action.payload</code> em <code>fulfilled</code>. O segundo (<code>string</code>) define o tipo do argumento que o thunk recebe. No objeto de configuração, <code>rejectValue</code> especifica o tipo do erro retornado. TypeScript agora garantirá que você não cometa erros ao usar esses tipos.</p>
<h2>RTK Query: Gerenciamento de Cache de Dados Remotos</h2>
<h3>Por que RTK Query?</h3>
<p>Quando você tem múltiplas requisições HTTP, o padrão thunks + slices começa a ficar repetitivo: você escreve pending/fulfilled/rejected para cada requisição, gerencia cache manualmente, e lida com sincronização de estado. O <strong>RTK Query</strong> é uma solução de alto nível que abstrai toda essa complexidade, focando em <strong>APIs e cache automático</strong>.</p>
<p>RTK Query é particularmente poderoso porque oferece:</p>
<ul>
<li>Cache automático com estratégias de invalidação</li>
<li>Requisições dedupliquées (mesma requisição não é feita duas vezes no mesmo tempo)</li>
<li>Refetch automático em condições específicas</li>
<li>Serialização de argumentos inteligente</li>
<li>Suporte completo a WebSockets com <code>streamingUpdates</code></li>
</ul>
<h3>Criando uma API com RTK Query</h3>
<pre><code class="language-typescript">import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
interface Product {
id: string;
name: string;
price: number;
description: string;
}
interface ApiResponse<T> {
data: T;
success: boolean;
}
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com',
}),
tagTypes: ['Product'],
endpoints: (builder) => ({
// Query: para leitura de dados
getProducts: builder.query<Product[], void>({
query: () => '/products',
providesTags: ['Product'],
}),
// Query parametrizada: para leitura com filtros
getProductById: builder.query<Product, string>({
query: (id) => /products/${id},
providesTags: (result, error, id) => [{ type: 'Product', id }],
}),
// Mutation: para escrita/atualização de dados
createProduct: builder.mutation<Product, Omit<Product, 'id'>>({
query: (newProduct) => ({
url: '/products',
method: 'POST',
body: newProduct,
}),
invalidatesTags: ['Product'],
}),
// Mutation para atualização
updateProduct: builder.mutation<
Product,
{ id: string; data: Partial<Product> }
>({
query: ({ id, data }) => ({
url: /products/${id},
method: 'PATCH',
body: data,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'Product', id },
'Product',
],
}),
// Mutation para deleção
deleteProduct: builder.mutation<void, string>({
query: (id) => ({
url: /products/${id},
method: 'DELETE',
}),
invalidatesTags: ['Product'],
}),
}),
});
export const {
useGetProductsQuery,
useGetProductByIdQuery,
useCreateProductMutation,
useUpdateProductMutation,
useDeleteProductMutation,
} = productsApi;</code></pre>
<h3>Usando RTK Query em Componentes React</h3>
<pre><code class="language-typescript">import React from 'react';
import {
useGetProductsQuery,
useCreateProductMutation,
} from './productsApi';
export const ProductList: React.FC = () => {
const { data: products, isLoading, error } = useGetProductsQuery();
const [createProduct, { isLoading: isCreating }] =
useCreateProductMutation();
if (isLoading) return <div>Carregando...</div>;
if (error) return <div>Erro ao carregar produtos</div>;
const handleAddProduct = async () => {
try {
await createProduct({
name: 'Novo Produto',
price: 99.99,
description: 'Um produto incrível',
}).unwrap(); // unwrap() torna a promise rejeitável
alert('Produto criado com sucesso');
} catch (err) {
alert('Falha ao criar produto');
}
};
return (
<div>
<button onClick={handleAddProduct} disabled={isCreating}>
{isCreating ? 'Criando...' : 'Adicionar Produto'}
</button>
<ul>
{products?.map((product) => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</div>
);
};</code></pre>
<p>RTK Query cuida de tudo: requisição, cache, atualização automática do cache quando você dispara uma mutation. Não há necessidade de escrever reducers extras ou gerenciar flags de loading manualmente (embora você tenha acesso a elas se precisar).</p>
<h2>Integrando Tudo: Store Completo e Tipado</h2>
<h3>Configurando a Store com TypeScript</h3>
<p>Agora que você entende Slices, Thunks e RTK Query, é hora de montar uma store funcional e totalmente tipada:</p>
<pre><code class="language-typescript">import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './slices/cartSlice';
import userReducer from './slices/userSlice';
import { productsApi } from './apis/productsApi';
export const store = configureStore({
reducer: {
cart: cartReducer,
user: userReducer,
[productsApi.reducerPath]: productsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productsApi.middleware),
});
// Tipos exportados para uso em todo o app
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;</code></pre>
<h3>Hooks Tipados para Uso em Componentes</h3>
<pre><code class="language-typescript">import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Use esses hooks em vez dos imports diretos
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector = <T,>(selector: (state: RootState) => T): T =>
useSelector(selector);</code></pre>
<p>Com esses hooks, TypeScript saberá exatamente qual é o tipo de cada slice e garantirá que você não tente acessar propriedades inexistentes. Exemplo de uso:</p>
<pre><code class="language-typescript">import { useAppSelector, useAppDispatch } from './hooks';
import { fetchUser } from './slices/userSlice';
import { addItem } from './slices/cartSlice';
export const MyComponent: React.FC = () => {
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.user.user); // Tipado automaticamente
const cartItems = useAppSelector((state) => state.cart.items); // Tipado automaticamente
const handleAddToCart = () => {
dispatch(
addItem({
id: '1',
name: 'Produto',
price: 100,
quantity: 1,
})
);
};
const handleLoadUser = () => {
dispatch(fetchUser('123')); // Tipado como string
};
return (
<div>
{/ Seu componente aqui /}
</div>
);
};</code></pre>
<h2>Conclusão</h2>
<p>Aprendemos três pilares fundamentais do Redux Toolkit moderno: <strong>Slices</strong> simplificam drasticamente o boilerplate reduzindo actions, reducers e initial state em uma única declaração; <strong>Thunks tipados</strong> permitem operações assíncronas com segurança de tipo, onde pending, fulfilled e rejected são gerenciados automaticamente; <strong>RTK Query</strong> abstrai o gerenciamento de cache HTTP, eliminando código repetitivo e oferecendo recursos sofisticados como deduplicação e invalidação automática de tags. Juntos, esses três conceitos formam uma stack completa e robusta para gerenciamento de estado em aplicações modernas com TypeScript.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer">Redux Toolkit Official Documentation</a></li>
<li><a href="https://redux-toolkit.js.org/rtk-query/overview" target="_blank" rel="noopener noreferrer">RTK Query Guide</a></li>
<li><a href="https://redux-toolkit.js.org/usage/usage-guide#async-thunks" target="_blank" rel="noopener noreferrer">Redux Thunks with TypeScript</a></li>
<li><a href="https://immerjs.github.io/immer/" target="_blank" rel="noopener noreferrer">Immer Documentation</a></li>
<li><a href="https://redux.js.org/usage/thinking-in-redux" target="_blank" rel="noopener noreferrer">Modern Redux Patterns - Redux Blog</a></li>
</ul>
<p><!-- FIM --></p>