React & Frontend

Boas Práticas de Zustand em Profundidade: Slices, Middleware e Persist para Times Ágeis

13 min de leitura

Boas Práticas de Zustand em Profundidade: Slices, Middleware e Persist para Times Ágeis

Introdução ao Zustand e Sua Arquitetura Zustand é uma biblioteca de gerenciamento de estado para JavaScript e React que se destaca pela sua simplicidade, performance e curva de aprendizado suave. Diferentemente de Redux ou MobX, que exigem boilerplate significativo, Zustand oferece uma API minimalista sem perder em funcionalidade. Sua estratégia é baseada em hooks customizados e imutabilidade, permitindo criar stores de forma declarativa com poucas linhas de código. A razão pela qual Zustand ganhou popularidade reside em sua filosofia: ser pequeno (cerca de 2KB), sem dependências externas e extremamente flexível. Você não precisa de actions, reducers ou dispatch explícitos — apenas crie uma função que retorna um objeto com estado e métodos. Isso significa que você pode começar simples e crescer em complexidade conforme necessário, introduzindo Slices, Middleware e Persist apenas quando forem relevantes para seu projeto. Criando Stores e Entendendo Slices O Básico: Sua Primeira Store Uma store Zustand é criada através da função , que recebe um callback.

<h2>Introdução ao Zustand e Sua Arquitetura</h2>

<p>Zustand é uma biblioteca de gerenciamento de estado para JavaScript e React que se destaca pela sua simplicidade, performance e curva de aprendizado suave. Diferentemente de Redux ou MobX, que exigem boilerplate significativo, Zustand oferece uma API minimalista sem perder em funcionalidade. Sua estratégia é baseada em hooks customizados e imutabilidade, permitindo criar stores de forma declarativa com poucas linhas de código.</p>

<p>A razão pela qual Zustand ganhou popularidade reside em sua filosofia: ser pequeno (cerca de 2KB), sem dependências externas e extremamente flexível. Você não precisa de actions, reducers ou dispatch explícitos — apenas crie uma função que retorna um objeto com estado e métodos. Isso significa que você pode começar simples e crescer em complexidade conforme necessário, introduzindo Slices, Middleware e Persist apenas quando forem relevantes para seu projeto.</p>

<h2>Criando Stores e Entendendo Slices</h2>

<h3>O Básico: Sua Primeira Store</h3>

<p>Uma store Zustand é criada através da função <code>create</code>, que recebe um callback. Esse callback espera uma função que retorna um objeto com o estado inicial e os métodos que o modificam. Vamos começar com um exemplo prático de uma store de carrinho de compras:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

const useCartStore = create((set) =&gt; ({

items: [],

total: 0,

addItem: (item) =&gt; set((state) =&gt; ({

items: [...state.items, item],

total: state.total + item.price,

})),

removeItem: (id) =&gt; set((state) =&gt; ({

items: state.items.filter(item =&gt; item.id !== id),

total: state.total - state.items.find(item =&gt; item.id === id)?.price || 0,

})),

clearCart: () =&gt; set({ items: [], total: 0 }),

}));</code></pre>

<p>O parâmetro <code>set</code> é uma função que recebe um novo estado (ou um callback que retorna o novo estado) e o mescla com o estado atual. Note que você é responsável por manter a imutabilidade — Zustand não força isso, mas é a prática correta.</p>

<h3>Entendendo Slices: Organização em Larga Escala</h3>

<p>Quando sua aplicação cresce, manter tudo em uma única store pode ficar desordenado. Aqui entram os <strong>Slices</strong> — padrão de organização que divide a store em pedaços lógicos. A ideia é criar funções que retornam partes do estado e seus métodos, depois combiná-las em uma única store.</p>

<p>Vamos refatorar nosso exemplo usando slices. Primeiro, criamos um slice para o carrinho:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

// Slice: Gerenciamento do Carrinho

const createCartSlice = (set) =&gt; ({

items: [],

total: 0,

addItem: (item) =&gt; set((state) =&gt; ({

items: [...state.items, item],

total: state.total + item.price,

})),

removeItem: (id) =&gt; set((state) =&gt; ({

items: state.items.filter(item =&gt; item.id !== id),

total: state.total - (state.items.find(item =&gt; item.id === id)?.price || 0),

})),

clearCart: () =&gt; set({ items: [], total: 0 }),

});

// Slice: Gerenciamento de Usuário

const createUserSlice = (set) =&gt; ({

user: null,

isAuthenticated: false,

setUser: (user) =&gt; set({ user, isAuthenticated: !!user }),

logout: () =&gt; set({ user: null, isAuthenticated: false }),

});

// Combinando Slices em uma Store Única

const useAppStore = create((set) =&gt; ({

...createCartSlice(set),

...createUserSlice(set),

}));</code></pre>

<p>Essa abordagem oferece várias vantagens: cada slice é isolado logicamente, testável separadamente, e fácil de manter. Quando seu projeto escalou para 50+ estados, essa organização evita que tudo se torne um caos. Você pode até mesmo colocar cada slice em um arquivo separado e importá-los conforme necessário.</p>

<h2>Middleware: Interceptando e Transformando Ações</h2>

<h3>O Que é Middleware no Contexto de Zustand</h3>

<p>Middleware no Zustand funciona de forma similar a outras bibliotecas: é uma camada que intercepta chamadas de <code>set</code> e permite transformar, logar ou validar mudanças de estado. Zustand oferece uma API simplificada para isso através de funções que envolvem a store. O middleware recebe acesso ao estado anterior, à função <code>set</code>, à ação que está sendo realizada e metadados.</p>

<h3>Implementando Seu Primeiro Middleware</h3>

<p>Vamos criar um middleware que registra todas as mudanças de estado — uma prática comum em desenvolvimento:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

// Middleware de Logging

const loggerMiddleware = (config) =&gt; (set, get, api) =&gt;

config(

(args) =&gt; {

console.log(&#039;Estado anterior:&#039;, get());

console.log(&#039;Ação sendo executada:&#039;, args);

set(args);

console.log(&#039;Novo estado:&#039;, get());

},

get,

api

);

// Store com Middleware

const useStore = create(

loggerMiddleware((set) =&gt; ({

count: 0,

increment: () =&gt; set((state) =&gt; ({ count: state.count + 1 })),

decrement: () =&gt; set((state) =&gt; ({ count: state.count - 1 })),

}))

);</code></pre>

<p>Cada vez que você chama <code>increment()</code> ou <code>decrement()</code>, o middleware vai imprimir o estado anterior, a ação e o novo estado. Isso é extremamente útil para debug em desenvolvimento.</p>

<h3>Middleware Mais Complexo: Validação de Estado</h3>

<p>Um caso de uso real é validar se uma mudança de estado é válida antes de permitir:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

// Middleware de Validação

const validatorMiddleware = (config) =&gt; (set, get, api) =&gt;

config(

(args) =&gt; {

// Se for uma função, executa a validação

const newState = typeof args === &#039;function&#039; ? args(get()) : args;

// Valida se count nunca fica negativo

if (newState.count &lt; 0) {

console.warn(&#039;Count não pode ser negativo. Operação rejeitada.&#039;);

return;

}

set(args);

},

get,

api

);

const useCountStore = create(

validatorMiddleware((set) =&gt; ({

count: 0,

increment: () =&gt; set((state) =&gt; ({ count: state.count + 1 })),

decrement: () =&gt; set((state) =&gt; ({ count: state.count - 1 })),

}))

);

// Tentativa de ir para -1 será bloqueada

useCountStore.getState().decrement(); // count = 0

useCountStore.getState().decrement(); // rejeitado, count continua 0</code></pre>

<h3>Combinando Múltiplos Middlewares</h3>

<p>Em projetos reais, você frequentemente precisa de vários middlewares. Zustand permite compor middlewares:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

const loggerMiddleware = (config) =&gt; (set, get, api) =&gt;

config(

(args) =&gt; {

console.log(&#039;Antes:&#039;, get());

set(args);

console.log(&#039;Depois:&#039;, get());

},

get,

api

);

const validatorMiddleware = (config) =&gt; (set, get, api) =&gt;

config(

(args) =&gt; {

const newState = typeof args === &#039;function&#039; ? args(get()) : args;

if (newState.count &lt; 0) {

console.warn(&#039;Validação falhou&#039;);

return;

}

set(args);

},

get,

api

);

// Aplicar middlewares em sequência

const useStore = create(

validatorMiddleware(

loggerMiddleware((set) =&gt; ({

count: 0,

increment: () =&gt; set((state) =&gt; ({ count: state.count + 1 })),

decrement: () =&gt; set((state) =&gt; ({ count: state.count - 1 })),

}))

)

);</code></pre>

<p>Note que a ordem importa — o middleware mais interno é executado primeiro. Neste exemplo, logger executa depois de validator, garantindo que apenas mudanças válidas sejam logadas.</p>

<h2>Persistência de Estado com Persist</h2>

<h3>Por Que Persistir Estado</h3>

<p>Muitas aplicações precisam manter estado entre sessões do usuário. Sem persistência, toda vez que o usuário recarrega a página, ele volta ao estado inicial. Zustand fornece um middleware built-in chamado <code>persist</code> que sincroniza o estado com localStorage (ou qualquer storage que você escolher).</p>

<h3>Configuração Básica do Persist</h3>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

import { persist } from &#039;zustand/middleware&#039;;

const useCartStore = create(

persist(

(set) =&gt; ({

items: [],

total: 0,

addItem: (item) =&gt; set((state) =&gt; ({

items: [...state.items, item],

total: state.total + item.price,

})),

clearCart: () =&gt; set({ items: [], total: 0 }),

}),

{

name: &#039;cart-store&#039;, // Nome da chave no localStorage

}

)

);</code></pre>

<p>Com essa simples configuração, sempre que o estado muda, ele é automaticamente salvo em <code>localStorage</code> com a chave <code>cart-store</code>. Quando a página é recarregada, o estado é restaurado automaticamente. Você não precisa fazer nada além disso — é totalmente transparente.</p>

<h3>Customizando o Storage e Comportamento</h3>

<p>Por padrão, persist usa localStorage, mas você pode usar qualquer storage que siga a interface <code>Storage</code>:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

import { persist } from &#039;zustand/middleware&#039;;

// Usando sessionStorage em vez de localStorage

const useTemporaryStore = create(

persist(

(set) =&gt; ({

sessionData: &#039;inicial&#039;,

setSessionData: (data) =&gt; set({ sessionData: data }),

}),

{

name: &#039;temp-store&#039;,

storage: sessionStorage, // Muda para sessionStorage

}

)

);

// Usando um Storage Customizado (por exemplo, AsyncStorage do React Native)

const customStorage = {

getItem: async (name) =&gt; {

// Implementação para buscar de um storage customizado

return JSON.parse(await AsyncStorage.getItem(name));

},

setItem: async (name, value) =&gt; {

// Implementação para salvar em um storage customizado

await AsyncStorage.setItem(name, JSON.stringify(value));

},

removeItem: async (name) =&gt; {

await AsyncStorage.removeItem(name);

},

};

const useNativeStore = create(

persist(

(set) =&gt; ({

data: [],

addData: (item) =&gt; set((state) =&gt; ({ data: [...state.data, item] })),

}),

{

name: &#039;native-store&#039;,

storage: customStorage,

}

)

);</code></pre>

<h3>Selecionando Quais Partes do Estado Persistir</h3>

<p>Nem sempre você quer persistir todo o estado. Imagine que tem dados sensíveis ou temporários — você pode escolher explicitamente o que salvar:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

import { persist } from &#039;zustand/middleware&#039;;

const useUserStore = create(

persist(

(set) =&gt; ({

user: { name: &#039;João&#039;, email: &#039;joao@email.com&#039; },

password: &#039;&#039;,

rememberMe: true,

setUser: (user) =&gt; set({ user }),

setPassword: (pwd) =&gt; set({ password: pwd }),

setRememberMe: (value) =&gt; set({ rememberMe: value }),

}),

{

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

partialize: (state) =&gt; ({

user: state.user,

rememberMe: state.rememberMe,

// password NÃO será persistido — informação sensível

}),

}

)

);</code></pre>

<p>Com <code>partialize</code>, você fornece uma função que retorna apenas as partes do estado que deseja persistir. Isso é crucial para lidar com informações sensíveis como senhas, tokens ou dados temporários.</p>

<h3>Combinando Persist com Slices</h3>

<p>Agora vamos integrar persist com a estrutura de slices que vimos antes:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

import { persist } from &#039;zustand/middleware&#039;;

const createCartSlice = (set) =&gt; ({

items: [],

total: 0,

addItem: (item) =&gt; set((state) =&gt; ({

items: [...state.items, item],

total: state.total + item.price,

})),

clearCart: () =&gt; set({ items: [], total: 0 }),

});

const createUserSlice = (set) =&gt; ({

user: null,

isAuthenticated: false,

setUser: (user) =&gt; set({ user, isAuthenticated: !!user }),

logout: () =&gt; set({ user: null, isAuthenticated: false }),

});

// Store com Persist, combinando slices

const useAppStore = create(

persist(

(set) =&gt; ({

...createCartSlice(set),

...createUserSlice(set),

}),

{

name: &#039;app-store&#039;,

// Persiste apenas dados relevantes

partialize: (state) =&gt; ({

items: state.items,

total: state.total,

user: state.user,

isAuthenticated: state.isAuthenticated,

}),

}

)

);</code></pre>

<p>Essa abordagem combina a organização limpa dos slices com a persistência automática, criando uma solução robusta e escalável.</p>

<h3>Ciclo de Vida e Eventos do Persist</h3>

<p>Zustand persist oferece hooks de ciclo de vida que permite executar código quando a store é inicializada ou sincronizada:</p>

<pre><code class="language-javascript">import create from &#039;zustand&#039;;

import { persist } from &#039;zustand/middleware&#039;;

const useStore = create(

persist(

(set) =&gt; ({

data: [],

addData: (item) =&gt; set((state) =&gt; ({ data: [...state.data, item] })),

}),

{

name: &#039;my-store&#039;,

onRehydrateStorage: () =&gt; (state, error) =&gt; {

if (error) {

console.error(&#039;Erro ao recuperar estado:&#039;, error);

} else {

console.log(&#039;Estado recuperado com sucesso:&#039;, state);

}

},

}

)

);</code></pre>

<p>O callback <code>onRehydrateStorage</code> é executado quando o estado é restaurado do storage. Isso é útil para validar dados, migrar versões antigas ou sincronizar com um servidor.</p>

<h2>Conclusão</h2>

<p>Zustand oferece uma abordagem diferente e refrescante para gerenciamento de estado: simplicidade sem sacrificar funcionalidade. Os três pilares que exploramos — <strong>Slices para organização escalável</strong>, <strong>Middleware para interceptar e controlar mudanças</strong>, e <strong>Persist para manter estado entre sessões</strong> — formam uma base sólida para construir aplicações React robustas. O grande diferencial é que você não é forçado a usar nada disso desde o início; você começa simples e introduz esses padrões conforme a necessidade surge. Isso torna Zustand ideal tanto para projetos pequenos quanto para aplicações empresariais complexas.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/pmndrs/zustand" target="_blank" rel="noopener noreferrer">Documentação Oficial do Zustand</a></li>

<li><a href="https://docs.pmnd.rs/zustand/api/creating-a-store#middleware" target="_blank" rel="noopener noreferrer">Zustand Middleware Guide</a></li>

<li><a href="https://tkdodo.eu/blog/react-query-meets-react-router" target="_blank" rel="noopener noreferrer">React State Management Comparison</a></li>

<li><a href="https://docs.pmnd.rs/zustand/integrations/persisting-store-data" target="_blank" rel="noopener noreferrer">Zustand Persist Middleware</a></li>

<li><a href="https://www.smashingmagazine.com/2022/09/inline-svg-react-components-nextjs/" target="_blank" rel="noopener noreferrer">Modern JavaScript State Management Patterns</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática
useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática

O Problema Real do useContext em Aplicações Escaláveis Quando iniciamos com R...

Guia Completo de React Scheduler: Prioridades de Renderização e Time Slicing
Guia Completo de React Scheduler: Prioridades de Renderização e Time Slicing

O Scheduler do React: Entendendo o Motor de Renderização O React é uma biblio...

Boas Práticas de Zod com React Hook Form: Schemas Complexos e Erros Customizados para Times Ágeis
Boas Práticas de Zod com React Hook Form: Schemas Complexos e Erros Customizados para Times Ágeis

Introdução: Por Que Zod com React Hook Form? A validação de formulários é um...