Dominando a Arquitetura de Redux: Guia Completo para Desenvolver Aplicações Escaláveis

Dominando a Arquitetura de Redux: Guia Completo para Desenvolver Aplicações Escaláveis

“`html





Dominando a Arquitetura de Redux — Guia Técnico


Dominando a Arquitetura de Redux

Quando Redux está bem arquitetado, o fluxo de dados fica previsível, mudanças locais não quebram o resto e o código evolui sem virar um “monstro de actions”.
Abaixo eu organizo um modelo prático (e técnico) para você dominar o desenho de store, slices, side effects e selectors com consistência.
Arquitetura • Redux Toolkit
Slices & Estado
Selectors performáticos
Side effects organizados

1) Estruture o Estado por Domínios (não por telas)

O erro mais comum é modelar o estado pensando em componentes: “como eu renderizo”, “qual tela está aberta”, “qual botão foi clicado”.
Redux fica mais simples quando o estado representa domínios e contratos.

Single Source of Truth
Dados e decisões vivem no estado; UI apenas reflete.

Contrato explícito
Estado responde “o que é” e “qual é a versão”.

Baixo acoplamento
Telas compõem; domínios não dependem de UI.

Como eu divido o estado na prática

  • Slice = um domínio (ex.: auth, orders, catalog).
  • Estado costuma guardar: entidades normalizadas, flags de carregamento e erros.
  • UI state (ex.: modal aberto) pode morar em slice quando afeta várias áreas; caso contrário, pode ficar local ao componente.
Regra de ouro:

Se dois ou mais componentes precisam “concordar” sobre algo, esse algo vira estado global (Redux).
Se só um componente usa, mantenha local. Isso evita Redux virando um depósito de detalhes visuais.

2) Use Slices com responsabilidades claras

Um slice bom tem fronteiras nítidas: define o formato do estado, publica reducers para mudanças previsíveis e exporta ações
que representam eventos do domínio. O restante do código consulta o estado via selectors.

Checklist para escrever reducers que escalam

  • Reducer síncrono: transforme estado em resposta a um evento já decidido (ex.: “usuário logou”, “produto foi adicionado”).
  • Imutabilidade sem sofrimento: use mutação “imutável” via Redux Toolkit (immer) com intenção clara.
  • Erros e status: padronize campos como status e error para o domínio.
  • Evite reducers “genéricos”: ações amplas do tipo “setData” quebram o entendimento do contrato do estado.

Formatação do estado: o padrão que eu recomendo

Para cada domínio, mantenha um formato consistente. Exemplo (conceitual):

  • entities: mapa por id (ou array se o volume for pequeno e estável).
  • ids: quando usar normalização.
  • status: “idle | loading | success | error”.
  • error: string/objeto com detalhe consumível na UI.
  • lastUpdated: opcional, ajuda a evitar refetch desnecessário.

3) Organize side effects: assíncrono fora do reducer

Redux deve manter decisões determinísticas nos reducers. Qualquer IO (requests, websockets, timers) entra como side effect.
A questão não é “se pode” — é como manter legibilidade, reuso e controle de estados de carregamento.

Como eu penso o fluxo

  • A ação de “intenção” dispara o efeito (ex.: fetchOrdersRequested).
  • O middleware/handler executa o async e despacha ações para sucesso/erro.
  • O reducer só recebe o resultado e atualiza o estado do domínio.
Benefício prático:

você separa “o que fazer” (orquestração do efeito) de “o que muda” (transformação do estado),
reduzindo bugs em cenários de concorrência (ex.: duas requisições seguidas).

Concorrência e cancelamento

Em cenários reais, você vai ter múltiplos disparos (scroll infinito, filtros, navegação).
O desenho ideal inclui:

  • um status por operação quando necessário (ex.: carregando lista vs carregando detalhes);
  • um mecanismo para ignorar respostas “antigas” (por exemplo, usando parâmetros/versões);
  • cancelamento quando fizer sentido (principalmente em navegação).

4) Selectors: performance, encapsulamento e previsibilidade

Selectors são a camada entre “como o estado é guardado” e “como a UI consome”.
Quando você cria selectors bem definidos, muda a estrutura interna do estado sem quebrar a UI.

O que um selector bom faz

  • Encapsula a forma interna do estado (evita acessar state.slice.x.y em todo lugar).
  • Minimiza recalculações com memoização quando necessário.
  • Expõe contratos simples para componentes: lista pronta, flags e valores deriváveis.

Evite: lógica pesada em componente

Se o componente está fazendo filtro, ordenação, agregações ou transformação de entidades, você provavelmente
está perdendo performance e criando código difícil de testar.

Dica objetiva:

Se um selector vira “utilitário de renderização”, ele precisa existir como selector para manter consistência e reduzir repetição.

// exemplos conceituais com Redux Toolkit (estrutura e contratos)
import { createAsyncThunk, createSlice, createSelector } from "@reduxjs/toolkit";

/** ====== Estado (domínio) ====== */
const initialState = {
  ids: [],
  entities: {}, // id -> item
  status: "idle", // idle | loading | success | error
  error: null,
  lastUpdated: null
};

/** ====== Side effect ====== */
export const fetchOrders = createAsyncThunk(
  "orders/fetchOrders",
  async ({ customerId, signal }) => {
    // IO fora do reducer
    const res = await fetch(`/api/customers/${customerId}/orders`, { signal });
    if (!res.ok) throw new Error("Falha ao buscar pedidos");
    return res.json(); // exemplo: array de pedidos
  },
  {
    condition: ({ customerId }, { getState }) => {
      // evita refetch desnecessário (exemplo simples)
      const state = getState();
      // se já atualizou muito recentemente, você pode retornar false
      // aqui deixei como exemplo estrutural:
      return Boolean(customerId);
    }
  }
);

/** ====== Slice ====== */
const ordersSlice = createSlice({
  name: "orders",
  initialState,
  reducers: {
    orderAdded(state, action) {
      const order = action.payload;
      if (state.entities[order.id]) return; // idempotência simples
      state.entities[order.id] = order;
      state.ids.push(order.id);
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchOrders.pending, (state) => {
        state.status = "loading";
        state.error = null;
      })
      .addCase(fetchOrders.fulfilled, (state, action) => {
        const orders = action.payload; // array
        state.ids = [];
        state.entities = {};

        for (const o of orders) {
          state.ids.push(o.id);
          state.entities[o.id] = o;
        }

        state.status = "success";
        state.lastUpdated = Date.now();
      })
      .addCase(fetchOrders.rejected, (state, action) => {
        state.status = "error";
        state.error = action.error?.message ?? "Erro desconhecido";
      });
  }
});

export const { orderAdded } = ordersSlice.actions;
export default ordersSlice.reducer;

/** ====== Selectors (encapsulamento) ====== */
const selectOrdersState = (rootState) => rootState.orders;

export const selectOrdersStatus = createSelector(
  [selectOrdersState],
  (orders) => orders.status
);

export const selectOrdersError = createSelector(
  [selectOrdersState],
  (orders) => orders.error
);

// Selector derivado: “lista pronta” para UI
export const selectOrdersList = createSelector(
  [selectOrdersState],
  (orders) => orders.ids.map((id) => orders.entities[id])
);

// Selector parametrizado (exemplo de detalhe por id)
export const makeSelectOrderById = () =>
  createSelector(
    [selectOrdersState, (_rootState, id) => id],
    (orders, id) => orders.entities[id] ?? null
  );

      
Pronto para deixar sua base de Redux ainda mais sólida?

Agora que você tem um modelo arquitetural claro (domínios, slices, side effects e selectors),
continue aprofundando com outros posts em yurideveloper.com focados em padrões práticos e consistência de código.


Ver mais posts


Dica final: mantenha contratos do estado estáveis e deixe UI consumir via selectors — isso sustenta a arquitetura por meses.



“`