
Manejo de estado global con Zustand en Next.js – Login y persistencia
Zustand es una librería de manejo de estado global para React, ligera y muy poderosa. En este tutorial vamos a integrarla en un proyecto con Next.js (App Router) para:
-
Simular un login con un endpoint mock
-
Guardar y persistir los datos del usuario
-
Proteger rutas
-
Mostrar la información del usuario en un
Header
-
Pasar datos al componente
Slider
Vamos a trabajar directamente sobre un proyecto Next.js ya creado, usando la estructura con
app
,components
,stores
, etc.
Estructura base
Nuestro árbol de carpetas relevante será:
src/
├── app/
│ ├── page.tsx // Login (index)
│ └── dashboard/
│ └── page.tsx // Página interna
├── components/
│ ├── Header.tsx
│ └── Slider.tsx
├── hooks/
│ └── useUserClient.ts
├── mocks/
│ └── user.ts
└── stores/
└── userStore.ts
Instalando Zustand
Si aún no lo tienes instalado:
npm install zustand
Y para persistencia:
npm install zustand/middleware
1. Mock del endpoint de login
Creamos un login simulado en src/mocks/user.ts
:
export async function mockLogin(username: string, password: string) {
await new Promise((r) => setTimeout(r, 500));
if (username === "admin" && password === "1234") {
return {
id: 1,
name: "Luis Velito",
email: "admin@example.com",
role: "admin",
sliderValue: 60,
sliderNegative: true,
steps: 10,
showTooltip: "always",
};
}
throw new Error("Invalid credentials");
}
2. Zustand store con persistencia
Creamos el store en src/stores/userStore.ts
:
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface User {
id: number;
name: string;
email: string;
role: string;
sliderValue: number;
sliderNegative: boolean;
steps: number;
showTooltip: string;
}
interface UserStore {
user: User | null;
login: (data: User) => void;
logout: () => void;
}
export const useUserStore = create<UserStore>()(
persist(
(set) => ({
user: null,
login: (data) => set({ user: data }),
logout: () => set({ user: null }),
}),
{
name: "user-storage", // clave en localStorage
}
)
);
3. Hook useUserClient
para manejar hidratación
En Next.js con SSR, hay que esperar a que Zustand se hidrate. Creamos un hook en src/hooks/useUserClient.ts
:
"use client";
import { useEffect, useState } from "react";
import { useUserStore } from "@/stores/userStore";
export function useUserClient() {
const user = useUserStore((s) => s.user);
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
setIsHydrated(true);
}, []);
return { user, isHydrated };
}
4. Página de login (/
)
Usamos el mock de login y guardamos la sesión con Zustand:
// src/app/page.tsx
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { mockLogin } from "@/mocks/user";
import { useUserStore } from "@/stores/userStore";
import { useUserClient } from "@/hooks/useUserClient";
export default function LoginPage() {
const router = useRouter();
const login = useUserStore((s) => s.login);
const { user } = useUserClient();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
useEffect(() => {
if (user) router.push("/dashboard");
}, [user]);
const handleLogin = async () => {
try {
const loggedUser = await mockLogin(username, password);
login(loggedUser);
} catch (err) {
setError("Credenciales inválidas: " + err);
}
};
return (
<div className="flex items-center justify-center min-h-screen min-w-screen bg-gray-200">
<div className="bg-white p-10 rounded-lg shadow-md text-gray-700">
<h1 className="text-2xl font-bold mb-4">Iniciar sesión</h1>
<input
className="border p-2 w-full mb-2"
placeholder="Usuario"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
className="border p-2 w-full mb-4"
type="password"
placeholder="Contraseña"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button className="bg-black text-white px-4 py-2" onClick={handleLogin}>
Entrar
</button>
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
</div>
);
}
5. Página interna /dashboard
Protegida, con redirección si no hay sesión:
// src/app/dashboard/page.tsx
"use client";
import Header from "@/components/Header";
import Slider from "@/components/Slider";
import { useUserClient } from "@/hooks/useUserClient";
export default function DashboardPage() {
const { user, isHydrated } = useUserClient();
if (!isHydrated) return null;
if (!user) return null;
return (
<div className="min-h-screen min-w-screen bg-gray-200">
<Header />
<div className="flex items-center justify-center container mx-auto p-4">
<div className="bg-white p-8 rounded-lg shadow-md min-w-[600px]">
<h2 className="text-xl font-semibold mb-12 text-gray-700">Slider personalizado</h2>
<Slider
value={user.sliderValue}
negative={user.sliderNegative}
steps={user.steps}
showTooltip={user.showTooltip as "always" | "onInteraction" | undefined}
/>
</div>
</div>
</div>
);
}
5. Componente Header con cierre de sesión
Código del componente header:
// src/components/Header.tsx
"use client";
import { useUserClient } from "@/hooks/useUserClient";
import { useUserStore } from "@/stores/userStore";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Header() {
const { user, isHydrated } = useUserClient();
const logout = useUserStore((s) => s.logout);
const router = useRouter();
useEffect(() => {
if (isHydrated && !user) {
router.push("/");
}
}, [user, isHydrated]);
if (!isHydrated || !user) return null;
return (
<header className="flex justify-between items-center p-4 bg-gray-100 rounded-md shadow">
<div>
<p className="text-sm text-gray-700">Bienvenido,</p>
<p className="font-bold text-gray-900 text-lg">{user.name}</p>
</div>
<div className="flex flex-col items-end">
<p className="text-sm text-gray-600">{user.email}</p>
<p
className="mt-4 text-red-500 cursor-pointer text-xs hover:text-red-600 uppercase"
onClick={() => {
logout();
router.push("/");
}}
>
Cerrar sesión
</p>
</div>
</header>
);
}
Resultado final
-
✅ Login simulado con Zustand
-
✅ Datos persistentes entre recargas
-
✅ Página protegida con redirección
-
✅ Header con sesión e información
-
✅ Slider dinámico con props desde el store
Conclusión
Zustand es ideal para manejar sesiones de usuario en aplicaciones React y Next.js, sin el peso de soluciones más complejas. Con persist
, una estructura clara y algunos hooks de cliente, puedes construir experiencias sólidas y modernas fácilmente.
-- ¿Quieres recibir mi newsletter? --
No te pierdas de aprender:
Mi newsletter mensual viene con una dosis de inspiración, recursos para descargar, consejos de desarrollo rápidos y los mismos recursos que aprendo.