Introduction
State management is one of the cornerstones of React applications. As your app grows in complexity, managing the state efficiently can become a challenge. In large applications, tools like Redux or Context API might seem over-complicated, with a lot of boilerplate code and performance concerns.
In this article, weāll explore Zustand, a minimalistic state management library, and integrate it with the App Router in Next.js (version 13+). Zustand offers a simple and flexible approach to managing global state without the overhead of Redux or Context API, while also being well-suited for modern Next.js applications.
By the end of this article, you'll have a clear understanding of how Zustand works with the App Router and be ready to implement it in your own projects.
What is Zustand?
Zustand is a lightweight state management library that simplifies state handling in React applications. With no reducers or actions, Zustand enables easy state management by directly creating stores. Itās a great fit for applications where you want to avoid the complexity of Redux but still need a global state solution.
Key Benefits of Zustand:
- Minimal boilerplate: No actions, reducers, or providers needed.
- Performance-focused: Components only re-render when the specific parts of the state they subscribe to change.
- Simple API: Easy to integrate with any React application, including Next.js.
Setting Up Zustand with the App Router in Next.js
Setting up Zustand with Next.js using the App Router is very straightforward. The App Router is the default for new Next.js apps, leveraging the new file-system-based routing and support for server-side rendering.
1. Install Zustand
Start by installing Zustand in your Next.js app:
npm install zustand
2. Create a Zustand Store
Zustand allows you to create a store that holds all of your global state. Hereās an example of a store that manages a simple counter.
In Next.js (App Router), itās recommended to keep the store outside the pages or app directory, typically in a lib or stores directory.
Create a store.js file in a lib folder:
import { create } from 'zustand';
// Note: 'create' as a default export is a deprecated import.
const useStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: get().count + 1 })),
}));
export default useStore;
create
is used to define the store.
The store holds the count state, and increment is a function to update the count.
3. Using the Store in the App Router
With Zustand, you can use your store directly in any component or page. Hereās how to set up components to use the store.
Let's define the main page of the app as app/page.tsx
for the sake of the example.
import { useStore } from '@/lib/store'; // The store we defined earlier
import Link from 'next/link';
export default function Home() {
const { count, increment } = useStore();
return (
<div>
<h1>Home Page</h1>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<Link href="/page2">Go to Second Page</Link>
</div>
);
};
Since Zustand's store is persistent across pages, we can create another page app/page2.tsx
and the state will be kept and change for both pages:
import { useStore } from '@/lib/store'
export default function SecondPage() {
const { count, increment } = useStore();
return (
<div>
<h1>Second Page Page</h1>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<Link href="/">Go to Home Page</Link>
</div>
);
};
Persisting State with Zustand
You can use Zustand to persist parts of your state across browser sessions. Hereās an example where we persist the darkMode setting to localStorage:
// lib/store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'
const usePersistentStore = create(
persist((set) => ({
darkMode: false,
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
}),
{ name: 'persistent-store' } // Keep the store persistent on localStorage, a storage prop is optional (localStorage chosen by default)
),
);
export default useStore;
This way, even after the user refreshes or closes the app, the darkMode state remains in localStorage.
Handling Async Actions with Zustand
You can handle async actions, such as fetching data from an API, by using async functions within your Zustand store. Hereās an example:
// lib/store.ts
import { create } from 'zustand';
const useStore = create((set) => ({
data: null,
fetchData: async () => {
const response = await fetch('/api/data');
const result = await response.json();
set({ data: result });
},
}));
export default useStore;
Now, you can call fetchData from any component, and Zustand will manage the async state without any extra complexity.
Advanced Store Configuration
Zustand also allows you to create multiple stores for different concerns or use middleware for state persistence, logging, etc. You can encapsulate store logic for better organization in larger apps.
Why Zustand for Next.js?
Simplified State Logic - Zustand is a minimalistic solution that doesnāt require defining actions, reducers, or wrapping components with providers. It simplifies the state logic in a way that makes it easy to use in any Next.js app.
Optimized for Performance - Zustand is highly optimized for performance, ensuring that components only re-render when the specific part of the state theyāre subscribed to changes. This prevents unnecessary re-renders and keeps your app fast and responsive.
Seamless SSR & SSG Integration - Zustand works seamlessly with Next.js's SSR and SSG features. Since Zustand stores are just JavaScript objects, you can use them directly in both server-side and client-side components without additional configuration.
Conclusion
Zustand is a great state management solution for React and Next.js applications, especially when using the App Router. Its minimalistic design, combined with its easy-to-use API, makes it ideal for both small and large-scale applications. Whether you're building a simple app or a complex system, Zustand allows you to manage state with less boilerplate and better performance.
If youāre working with Next.js 13+ and want an efficient way to manage state in your app, Zustand is definitely worth considering. Try integrating it into your projects and let me know how it works for you!
Happy coding (ć£āāæā)ć£
Top comments (11)
What about hydration issues?
Excellent question, Thomas Burleson!
Hydration issues can arise when the store isnāt handled properly, particularly if itās initialized and used differently on the client and server. From my experience with Zustand, Iāve never encountered hydration issues caused by Zustand itself. This is because I separate my store implementations on the server side and avoid initializing them directly on the client.
While I donāt have personal experience with hydration issues caused by Zustand, Iād recommend keeping your stores in a dedicated folder, such as
/src/store
or another path that fits your project structure. This practice helps ensure thereās no conflicting state between the client and server.Thanks for bringing this up!
Zustand makes state management in React & Next.js effortless, with minimal boilerplate and maximum performance. Perfect for modern web apps! š
Absolutely! Glad to hear you found my article useful š
Why persistent, across pages? it could be fetch in source path. Like dark / light theme flag vale.
Hey Anupam Maurya!
The source path fetch is less advisable as things can get quite messy when working with many different variables within the state. The state is persistent and should be that way to provide the user with their selected theme even after leaving the page and returning back to it.
In any case, my example was only to show the option to persist the state across multiple pages and keep it even if the user has quit the page instance. In real cases of implementing dark mode and light mode, I would advise using the next-themes package instead of a persistent state.
Thanks for your question!
When taking a look at the official docs they recommend to initialize the store per request using a provider zustand.docs.pmnd.rs/guides/nextjs
How are you getting around this issue?
Great question, Andreas!
The provider-localized store recommended in the docs is ideal for isolating state per request or page instance, ensuring changes donāt carry over unintentionally. This works well for cases like guest-mode demos, where state resets on each session or page.
However, it depends on your use case. In my project CodeLib, I needed a global state to manage data fetched dynamically from an external SDK client (which was used together with a search functionality for all of the user's data). This state had to persist across pages to provide seamless access to code snippets. A provider setup wouldnāt work here, as it ties state to a single page.
Provider-localized state is perfect for temporary data, while global state is better for persistent needs like user authentication or shared resources. Both have their placeāchoose based on your appās requirements.
Let me know if you have anymore questions!
Looks pretty good!
Thank you so much! Stay tuned for an interesting article tomorrow as well :)
Ever since I got introduced to Zustand about 2 years ago, it has since being my state management library in all my projects.
I love Zustand