Visão introdutória de Redux Toolkit e Zustand para React Native

Capítulo 8

Tempo estimado de leitura: 10 minutos

+ Exercício

Quando considerar Redux Toolkit ou Zustand

Ao sair do gerenciamento de estado com Context, dois caminhos comuns em apps React Native são: Redux Toolkit (RTK), que estrutura o estado global com um fluxo explícito (store, actions, reducers, slices e middleware), e Zustand, que oferece uma abordagem mais direta baseada em hooks e lojas (stores) com menos cerimônia.

Ambos resolvem problemas típicos de apps que crescem: compartilhamento de estado entre telas, previsibilidade, depuração, separação de responsabilidades e redução de acoplamento entre UI e regras de negócio.

Conceitos essenciais (sem excesso de teoria)

Redux Toolkit (RTK)

  • Store: objeto central que guarda o estado global do app. No React Native, normalmente fica no topo da árvore (Provider).
  • Actions: eventos que descrevem “o que aconteceu” (ex.: cart/addItem). No RTK, muitas actions são geradas automaticamente pelos slices.
  • Reducers: funções que recebem (state, action) e produzem o novo estado. No RTK, você escreve reducers “mutáveis” graças ao Immer, mas o resultado é imutável.
  • Slices: agrupam estado + reducers + actions em um módulo coeso (ex.: cartSlice).
  • Middleware: camada entre dispatch e reducer para lidar com efeitos (logs, métricas, chamadas assíncronas). No RTK, o padrão para async é createAsyncThunk (que se integra ao fluxo de actions).

Zustand

  • Store: uma função que cria um estado e ações associadas. Não exige reducers nem actions explícitas.
  • Hooks: você consome o estado com um hook (ex.: useCartStore) e seleciona apenas o que precisa, reduzindo re-renders.
  • Atualizações diretas: você chama funções (ações) que atualizam o estado via set.
  • Middleware (opcional): persistência, devtools, logs, etc., via wrappers (ex.: persist).

Exemplo comparável (mesma feature): carrinho simples

Vamos implementar a mesma feature em RTK e em Zustand para comparar ergonomia e escalabilidade. A feature: adicionar/remover itens, limpar carrinho e calcular total.

Modelo de dados usado nos dois exemplos

type CartItem = { id: string; name: string; price: number; qty: number };

Redux Toolkit na prática

1) Instalação

npm i @reduxjs/toolkit react-redux

2) Organização de arquivos (sugestão)

  • src/store/index.ts (configuração da store)
  • src/features/cart/cartSlice.ts (slice do carrinho)
  • src/features/cart/selectors.ts (seletores, quando fizer sentido)
  • src/app/providers/AppProviders.tsx (Provider do Redux, se você centraliza providers)

Essa organização por feature tende a escalar melhor do que separar por “tipo” (reducers/actions) em apps grandes.

3) Criando o slice

// src/features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

type CartItem = { id: string; name: string; price: number; qty: number };

type CartState = {
  items: CartItem[];
};

const initialState: CartState = {
  items: [],
};

function upsertItem(items: CartItem[], item: Omit<CartItem, 'qty'>) {
  const found = items.find(i => i.id === item.id);
  if (found) found.qty += 1;
  else items.push({ ...item, qty: 1 });
}

export const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addItem: (state, action: PayloadAction<{ id: string; name: string; price: number }>) => {
      upsertItem(state.items, action.payload);
    },
    removeItem: (state, action: PayloadAction<{ id: string }>) => {
      state.items = state.items.filter(i => i.id !== action.payload.id);
    },
    clear: (state) => {
      state.items = [];
    },
    setQty: (state, action: PayloadAction<{ id: string; qty: number }>) => {
      const item = state.items.find(i => i.id === action.payload.id);
      if (!item) return;
      item.qty = Math.max(1, action.payload.qty);
    },
  },
});

export const { addItem, removeItem, clear, setQty } = cartSlice.actions;
export default cartSlice.reducer;

O que observar: o RTK gera as actions automaticamente e você escreve reducers de forma direta. Ainda assim, existe uma estrutura formal (slice + store + Provider).

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

4) Configurando a store

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from '../features/cart/cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

5) Conectando no app (Provider)

// src/app/providers/AppProviders.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../../store';

type Props = { children: React.ReactNode };

export function AppProviders({ children }: Props) {
  return <Provider store={store}>{children}</Provider>;
}

6) Consumindo no componente

// Exemplo de uso em uma tela/componente
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '../store';
import { addItem, clear } from '../features/cart/cartSlice';

export function CartSummary() {
  const dispatch = useDispatch<AppDispatch>();
  const items = useSelector((state: RootState) => state.cart.items);
  const total = items.reduce((acc, i) => acc + i.price * i.qty, 0);

  return (
    <View>
      <Text>Itens: {items.length}</Text>
      <Text>Total: {total.toFixed(2)}</Text>
      <Button
        title="Adicionar produto A"
        onPress={() => dispatch(addItem({ id: 'a', name: 'Produto A', price: 10 }))}
      />
      <Button title="Limpar" onPress={() => dispatch(clear())} />
    </View>
  );
}

7) Onde entram middleware e async (visão rápida)

Se o carrinho precisar sincronizar com API (ex.: validar estoque), o RTK costuma usar createAsyncThunk e tratar estados pending/fulfilled/rejected no slice. Isso mantém um fluxo previsível e fácil de rastrear.

// Exemplo mínimo (sem detalhar API)
import { createAsyncThunk } from '@reduxjs/toolkit';

export const syncCart = createAsyncThunk('cart/sync', async () => {
  // await api.post('/cart/sync', ...)
  return true;
});

Zustand na prática

1) Instalação

npm i zustand

2) Organização de arquivos (sugestão)

  • src/stores/cartStore.ts (store do carrinho)
  • src/stores/selectors.ts (opcional, para seletores reutilizáveis)
  • src/domain/cart/ (opcional, regras puras para não acoplar à store)

Zustand não exige Provider. Isso reduz configuração, mas aumenta a importância de organizar bem o código para evitar “store gigante”.

3) Criando a store do carrinho

// src/stores/cartStore.ts
import { create } from 'zustand';

type CartItem = { id: string; name: string; price: number; qty: number };

type CartState = {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'qty'>) => void;
  removeItem: (id: string) => void;
  clear: () => void;
  setQty: (id: string, qty: number) => void;
};

export const useCartStore = create<CartState>((set, get) => ({
  items: [],

  addItem: (item) =>
    set((state) => {
      const found = state.items.find((i) => i.id === item.id);
      if (found) {
        return {
          items: state.items.map((i) =>
            i.id === item.id ? { ...i, qty: i.qty + 1 } : i
          ),
        };
      }
      return { items: [...state.items, { ...item, qty: 1 }] };
    }),

  removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })),

  clear: () => set({ items: [] }),

  setQty: (id, qty) =>
    set((state) => ({
      items: state.items.map((i) =>
        i.id === id ? { ...i, qty: Math.max(1, qty) } : i
      ),
    })),
}));

O que observar: não há reducers nem actions formais. Você chama funções diretamente. Para equipes grandes, isso pode ser ótimo (menos boilerplate) ou perigoso (se virar “qualquer um muda qualquer coisa”). A organização e convenções do time fazem diferença.

4) Consumindo no componente (com seletores)

import React from 'react';
import { View, Text, Button } from 'react-native';
import { useCartStore } from '../stores/cartStore';

export function CartSummary() {
  const items = useCartStore((s) => s.items);
  const addItem = useCartStore((s) => s.addItem);
  const clear = useCartStore((s) => s.clear);

  const total = items.reduce((acc, i) => acc + i.price * i.qty, 0);

  return (
    <View>
      <Text>Itens: {items.length}</Text>
      <Text>Total: {total.toFixed(2)}</Text>
      <Button
        title="Adicionar produto A"
        onPress={() => addItem({ id: 'a', name: 'Produto A', price: 10 })}
      />
      <Button title="Limpar" onPress={clear} />
    </View>
  );
}

O uso de seletores ((s) => s.items) é importante para evitar re-render desnecessário: cada componente assina apenas o pedaço do estado que precisa.

5) Middleware útil: persistência (visão rápida)

Em React Native, persistir carrinho é comum. Zustand oferece middleware como persist. Em RN, normalmente você integra com um storage (ex.: AsyncStorage) via configuração, mas a ideia principal é: persistir o estado sem criar uma camada grande de reducers/actions.

// Exemplo conceitual (detalhes de storage variam)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create(
  persist(
    (set) => ({ items: [], clear: () => set({ items: [] }) }),
    { name: 'cart' }
  )
);

Comparando ergonomia e escalabilidade (na prática)

CritérioRedux ToolkitZustand
Boilerplate inicialMédio (store + Provider + slices)Baixo (criar store e usar hook)
Padronização para timesAlta (fluxo e estrutura bem definidos)Média (depende de convenções internas)
Previsibilidade do fluxoAlta (actions/reducers, bom para auditoria)Alta se bem organizado, mas mais livre
Async e efeitosBem estruturado (thunks/middleware)Direto (ações async na store), exige disciplina
Escala (muitas features)Ótimo com organização por featureÓtimo se dividir stores e evitar “store monolítica”
Curva de aprendizadoMédia (conceitos do Redux)Baixa (API pequena)

Critérios práticos de escolha

Escolha Redux Toolkit quando

  • Você precisa de um padrão forte para um time grande (muitas pessoas mexendo no estado).
  • Há necessidade de rastreabilidade clara (ex.: auditoria de eventos, debugging estruturado, logs por action).
  • O app tem muitos fluxos assíncronos e você quer um caminho consistente para lidar com loading/erro/sucesso.
  • Você quer incentivar separação explícita entre “evento” (action) e “mudança de estado” (reducer).

Escolha Zustand quando

  • Você quer simplicidade e rapidez para evoluir features sem montar muita infraestrutura.
  • O estado global é moderado e você prefere uma abordagem direta (funções que atualizam estado).
  • Você quer reduzir camadas e manter o estado próximo do uso, sem abrir mão de reuso.
  • Você consegue impor convenções para evitar que a store vire um “depósito” de lógica sem organização.

Como evitar boilerplate e acoplamento (boas práticas aplicáveis aos dois)

1) Separe regras puras da camada de estado

Evite colocar toda regra de negócio dentro do slice/store. Extraia funções puras para um módulo de domínio. Isso reduz acoplamento e facilita testes.

// src/domain/cart/cartMath.ts
import type { CartItem } from './types';

export function calcTotal(items: CartItem[]) {
  return items.reduce((acc, i) => acc + i.price * i.qty, 0);
}

No componente, use calcTotal(items) em vez de duplicar lógica. No Redux, você pode transformar isso em selector; no Zustand, pode ser um helper compartilhado.

2) Prefira organização por feature

  • Redux: src/features/cart/ com cartSlice.ts, selectors.ts, types.ts.
  • Zustand: src/stores/cartStore.ts e, se crescer, src/stores/cart/ com ações separadas.

3) Crie seletores reutilizáveis (quando o app crescer)

Seletores evitam repetição e centralizam cálculos derivados.

// Redux selector
// src/features/cart/selectors.ts
import type { RootState } from '../../store';

export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = (state: RootState) =>
  state.cart.items.reduce((acc, i) => acc + i.price * i.qty, 0);
// Zustand selector helper (opcional)
export const selectCartTotal = (s: { items: { price: number; qty: number }[] }) =>
  s.items.reduce((acc, i) => acc + i.price * i.qty, 0);

4) Evite “estado global por padrão”

Mesmo usando RTK ou Zustand, nem tudo precisa ser global. Use store global para dados realmente compartilhados (sessão, carrinho, preferências, cache). Estado de UI local (input, toggle, estado de modal específico) costuma ficar melhor no componente.

5) Defina limites claros para cada store/slice

  • Um slice/store deve representar uma feature ou um domínio (ex.: auth, cart, settings).
  • Evite dependências circulares: UI chama ações; ações atualizam estado; regras puras ficam fora.
  • Se uma feature depende de outra, prefira integrar via funções de domínio/serviços, não importando diretamente o estado interno de outra store/slice.

6) Convenções para reduzir acoplamento com a UI

  • Exponha ações com nomes de intenção: addItem, clear, syncCart (em vez de setItems em todo lugar).
  • Evite passar objetos gigantes para ações; prefira payloads pequenos e claros.
  • Centralize transformações complexas em helpers/serviços para não espalhar lógica por telas.

Agora responda o exercício sobre o conteúdo:

Ao migrar do Context para uma solução de estado global em um app React Native, qual cenário indica melhor o uso do Redux Toolkit em vez do Zustand?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O Redux Toolkit é mais indicado quando você precisa de padronização e rastreabilidade do fluxo (actions/reducers) e quer um modelo bem estruturado para efeitos e async (middleware/thunks), especialmente em times maiores.

Próximo capitúlo

Formulários em React Native: validação, máscaras e experiência de uso

Arrow Right Icon
Capa do Ebook gratuito React Native Essencial: criando apps completos com JavaScript e boas práticas
50%

React Native Essencial: criando apps completos com JavaScript e boas práticas

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.