Overview
The Auth Dashboard uses Zustand for state management. Zustand is a lightweight, fast, and scalable state management solution that uses React hooks.
Zustand was chosen for its simplicity, minimal boilerplate, and excellent TypeScript support compared to Redux or Context API.
Why Zustand?
Minimal Boilerplate No providers, actions, or reducers needed. Just create a store and use it.
TypeScript First Excellent TypeScript inference with minimal type annotations.
Persistence Built-in Easy persistence with the persist middleware.
No Re-render Issues Fine-grained subscriptions prevent unnecessary re-renders.
Store Architecture
Each feature has its own dedicated store:
src/
├── features/
│ ├── auth/
│ │ └── authStore.tsx # Authentication state
│ └── users/
│ └── usersStore.ts # Users management state
└── shared/
└── store/
├── useToastStore.ts # Global toast notifications
└── useSettingsStore.ts # App settings (theme, language)
Creating a Store
Basic Store Pattern
Here’s the pattern used throughout the application:
import { create } from "zustand" ;
interface MyState {
// State properties
data : MyData [];
loading : boolean ;
error : string | null ;
// Actions
fetchData : () => Promise < void >;
updateData : ( item : MyData ) => void ;
}
export const useMyStore = create < MyState >(( set , get ) => ({
// Initial state
data: [],
loading: false ,
error: null ,
// Actions
fetchData : async () => {
set ({ loading: true , error: null });
try {
const result = await apiCall ();
set ({ data: result , loading: false });
} catch ( error ) {
set ({ error: error . message , loading: false });
}
},
updateData : ( item ) => set (( state ) => ({
data: state . data . map ( d => d . id === item . id ? item : d )
})),
}));
Authentication Store
The auth store (features/auth/authStore.tsx:7-38) manages user authentication state:
import { create } from "zustand" ;
import { persist } from "zustand/middleware" ;
import { loginRequest } from "./authService" ;
import type { AuthState } from "./types" ;
export const useAuthStore = create < AuthState >()(\ n persist (
( set ) => ({
user: null ,
loading: false ,
login : async ( username , password ) => {
try {
set ({ loading: true });
const data = await loginRequest ({ username , password });
set ({ user: data , loading: false });
localStorage . setItem ( "token" , data . token );
} catch ( error ) {
set ({ loading: false });
console . error ( error );
alert ( "Credenciales incorrectas" );
}
},
logout : () => {
localStorage . removeItem ( "token" );
set ({ user: null });
},
}),
{ name: "auth-storage" }
)
);
Using the Auth Store
import { useAuthStore } from "@/features/auth/authStore" ;
function LoginPage () {
const login = useAuthStore (( state ) => state . login );
const loading = useAuthStore (( state ) => state . loading );
const handleSubmit = async ( e ) => {
e . preventDefault ();
await login ( username , password );
};
return (
< form onSubmit = { handleSubmit } >
{ /* form fields */ }
< button disabled = { loading } >
{ loading ? "Logging in..." : "Login" }
</ button >
</ form >
);
}
The auth store uses the persist middleware to save authentication state to localStorage with the key auth-storage.
Users Store
The users store (features/users/usersStore.ts:18-66) demonstrates a more complex state management pattern:
import { create } from "zustand" ;
import { persist } from "zustand/middleware" ;
import { getUsers } from "./usersService" ;
import type { User } from "./types" ;
interface UsersState {
users : User [];
loading : boolean ;
error : string | null ;
deletingId : number | null ;
fetchUsers : () => Promise < void >;
addUser : ( user : User ) => void ;
deleteUser : ( id : number ) => void ;
updateUser : ( user : User ) => void ;
}
export const useUsersStore = create < UsersState >()(\ n persist (
( set , get ) => ({
users: [],
loading: false ,
error: null ,
deletingId: null ,
fetchUsers : async () => {
// Skip if already loaded
if ( get (). users . length > 0 ) return ;
try {
set ({ loading: true , error: null });
const usersFromApi = await getUsers ();
set ({ users: usersFromApi , loading: false });
} catch {
set ({ error: "Error fetching users" , loading: false });
}
},
addUser : ( user ) =>
set (( state ) => ({
users: [ user , ... state . users ],
})),
deleteUser : ( id ) => {
set ({ deletingId: id });
set (( state ) => ({
users: state . users . filter (( user ) => user . id !== id ),
deletingId: null ,
}));
},
updateUser : ( updatedUser : User ) =>
set (( state ) => ({
users: state . users . map (( user ) =>
user . id === updatedUser . id ? updatedUser : user
),
})),
}),
{ name: "users-storage" }
)
);
Key Patterns
The addUser and updateUser actions update the local state immediately without waiting for API confirmation. This provides a snappy user experience. addUser : ( user ) =>
set (( state ) => ({
users: [ user , ... state . users ],
})),
Track multiple loading states for different operations: loading : false , // General loading
deletingId : null , // Specific item being deleted
The fetchUsers function checks if data is already loaded before making an API call: fetchUsers : async () => {
if ( get (). users . length > 0 ) return ;
// ... fetch from API
}
Using get() for Current State
The get function provides access to the current state within actions: create < State >()(( set , get ) => ({
myAction : () => {
const currentUsers = get (). users ;
// ... use currentUsers
}
}))
Global Stores
Toast Store
Simple store for showing temporary notifications (shared/store/useToastStore.ts:9-13):
import { create } from "zustand" ;
interface ToastState {
message : string | null ;
show : ( msg : string ) => void ;
hide : () => void ;
}
export const useToastStore = create < ToastState >(( set ) => ({
message: null ,
show : ( msg ) => set ({ message: msg }),
hide : () => set ({ message: null }),
}));
Usage:
import { useToastStore } from "@/shared/store/useToastStore" ;
function MyComponent () {
const showToast = useToastStore (( state ) => state . show );
const handleSuccess = () => {
showToast ( "Operation successful!" );
};
}
Settings Store
Manages app-wide settings like theme and language preferences:
import { create } from "zustand" ;
import { persist } from "zustand/middleware" ;
interface SettingsState {
theme : "light" | "dark" ;
language : "en" | "es" ;
setTheme : ( theme : "light" | "dark" ) => void ;
setLanguage : ( language : "en" | "es" ) => void ;
}
export const useSettingsStore = create < SettingsState >()(\ n persist (
( set ) => ({
theme: "light" ,
language: "en" ,
setTheme : ( theme ) => set ({ theme }),
setLanguage : ( language ) => set ({ language }),
}),
{ name: "settings-storage" }
)
);
Using Stores in Components
Selector Pattern
Select only the state you need to prevent unnecessary re-renders: function UserList () {
// Only re-renders when users array changes
const users = useUsersStore (( state ) => state . users );
const fetchUsers = useUsersStore (( state ) => state . fetchUsers );
useEffect (() => {
fetchUsers ();
}, []);
return < div > { /* render users */ } </ div > ;
}
Don’t select the entire state unless you need it: // ❌ Re-renders on ANY state change
const state = useUsersStore ();
Multiple Selectors
function UserStats () {
const users = useUsersStore (( state ) => state . users );
const loading = useUsersStore (( state ) => state . loading );
const error = useUsersStore (( state ) => state . error );
if ( loading ) return < Spinner /> ;
if ( error ) return < Error message = { error } /> ;
return < div > Total users: { users . length } </ div > ;
}
Calling Actions
function AddUserButton () {
const addUser = useUsersStore (( state ) => state . addUser );
const handleClick = () => {
addUser ({
id: Date . now (),
name: "New User" ,
email: "[email protected] "
});
};
return < button onClick = { handleClick } > Add User </ button > ;
}
Persist Middleware
The persist middleware automatically saves and restores state from localStorage.
Configuration
import { persist } from "zustand/middleware" ;
export const useMyStore = create < MyState >()(\ n persist (
( set , get ) => ({
// ... store implementation
}),
{
name: "my-storage" , // localStorage key
partialize : ( state ) => ({ // Optional: only persist specific fields
users: state . users ,
}),
}
)
);
Sensitive data like tokens should be stored in localStorage separately, not in persisted Zustand stores, for better security control.
Best Practices
One Store Per Feature Keep stores focused on a single domain. Don’t create a global store for everything.
Use Selectors Always use selectors to pick specific state slices. This prevents unnecessary re-renders.
Async in Actions Handle async operations inside store actions, not in components.
TypeScript Types Define TypeScript interfaces in separate types.ts files for reusability.
Do’s and Don’ts
// ✅ Select specific state
const user = useAuthStore (( state ) => state . user );
// ✅ Handle errors in the store
try {
const data = await api ();
set ({ data });
} catch ( error ) {
set ({ error: error . message });
}
// ✅ Use functional updates for state that depends on previous state
set (( state ) => ({
count: state . count + 1
}));
// ❌ Don't select entire state
const state = useAuthStore ();
// ❌ Don't handle API calls in components
const users = useUsersStore (( state ) => state . users );
useEffect (() => {
api . get ( '/users' ). then ( data => /* update store */ );
}, []);
// ❌ Don't mutate state directly
state . users . push ( newUser ); // Wrong!
Testing Stores
Stores are easy to test since they’re just JavaScript functions:
import { renderHook , act } from "@testing-library/react" ;
import { useUsersStore } from "./usersStore" ;
describe ( "useUsersStore" , () => {
beforeEach (() => {
// Reset store before each test
useUsersStore . setState ({ users: [], loading: false });
});
it ( "should add a user" , () => {
const { result } = renderHook (() => useUsersStore ());
act (() => {
result . current . addUser ({
id: 1 ,
name: "Test User" ,
email: "[email protected] "
});
});
expect ( result . current . users ). toHaveLength ( 1 );
expect ( result . current . users [ 0 ]. name ). toBe ( "Test User" );
});
});
Next Steps
Architecture Learn about the overall application architecture
API Integration See how stores integrate with API services
Components Use stores in shared components
Zustand Docs Official Zustand documentation