State management - zustand
abcd
React is a component-based library, there will be data in your application that you need to access across multiple unrelated components at different nesting levels. For example, imagine you're building an e-commerce application and you need shopping cart data in many places - the navbar needs to show a cart icon with the item count badge, each product card needs an "Add to Cart" button that updates the cart, the sidebar shows a mini cart preview, the checkout page displays full cart details, and the footer shows an order summary. Without a state management solution like Zustand, you'd have to store the cart data in your top-level App component and then pass it down through every single layer of components, even those that don't actually need the cart data but are just in the middle of the component tree. This is called prop drilling and it becomes extremely messy and hard to maintain as your app grows.
State management is absolutely a necessary component in building a large scale applications. There are multiple solutions like Redux Toolkit or ContextAPI is available to solve this purpose. But in some cases they lack the simplicity.
Zustand is lightweight state management library that offers simple and flexible approach to efficiently manage states in applications. Think zustand as your global storage box. You can read the data in any component of your application
Today we will build a state management for handling user data. Assume you are building an ecommerce website. You need `username` in multiple places. We will store the data i.e. user data using zustand and can use it in any component we want for example profile page, cart page, navbar, etc.
Enough of motivations. Lets get started with the code..
Installation
npm i zustand
Create Your Store
import { create } from 'zustand';
`create` is used for creating the store in your application where you will store your data.
interface User {
id?: string;
name?: string;
email?: string;
avatar?: string;
}
interface UserStore {
user: User | null;
setUser: (user: User | null) => void;
}
- TypeScript interface defining what a User looks like
user: The actual data (state)setUser: An action to update the user
export const useUserStore = create<UserStore>((set, get) => ({
user: null,
setUser: (user) => set(() => ({ user}))
}));
export const useUserStore = create<UserStore>((set, get) => ({: We are creating a hook named `useUserStore` to use any in components to get access the data or perorm any action on the datauser: null: we are initializing our data as null in the beginningset: is used to UPDATE the statesetUser: (user) => set({ user }): This is an action to update the user written as an arrow function. It takes `user` parameter and update the store with this new user usingset(() => ({ user}))
Different ways to write `set`:
Let me give an example with more state so that the idea of writing `set` in different ways becomes clear.
export const useUserStore = create<UserStore>((set, get) => ({
// STATE (Data)
user: null, // user data
cartCount: 0, // get how many items are in the cart
isLoading: false,
// ACTIONS (Functions to modify state)
setUser: (user) => set({ user }),
incrementcartCount: () => set((state) => ({
cartCount: state.cartCount + 1
})),
setLoading: (loading) => set({ isLoading: loading }),
// GETTERS (Read-only computed values)
getTotalcartCount: () => get().cartCount * 2,
}))
Method 1: Direct Update
setUser: (user) => set({ user }) // Just replace the value directly.
Method 2 : Update the value using current state
incrementCount: () => set((state) => ({
count: state.cartCount + 1 // updating the value using current state as state.count
}))
Method 3: Update Multiple Values
updateUserAndCount: (user, count) => set({
user: user,
cartCount: cartCount,
isLoading: false
}). // update multiple values at once
How you can use store in components
There are multiple ways to do so.
Method 1: Get Everything
const { user, setUser } = useUserStore()
But this hs one problem. Component re-renders even if any other value in the store changes. Therefore it s better to access only those values or functions which is needed.
Method 2 : Select specific values
const user = useUserStore((state) => state.user)
const setUser = useUserStore((state) => state.setUser)
- Call the hook
useUserStore (state): selector function receives the full state- select only the value which is needed
state.userorstate.setUser
Custom component using the store
For handling the user which will change upon sign-in or logout we write a custom component that updates the store whenever such user actions happens. Therefore this function returns null.
export function UserSync() {
const { isSignedIn, user, isLoaded } = useUser(); // Clerk hook to get user data, user signed in status you can use your own hooks to get these data
const setUser = useUserStore((state) => state.setUser); // Zustand action
useEffect(() => {
if (isLoaded) {
if (isSignedIn && user) {
setUser({ // Update Zustand store
id: user.id,
name: user.firstName ?? undefined,
email: user.primaryEmailAddress?.emailAddress,
avatar: user.imageUrl,
});
} else {
setUser(null); // setUser as null
}
}
}, [isSignedIn, user, isLoaded, setUser]);
return null;
}
- Clerk provides user data via
useUserhook - Get the Zustand action
setUserfrom store - Watch for changes using useEffect
- When Clerk user changes:
- If user logged in → Update Zustand with user data
- If user logged out → Set Zustand user to
null
We also need to add this into your main entry point of your application so that whole app has an access to it. For example i added this to main layout.tsx file in a NextJS project.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<NavBar />
<UserSync /> // here you add your user data controlling component
{children}
<GoogleTagManager gtmId="Your GTM ID" />
<Toaster position="top-right" />
</body>
</html>
</ClerkProvider>
);
}
Example: Profile Page
Now you can write a profile page component with the use of data from your store
'use client';
import React from 'react'
import { useUserStore } from '@/lib/store'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Mail, User, IdCard, Shield } from 'lucide-react'
function ProfileData() {
const user = useUserStore((state) => state.user);
// Get initials for avatar fallback
const getInitials = (name?: string) => {
if (!name) return 'U';
return name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
};
if (!user) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Card className="w-full max-w-md">
<CardContent className="pt-6">
<p className="text-center text-neutral-500">No user data available</p>
</CardContent>
</Card>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8 md:py-12">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header Card with Avatar */}
<Card className="border-neutral-200 shadow-sm">
<CardHeader className="pb-4">
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6">
<Avatar className="w-24 h-24 sm:w-28 sm:h-28 border-4 border-neutral-100 shadow-md">
<AvatarImage src={user.avatar} alt={user.name || 'User'} />
<AvatarFallback className="bg-neutral-200 text-neutral-700 text-2xl font-semibold">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="flex-1 text-center sm:text-left space-y-2">
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<CardTitle className="text-3xl font-bold text-neutral-900">
{user.name || 'User'}
</CardTitle>
<Badge variant="secondary" className="bg-neutral-100 text-neutral-700 hover:bg-neutral-200 w-fit mx-auto sm:mx-0">
Active
</Badge>
</div>
<CardDescription className="text-neutral-600 text-base">
Member Profile
</CardDescription>
</div>
</div>
</CardHeader>
</Card>
{/* Profile Information Card */}
<Card className="border-neutral-200 shadow-sm">
<CardHeader>
<CardTitle className="text-xl font-semibold text-neutral-900">
Profile Information
</CardTitle>
<CardDescription className="text-neutral-600">
Your personal details and account information
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Full Name */}
<div className="flex items-start gap-4">
<div className="mt-1 p-2 rounded-lg bg-neutral-100">
<User className="w-5 h-5 text-neutral-600" />
</div>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium text-neutral-500">Full Name</p>
<p className="text-base font-semibold text-neutral-900">
{user.name || 'Not provided'}
</p>
</div>
</div>
<Separator className="bg-neutral-200" />
{/* Email */}
<div className="flex items-start gap-4">
<div className="mt-1 p-2 rounded-lg bg-neutral-100">
<Mail className="w-5 h-5 text-neutral-600" />
</div>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium text-neutral-500">Email Address</p>
<p className="text-base font-semibold text-neutral-900 break-all">
{user.email || 'Not provided'}
</p>
</div>
</div>
<Separator className="bg-neutral-200" />
{/* User ID */}
<div className="flex items-start gap-4">
<div className="mt-1 p-2 rounded-lg bg-neutral-100">
<IdCard className="w-5 h-5 text-neutral-600" />
</div>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium text-neutral-500">User ID</p>
<p className="text-base font-mono text-neutral-700 break-all">
{user.id || 'Not provided'}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Account Status Card */}
<Card className="border-neutral-200 shadow-sm bg-neutral-50">
<CardHeader>
<CardTitle className="text-xl font-semibold text-neutral-900 flex items-center gap-2">
<Shield className="w-5 h-5 text-neutral-600" />
Account Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm text-neutral-600">Your account is active and verified</p>
<p className="text-xs text-neutral-500">All features are available</p>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
<span className="text-sm font-medium text-neutral-700">Online</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
export default ProfileData
Hope, you learnt something new. Share with your friends.
Follow and tag me on twitter.
Check out my projects, designs I have built in recent times.
I will be back with new concepts. See you next time.
