React & Frontend

Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção

12 min de leitura

Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção

Entendendo Redux Toolkit: Fundamentos e Filosofia 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. A filosofia do Redux Toolkit é simples: reduzir a complexidade sem perder o poder. 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 Slices. 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). Slices: O Coração do Redux Toolkit O que é um Slice? 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.

<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 &quot;puro&quot; 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 &#039;@reduxjs/toolkit&#039;;

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: &#039;cart&#039;,

initialState,

reducers: {

addItem: (state, action: PayloadAction&lt;CartItem&gt;) =&gt; {

const existingItem = state.items.find(

(item) =&gt; 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&lt;string&gt;) =&gt; {

const item = state.items.find((item) =&gt; item.id === action.payload);

if (item) {

state.totalPrice -= item.price * item.quantity;

state.items = state.items.filter((item) =&gt; item.id !== action.payload);

}

},

clearCart: (state) =&gt; {

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&lt;CartItem&gt;</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 &#039;@reduxjs/toolkit&#039;;

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&lt;

User, // Tipo de retorno (fulfilled)

string, // Tipo do argumento

{

rejectValue: string; // Tipo do erro

}

&gt;(

&#039;user/fetchUser&#039;,

async (userId, { rejectWithValue }) =&gt; {

try {

const response = await fetch(/api/users/${userId});

if (!response.ok) {

return rejectWithValue(&#039;Falha ao buscar usuário&#039;);

}

return await response.json();

} catch (error) {

return rejectWithValue(&#039;Erro de rede&#039;);

}

}

);

const userSlice = createSlice({

name: &#039;user&#039;,

initialState,

reducers: {},

extraReducers: (builder) =&gt; {

builder

.addCase(fetchUser.pending, (state) =&gt; {

state.loading = true;

state.error = null;

})

.addCase(fetchUser.fulfilled, (state, action) =&gt; {

state.loading = false;

state.user = action.payload; // payload é tipado como User

})

.addCase(fetchUser.rejected, (state, action) =&gt; {

state.loading = false;

state.error = action.payload ?? &#039;Erro desconhecido&#039;; // 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 &#039;@reduxjs/toolkit/query/react&#039;;

interface Product {

id: string;

name: string;

price: number;

description: string;

}

interface ApiResponse&lt;T&gt; {

data: T;

success: boolean;

}

export const productsApi = createApi({

reducerPath: &#039;productsApi&#039;,

baseQuery: fetchBaseQuery({

baseUrl: &#039;https://api.example.com&#039;,

}),

tagTypes: [&#039;Product&#039;],

endpoints: (builder) =&gt; ({

// Query: para leitura de dados

getProducts: builder.query&lt;Product[], void&gt;({

query: () =&gt; &#039;/products&#039;,

providesTags: [&#039;Product&#039;],

}),

// Query parametrizada: para leitura com filtros

getProductById: builder.query&lt;Product, string&gt;({

query: (id) =&gt; /products/${id},

providesTags: (result, error, id) =&gt; [{ type: &#039;Product&#039;, id }],

}),

// Mutation: para escrita/atualização de dados

createProduct: builder.mutation&lt;Product, Omit&lt;Product, &#039;id&#039;&gt;&gt;({

query: (newProduct) =&gt; ({

url: &#039;/products&#039;,

method: &#039;POST&#039;,

body: newProduct,

}),

invalidatesTags: [&#039;Product&#039;],

}),

// Mutation para atualização

updateProduct: builder.mutation&lt;

Product,

{ id: string; data: Partial&lt;Product&gt; }

&gt;({

query: ({ id, data }) =&gt; ({

url: /products/${id},

method: &#039;PATCH&#039;,

body: data,

}),

invalidatesTags: (result, error, { id }) =&gt; [

{ type: &#039;Product&#039;, id },

&#039;Product&#039;,

],

}),

// Mutation para deleção

deleteProduct: builder.mutation&lt;void, string&gt;({

query: (id) =&gt; ({

url: /products/${id},

method: &#039;DELETE&#039;,

}),

invalidatesTags: [&#039;Product&#039;],

}),

}),

});

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 &#039;react&#039;;

import {

useGetProductsQuery,

useCreateProductMutation,

} from &#039;./productsApi&#039;;

export const ProductList: React.FC = () =&gt; {

const { data: products, isLoading, error } = useGetProductsQuery();

const [createProduct, { isLoading: isCreating }] =

useCreateProductMutation();

if (isLoading) return &lt;div&gt;Carregando...&lt;/div&gt;;

if (error) return &lt;div&gt;Erro ao carregar produtos&lt;/div&gt;;

const handleAddProduct = async () =&gt; {

try {

await createProduct({

name: &#039;Novo Produto&#039;,

price: 99.99,

description: &#039;Um produto incrível&#039;,

}).unwrap(); // unwrap() torna a promise rejeitável

alert(&#039;Produto criado com sucesso&#039;);

} catch (err) {

alert(&#039;Falha ao criar produto&#039;);

}

};

return (

&lt;div&gt;

&lt;button onClick={handleAddProduct} disabled={isCreating}&gt;

{isCreating ? &#039;Criando...&#039; : &#039;Adicionar Produto&#039;}

&lt;/button&gt;

&lt;ul&gt;

{products?.map((product) =&gt; (

&lt;li key={product.id}&gt;{product.name} - ${product.price}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

};</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 &#039;@reduxjs/toolkit&#039;;

import cartReducer from &#039;./slices/cartSlice&#039;;

import userReducer from &#039;./slices/userSlice&#039;;

import { productsApi } from &#039;./apis/productsApi&#039;;

export const store = configureStore({

reducer: {

cart: cartReducer,

user: userReducer,

[productsApi.reducerPath]: productsApi.reducer,

},

middleware: (getDefaultMiddleware) =&gt;

getDefaultMiddleware().concat(productsApi.middleware),

});

// Tipos exportados para uso em todo o app

export type RootState = ReturnType&lt;typeof store.getState&gt;;

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 &#039;react-redux&#039;;

import type { RootState, AppDispatch } from &#039;./store&#039;;

// Use esses hooks em vez dos imports diretos

export const useAppDispatch = () =&gt; useDispatch&lt;AppDispatch&gt;();

export const useAppSelector = &lt;T,&gt;(selector: (state: RootState) =&gt; T): T =&gt;

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 &#039;./hooks&#039;;

import { fetchUser } from &#039;./slices/userSlice&#039;;

import { addItem } from &#039;./slices/cartSlice&#039;;

export const MyComponent: React.FC = () =&gt; {

const dispatch = useAppDispatch();

const user = useAppSelector((state) =&gt; state.user.user); // Tipado automaticamente

const cartItems = useAppSelector((state) =&gt; state.cart.items); // Tipado automaticamente

const handleAddToCart = () =&gt; {

dispatch(

addItem({

id: &#039;1&#039;,

name: &#039;Produto&#039;,

price: 100,

quantity: 1,

})

);

};

const handleLoadUser = () =&gt; {

dispatch(fetchUser(&#039;123&#039;)); // Tipado como string

};

return (

&lt;div&gt;

{/ Seu componente aqui /}

&lt;/div&gt;

);

};</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Higher-Order Components em React: Composição e Tipagem Correta na Prática
Higher-Order Components em React: Composição e Tipagem Correta na Prática

O que é um Higher-Order Component? Um Higher-Order Component (HOC) é um padrã...

Como Usar Micro-frontends com React: Module Federation e Arquitetura Distribuída em Produção
Como Usar Micro-frontends com React: Module Federation e Arquitetura Distribuída em Produção

O que são Micro-frontends e Por Que Module Federation? Micro-frontends repres...

O que Todo Dev Deve Saber sobre Hooks para WebSockets: Conexão Reativa e Reconexão Automática
O que Todo Dev Deve Saber sobre Hooks para WebSockets: Conexão Reativa e Reconexão Automática

Entendendo WebSockets e a Necessidade de Hooks Reativos WebSockets estabelece...