Zustand is a lightweight, fast, and scalable state management library for React applications, including those built with Next.js. It offers a simple API with minimal boilerplate, making it an excellent choice for managing complex state in modern web applications. This guide provides a step-by-step approach to integrating Zustand into your Next.js project, covering installation, store creation, state management within components, server-side rendering (SSR), asynchronous actions, middleware usage, and best practices.
To begin using Zustand in your Next.js application, you need to install it using either npm or yarn:
npm install zustand
yarn add zustand
Ensure that Zustand is added to your project's dependencies to make its functionality available throughout your application.
A store in Zustand holds the state and actions that manipulate the state. It's typically created in a separate file to maintain a clean and organized project structure.
Create a directory named store
inside your src
folder (or as per your project structure). Inside this directory, create a file named useStore.js
:
// /store/useStore.js
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
// Add more state properties and actions as needed
}));
export default useStore;
In this example, the store manages a simple counter with actions to increment and decrement the count.
For more complex applications, you can define multiple stores or include additional middleware for features like persistence or logging.
// /store/useUserStore.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useUserStore = create(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}),
{
name: 'user-storage', // unique name
getStorage: () => localStorage, // (optional) by default, 'localStorage' is used
}
)
);
export default useUserStore;
This example demonstrates using the persist
middleware to save the user state in localStorage
, ensuring the state persists across page reloads.
Once the store is set up, you can use it within your Next.js components to access and manipulate the state.
Import the store and use it to access state variables and actions:
// /components/Counter.js
import React from 'react';
import useStore from '../store/useStore';
const Counter = () => {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
Counter: {count}
);
};
export default Counter;
The useStore
hook accepts a selector function to retrieve specific parts of the state, enhancing performance by preventing unnecessary re-renders.
Zustand integrates seamlessly with Next.js's SSR capabilities. To ensure the state is correctly hydrated on the client side, follow these steps:
// /pages/index.js
import React, { useEffect } from 'react';
import useStore from '../store/useStore';
const Home = ({ initialData }) => {
const setData = useStore((state) => state.setData);
useEffect(() => {
setData(initialData);
}, [setData, initialData]);
return (
Welcome to the Home Page
{/* Your component logic */}
);
};
export const getServerSideProps = async () => {
const data = await fetchData(); // Replace with your data fetching logic
return { props: { initialData: data } };
};
export default Home;
In this example, data is fetched on the server side using getServerSideProps
and then passed to the client to initialize the Zustand store.
Zustand simplifies the handling of asynchronous actions, such as fetching data from an API.
Extend your store to include async functions:
// /store/useUserStore.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useUserStore = create(
persist(
(set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
set({ user: data, loading: false });
} catch (err) {
set({ error: err.message, loading: false });
}
},
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}),
{
name: 'user-storage',
getStorage: () => localStorage,
}
)
);
export default useUserStore;
Invoke asynchronous actions within your components to handle tasks like data fetching:
// /components/UserProfile.js
import React, { useEffect } from 'react';
import useUserStore from '../store/useUserStore';
const UserProfile = ({ userId }) => {
const { user, loading, error, fetchUser } = useUserStore();
useEffect(() => {
fetchUser(userId);
}, [fetchUser, userId]);
if (loading) return Loading...
;
if (error) return Error: {error}
;
if (!user) return No user data available.
;
return (
User Profile: {user.name}
Email: {user.email}
{/* Additional user details */}
);
};
export default UserProfile;
This component fetches user data when mounted and displays it, handling loading and error states seamlessly.
Zustand supports various middleware to extend its functionality, such as persistence, logging, and more.
Persisting state allows the application to retain state across sessions. This is achieved using the persist
middleware:
// /store/useSettingsStore.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useSettingsStore = create(
persist(
(set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}),
{
name: 'settings-storage',
getStorage: () => localStorage,
}
)
);
export default useSettingsStore;
This example creates a settings store that persists the user's theme preference in localStorage
.
Implementing a logging middleware can help in debugging by tracking state changes:
// /store/useLoggerStore.js
import create from 'zustand';
import { devtools } from 'zustand/middleware';
const useLoggerStore = create(
devtools((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
}))
);
export default useLoggerStore;
By integrating the devtools
middleware, you can leverage Redux DevTools for enhanced state debugging.
For larger applications, managing multiple stores can lead to better organization and scalability.
Define separate stores for distinct parts of your application:
// /store/useCartStore.js
import create from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({ items: state.items.filter(item => item.id !== id) })),
clearCart: () => set({ items: [] }),
}));
export default useCartStore;
// /store/useWishlistStore.js
import create from 'zustand';
const useWishlistStore = create((set) => ({
wishlist: [],
addToWishlist: (item) => set((state) => ({ wishlist: [...state.wishlist, item] })),
removeFromWishlist: (id) => set((state) => ({ wishlist: state.wishlist.filter(item => item.id !== id) })),
}));
export default useWishlistStore;
Import and use the necessary stores within your components:
// /components/ShoppingCart.js
import React from 'react';
import useCartStore from '../store/useCartStore';
const ShoppingCart = () => {
const { items, addItem, removeItem, clearCart } = useCartStore();
return (
Shopping Cart
{items.map((item) => (
-
{item.name} - ${item.price}
))}
);
};
export default ShoppingCart;
This approach ensures that each store manages its own piece of state, promoting modularity and separation of concerns.
Adhering to best practices enhances the maintainability, performance, and scalability of your application.
Design each store to manage a specific part of the state. This modular approach simplifies state management and makes your stores easier to reason about.
Utilize selector functions in the useStore
hook to subscribe only to the necessary slices of state. This reduces unnecessary re-renders and optimizes performance:
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
Leverage middleware like persist
for state persistence or devtools
for debugging, but avoid overcomplicating your store with unnecessary enhancements.
If you're using TypeScript, defining types for your store’s state and actions adds an extra layer of safety and improves developer experience:
// /store/useStore.ts
import create from 'zustand';
interface StoreState {
count: number;
increment: () => void;
decrement: () => void;
}
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
export default useStore;
While Zustand is powerful, not every piece of state needs to reside in the global store. Local component state is still appropriate for transient or UI-specific data.
For more complex requirements, Zustand offers advanced features and integrations.
While Zustand itself provides a global state, you might sometimes need to integrate it with React Context for additional functionality or to segregate certain parts of the state.
// /context/ThemeContext.js
import React, { createContext, useContext } from 'react';
import useStore from '../store/useStore';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const theme = useStore((state) => state.theme);
const toggleTheme = useStore((state) => state.toggleTheme);
return (
{children}
);
};
export const useTheme = () => useContext(ThemeContext);
Zustand's middleware capabilities allow for features like logging, persisting state, and more. Here's how to integrate multiple middleware:
// /store/useEnhancedStore.js
import create from 'zustand';
import { persist, devtools } from 'zustand/middleware';
const useEnhancedStore = create(
devtools(
persist(
(set) => ({
data: [],
addData: (item) => set((state) => ({ data: [...state.data, item] })),
// Additional state and actions
}),
{
name: 'data-storage',
getStorage: () => localStorage,
}
)
)
);
export default useEnhancedStore;
This configuration applies both persist
and devtools
middleware to the store, enabling state persistence and Redux DevTools integration simultaneously.
For further information, advanced usage, and updates, refer to the official Zustand documentation and repository:
Zustand offers a straightforward and efficient approach to state management in Next.js applications. Its minimalistic API, combined with powerful features like middleware support and seamless SSR integration, makes it a compelling choice for developers seeking both simplicity and scalability. By following the steps and best practices outlined in this guide, you can effectively incorporate Zustand into your projects, enhancing state management and overall application performance.