Conheça o Zustand - Gerenciamento de Estado Simplificado para Aplicativos React
- #React
Introdução
Em aplicações React o gerenciamento de estados é importante independente de quão escalonável seu projeto é. Para facilitar esse gerenciamento, o React atualmente tem muitas bibliotecas, o mais famoso é o Redux.
No decorrer do tempo foram surgindo outras bibliotecas para ajudar no gerenciamento, e o Zustand é umas delas.
O objetivo desse artigo é apresentar o Zustand e como pode ser interessante a adição dele em um projeto principalmente pela rapidez, por ser pequeno e escalonável.
Começando com o Zustand
Como o Zustand é uma biblioteca javascript nós precisamos de algumas ferramentas básicas.
- NodeJs - Para instalar o NodeJs basta acessar o link e realizar o donwload.
- npm ou yarn - Os dois são gerenciadores de pacotes para o Node, o npm já vem incluído quando o Node é instalado, más se tiver preferencia pelo yarn, pode rodar o comando npm i yarn -g para instalar globalmente.
Então vamos começar com um projeto simples:
Para a criação desse projeto eu vou usar o Vite que é basicamente uma ferramenta de construção que fornece umas melhor experiência no desenvolvimento.
Executar o comando no terminal:
Com NPM:
npm create vite@latest
Com YARN:
yarn create vite
Ao executar o comando devemos dar nome ao projeto.
Project name >> DIO-Zustand
Package name >> dio-zustand
O Vite apresenta algumas opções de framework, Nesse caso iremos escolher o React apenas com Javascript.
Select a framework >> React
Select a variant >> Javascript
E agora executamos em sequência os comandos:
cd DIO-Zustand
npm install
Agora devemos instalar o Zustand
Com NPM:
npm install zustand
Com YARN:
yarn add zustand
Digite a linha de comando abaixo para executar o projeto
npm run dev
Para acessar o servidor local basta segurar o ctrl e clicar no link localhost:
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
Sua aplicação no navegador será assim:
Agora vamos fazer algumas modificações para facilitar.
Abra o componente App.jsx dentro da pasta src e deixe-a igual o código abaixo:
import {useState} from "react"
import "./App.css"
function App() {
const [count,setCount] = useState(0)
return (
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
)
}
export default App
Vamos criar uma dentro da pasta src uma pasta chamada store, e dentro dessa pasta criar um arquivo chamada useCountStore.js
A estrutura da pasta src ficará assim:
src/
┣ assets/
┣ store/
┃ ┗ useCountStore.js
┣ App.css
┣ App.jsx
┣ index.css
┗ main.jsx
Agora vamos construir a nossa store:
import { create } from "zustand"
const useCountStore = create((set) => ({
count: 0
}));
export default useCountStore
A função create passa a função set como callback e é ela que é responsável pelo atualização do estado dentro da store.
Agora vamos modificar o componente App.jsx:
Importamos a store dentro do <App/> e para acessar o estado, nós criamos uma variável que recebe o useCountStore hook e usa uma função de callback que retorna um pedaço do estado da store.
import useCountStore from "./store/useCountStore";
import "./App.css";
function App() {
const count = useCountStore((state) => state.count);
return (
<div className="card">
<button>count is {count}</button>
</div>
);
}
export default App;
Agora precisamos atualizar este estado:
Dentro do useCountStore.js nós devemos acrescentar novos estados que irão usar a função set() para atualizar o estado de forma imutável.
import { create } from "zustand"
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({count:state.count + 1})),
decrement: () => set((state) => ({count:state.count - 1})),
}));
export default useCountStore
Agora vamos atualizar o <App/>.
import useCountStore from "./store/useCountStore";
import "./App.css";
function App() {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
return (
<div className="card">
<button onClick={decrement}>Decrement</button>
<p>{count}</p>
<button onClick={incremente}>Increment<button>
</div>
);
}
export default App;
O componente será renderizado quando o estado for alterado.
Podemos também alterar o estado de um componente de fora dele.
Vamos criar uma pasta chamada components dentro da pasta src. Dentro dessa pasta vamos criar um componente chamado Counter.jsx e importar a useCountStore.
import React from "react"
import useCountStore from "../store/useCountStore"
export const Counter = () => {
const count = useCountStore((state) => state.count);
return <div>{count}</div>;
};
Agora vamos importar o <Counter/> no <App/>.
import useCountStore from "./store/useCountStore";
import Counter from "./components/Counter";
import "./App.css";
function App() {
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
return (
<div className="card">
<button onClick={decrement}>Decrement</button>
<Counter/>
<button onClick={incremente}>Increment<button>
</div>
);
}
export default App;
Assim nós não precisamos passar o count como prop e todo click que ocorrer nos botões o estado será alterado e apenas o <Counter/> será re-renderizado.
Evitando o Prop Drilling
Prop Drilling é basicamente quando passamos as props de um componente pai para um componente filho e esse componente passa essas props para o seu filho e assim por diante.
Para simular essa situação dentro da pasta components devemos criar os seguintes componente, Container.jsx, Card.jsx, Message.jsx.
App.jsx
import { useState } from "react";
import "./App.css";
import { Container } from "./components/Container";
import { Counter } from "./components/Counter";
function App() {
const [count, setCount] = useState(0);
return (
<>
<Counter count={count} />
<Container count={count} setCount={setCount} />
</>
);
}
export default App;
Container.jsx
import React from "react";
import { Card } from "./Card";
export const Container = ({ count, setCount }) => {
return (
<div className="zone-container">
<h1>Container</h1>
<Card count={count} setCount={setCount} />
</div>
);
};
Card.jsx
import React from "react";
import { Message } from "./Message";
export const Card = ({ count, setCount }) => {
return (
<div className="zone-container">
<h2>Card</h2>
<Message count={count} setCount={setCount} />
</div>
);
};
Message.jsx
import React from "react";
export const Message = ({ count, setCount }) => {
const increment = () => {
setCount((prev) => prev + 1);
};
const decrement = () => {
setCount((prev) => prev + 1);
};
return (
<div className="zone-container">
<h4>Message counter: {count}</h4>
<button onClick={decrement}>decrement</button>
<button onClick={increment}>increment</button>
</div>
);
};
Counter.jsx
import React from "react";
export const Counter = ({ count }) => {
return (
<div className="zone-container">
<h3>Contador</h3>
<h1>{count}</h1>
</div>
);
};
A pasta src ficará com esta estrutura:
src/
┣ assets/
┣ components/
┃ ┣ Card.jsx
┃ ┣ Container.jsx
┃ ┣ Counter.jsx
┃ ┗ Message.jsx
┣ store/
┃ ┗ useCountStore.js
┣ App.css
┣ App.jsx
┣ index.css
┗ main.jsx
Neste caso nós estamos passando count e setCount como prop para o <Container/> depois para o <Card/> e por fim para o <Message/> onde são finalmente utilizados.
Isso causa renderizações desnecessárias quando o estado é alterado.
Para evitar estes comportamentos nós podemos usar o Zustand, importando a store dentro dos componentes que precisam dos estados.
Os componente ficarão assim:
App.jsx
import "./App.css";
import { Container } from "./components/Container";
import { Counter } from "./components/Counter";
function App() {
return (
<>
<Counter />
<Container />
</>
);
}
export default App;
Container.jsx
import React from "react";
import { Card } from "./Card";
export const Container = () => {
return (
<div className="zone-container">
<h1>Container</h1>
<Card />
</div>
);
};
Card.jsx
import React from "react";
import { Message } from "./Message";
export const Card = () => {
return (
<div className="zone-container">
<h2>Card</h2>
<Message />
</div>
);
};
Message.jsx
import React from "react";
import useCountStore from "../store/useCountStore";
export const Message = () => {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
return (
<div className="zone-container">
<h4>Message counter: {count}</h4>
<button onClick={decrement}>decrement</button>
<button onClick={increment}>increment</button>
</div>
);
};
Counter.jsx
import React from "react";
import useCountStore from "../store/useCountStore";
export const Counter = () => {
const count = useCountStore((state) => state.count);
return (
<div className="zone-container">
<h3>Contador</h3>
<h1>{count}</h1>
</div>
);
};
Então o fluxo da aplicação será, onde será re-renderizado apenas os componentes <Counter/> e <Message/>:
Neste caso nós poderiamos usar o context API do react, mas o Zustand oferece algumas vantagens como:
- Menos boilerplate.
- Renderização de componentes só quando os estados forem alterados.
- Gerenciamento de estado centralizado e baseado em ação.
Conclusão
O Zustand tem muito mais a oferecer, como funções assíncronas, alguns middlewares, suporte ao typescript, testes e Persisting Store.
Essa foi uma breve amostra de como o Zustand é fácil e divertido de usar para tarefas que às vezes pode parecer intimidadora.