¿Qué es Zustand?
Zustand es una biblioteca de manejo de estado para Aplicaciones de React, es muy facil integrarlo es un manjeador de estado más simple que Redux o Context.
Algunas caracteristicas:
- Soporta Typescript
- No usa Providers
- Codigo más consizo que Redux
- Rerender solo en cambios
Configuración del proyecto
Creemos un proyecto usando Vitejs:
npm create vite
name: zustand-tutorial framework: React language: Typescript
cd zustand-tutorial
npm install
code .
Configurando Zustand
Primero instalaremos Zustand:
npm install zustand
crear un archivo en src/store/bookStore.ts, con el siguiente codigo:
import create from "zustand";
interface CounterState {
count: number;
}
export const useCounterStore = create<CounterState>(() => ({
count: 10,
}));
En Zustand no es necesario usar un Provider como en Redux
Luego en App.tsx usar esto:
import { useCounterStore } from "./store/counterStore";
function App() {
const count = useCounterStore((state) => state.count);
return (
<div>
<h1>Counter: {count}</h1>
</div>
);
}
export default App;
Multiples Estados
Ahora vamos a añadir otro estado al store:
interface CounterState {
count: number;
title: string;
}
export const useCounterStore = create<CounterState>(() => ({
title: "Some title",
count: 10,
}));
Luego en App.tsx:
import { useCounterStore } from "./store/counterStore";
function App() {
const count = useCounterStore((state) => state.count);
const title = useCounterStore((state) => state.title);
return (
<div>
<h1>
{title}: {count}
</h1>
</div>
);
}
export default App;
Otra forma tambien de importar multiples estados y copiar multiples valores en un solo objeto para luego destructurarlos:
const { count, title } = useCounterStore((state) => ({
count: state.count,
title: state.title,
}));
Si la funcion que devuelve el objeto con multiples estados crece, es una buena idea separarlo en archivos por aparte
Shallow
Ahora, tambien es posible añadir una funcion extra que viene por parte de zustand.
import shallow from "zustand/shallow";
import { useCounterStore } from "./store/counterStore";
function App() {
const { count, title } = useCounterStore(
(state) => ({
count: state.count,
title: state.title,
}),
shallow
);
La razon de usar la funcion shallow, es porque cuando se importa individualmente los estados de un Store, los cambios de estos son detectados por zustand usando una igualdad estricta (old === new), lo que es muy eficiente para actualizaciones atomicas.
const count = useCounterStore((state) => state.count);
pero cuando no estamos obteniendo actualizaciones atomicas sino objetos o arrays, la comparacion estricta no sera util aqui, ya que lanzara un re-render incluso si el objeto no cambia.
Asi que la funcion Shallow, compara los objetos y array por keys, si estos cambia el hará el re-render.
Actualizando el estado con Actions
Ahora vamos a creare una funcion o Actions que permita actualizar el estado:
interface CounterState {
...
increment: (value: number) => void;
}
export const useCounterStore = create<CounterState>((set) => ({
...
increment: (value: number) =>
set((state) => ({ count: state.count + value })),
}));
Adicionalmente tambien podemos copiar el estado actual, para no sobreescribir otros valores del Store:
increment: (value: number) =>
set((state) => ({ ...state, count: state.count + value })),
Luego en App.tsx:
const { count, title } = useCounterStore(
(state) => ({
count: state.count,
title: state.title,
}),
shallow
);
const { increment } = useCounterStore();
return (
<div>
<h1>
{title}: {count}
</h1>
<button
onClick={() => {
increment(10);
}}
>
Increment by 10
</button>
</div>
);
Código Asíncrono
Tambien es posible ejecutar código asincrono, dentro de estos actions:
import create from "zustand";
export interface Post {
id: number;
title: string;
body: string;
}
interface CounterState {
...
posts: Post[];
getPosts: () => Promise<void>;
}
export const useCounterStore = create<CounterState>((set) => ({
...
posts: [],
getPosts: async () => {
const posts = await (
await fetch("https://jsonplaceholder.typicode.com/posts")
).json();
set((state) => ({ ...state, posts }));
},
}));
Luego en App.tsx:
const { count, title, posts } = useCounterStore(
(state) => ({
count: state.count,
title: state.title,
posts: state.posts,
}),
shallow
);
const { increment, getPosts } = useCounterStore();
useEffect(() => {
getPosts();
}, []);
return (
<div>
{JSON.stringify(posts)}
</div>
);
}
Reemplazar el modelo
tambien la funcion set recibe otro parametro que en lugar de combinar (merge) el estado, los reemplaza.
Se debe tener cuidado al usarlo ya que tambien elimina los actions del Store.
interface CounterState {
...
cleanStore: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
...
cleanStore: () => set({}, true),
}));
Luego en App.tsx:
<button onClick={() => cleanStore()}>clean store</button>
Get
tambien podemos acceder a valores del estado usando la funcion get:
interface CounterState {
multiply: (value: number) => void;
}
export const useCounterStore = create<CounterState>((set, get) => ({
multiply: (value: number) => {
// const count = get().count
const { count } = get();
set({ count: count * value });
},
}));
<button onClick={() => multiply(2)}>multiply by 2</button>