Arquitetura do Zustand: Guia Completo para Dominá-la e Otimizar Seu Estado no React

Arquitetura do Zustand: Guia Completo para Dominá-la e Otimizar Seu Estado no React

“`html




Dominando a arquitetura de Zustand

yurideveloper.com.br
Guia prático

Dominando a arquitetura de Zustand

Quando o store cresce, o problema não é “falta de Zustand”. É falta de arquitetura:
fronteiras claras, estado coeso, ações previsíveis e seletor com performance sustentada.

1) Modelando o domínio: estado coeso e responsabilidades separadas

A primeira decisão arquitetural é o que vira estado e como agrupar o que pertence junto.
Eu penso em Zustand como um runtime de UI state: coisas que mudam ao longo do fluxo do usuário e precisam ser compartilhadas.

Regra que eu sigo:
se uma informação não muda (ou não precisa ser compartilhada), ela não entra no store.
Se muda, entra. E se muda junto, fica junto.
  • Coesão: agrupe por caso de uso (ex.: carrinho, autenticação, filtros), não por “tipos” (ex.: só arrays, só flags).
  • Imutabilidade conceitual: mesmo usando mutações “internas” com immer (opcional), mantenha a intenção clara:
    atualize de forma determinística e traceável.
  • Derivações: prefira selectors para computar valores derivados ao invés de duplicar estado.
Entra no store
Não precisa no store

Dados do usuário que afetam renderização
Detalhes de implementação que podem ficar local

Flags de fluxo (loading, erro, step atual)
Regras puras (cálculos estáticos sem dependência do estado)

2) Estruturando slices: quando (e como) dividir seu store

Assim que você cruza o limite do “store pequeno”, a melhor forma de manter clareza é dividir por slices.
No Zustand moderno, isso costuma ser feito compondo o store com funções e tipando as interfaces.

1

Crie um slice por domínio (ex.: auth, checkout, ui).

Um slice deve ter seu próprio estado + suas ações, reduzindo acoplamento.

2

Combine no store “raiz”.

O store final vira um “sanduíche”: ações e estado de vários slices em um único ponto de acesso.

3

Evite ações globais genéricas demais.

Prefira ações que refletem o domínio: signIn, applyCoupon, setFilters.

4

Tipagem primeiro.

Interfaces bem definidas evitam que o store vire um “saco de propriedades”.

O que eu tento evitar:
chamar ações de um slice dentro de outro sem intenção. Se precisar, passe por funções “de orquestração”
(ex.: em um slice de use cases) para manter rastreabilidade.

3) Selectors e performance: o padrão que mantém a UI estável

A arquitetura também é como o estado flui até a renderização. O objetivo é simples:
cada componente deve re-renderizar apenas quando o que ele consome muda.

  • Selecione por “pedaços”: use selectors que retornem somente o dado necessário.
    Evite selecionar o store inteiro.
  • Estabilidade: evite criar objetos/arrays novos no selector sem necessidade.
    Se precisar, aplique memorização (ou derive valores em camadas apropriadas).
  • Funções de ação: em geral, ações podem ser selecionadas diretamente para manter referência estável.

Quando a UI começa a “piscar”, geralmente é porque um componente está assinando mudanças demais.
A correção é arquitetural: ajustar seletor e, se for o caso, reorganizar slices para reduzir dependências cruzadas.

Checklist rápido:

  • O componente está assinando somente o estado que ele realmente renderiza?
  • Os selectors retornam primitivas/estruturas estáveis?
  • Você está evitando “selector gigante” que mistura domínios diferentes?

4) Fluxos previsíveis: ações, async e consistência do estado

Um store bem arquitetado tem ações com semântica clara.
Em especial, para operações assíncronas, eu model;o o ciclo completo:
iniciar → sucesso → falha → finalizar.

A

Estado de execução: crie campos explícitos para status, error e/ou loading.

Isso elimina “gambiarras” como inferir loading por ausência de dados.

B

Atualizações atômicas: minimize janelas inconsistentes.

Se a UI precisa mudar em conjunto (ex.: limpar erro + setar dados), faça em uma sequência bem definida.

C

Orquestração de efeitos: se um fluxo envolve múltiplos domínios, concentre a coordenação em uma camada.

Assim você evita que cada slice “saia” do seu território.

Conseqüência direta:
seu store vira uma fonte de verdade com transições legíveis,
e debugging fica muito mais rápido.

Exemplo: store composto por slices + ações async com estado previsível

Abaixo eu monto um desenho típico para crescer com segurança:
slices separados, ações tipadas, e status/erro padronizados para async.

TypeScript + Zustand

import { create } from "zustand";

type AsyncStatus = "idle" | "loading" | "success" | "error";

type AuthState = {
  status: AsyncStatus;
  error: string | null;
  userId: string | null;
};

type AuthActions = {
  signIn: (email: string, password: string) => Promise<void>;
  signOut: () => void;
};

type FiltersState = {
  search: string;
  statusFilter: "all" | "open" | "closed";
};

type FiltersActions = {
  setSearch: (value: string) => void;
  setStatusFilter: (value: FiltersState["statusFilter"]) => void;
};

const createAuthSlice = (set: any): AuthState & AuthActions => ({
  status: "idle",
  error: null,
  userId: null,

  signIn: async (email, password) => {
    set({ status: "loading", error: null });

    try {
      // Exemplo: substitua por sua chamada real
      const result = await fakeRequestSignIn(email, password);

      set({
        status: "success",
        userId: result.userId,
        error: null,
      });
    } catch (err: any) {
      set({
        status: "error",
        error: err?.message ?? "Falha ao fazer login",
      });
    }
  },

  signOut: () => {
    set({
      status: "idle",
      userId: null,
      error: null,
    });
  },
});

const createFiltersSlice = (set: any): FiltersState & FiltersActions => ({
  search: "",
  statusFilter: "all",

  setSearch: (value) => set({ search: value }),
  setStatusFilter: (value) => set({ statusFilter: value }),
});

type AppStore = (AuthState & AuthActions) & (FiltersState & FiltersActions);

export const useAppStore = create<AppStore>((set, get) => ({
  ...createAuthSlice(set),
  ...createFiltersSlice(set),
}));

async function fakeRequestSignIn(email: string, password: string) {
  await new Promise((r) => setTimeout(r, 600));
  if (!email.includes("@") || password.length < 6) {
    throw new Error("Credenciais inválidas");
  }
  return { userId: "u_123" };
}

// Exemplo de uso em componente:
// const userId = useAppStore(s => s.userId);
// const signIn = useAppStore(s => s.signIn);

Dica prática: mantenha seus slices com dependências mínimas.
Se um slice precisa do outro, prefira resolver isso em uma ação “de orquestração” (um use case)
ao invés de misturar responsabilidades.

Pronto pra deixar sua base ainda mais sólida?

Agora que você já tem uma arquitetura consistente para Zustand, o próximo passo é evoluir padrões
de componentes, seletors e organização de código no projeto.



“`