js

Next.js State Management with Zustand: Simple Client State Without the Boilerplate

Learn how to use Zustand for Next.js client state, hydration, and persistence without heavy boilerplate. Build faster with a simpler approach.

Next.js State Management with Zustand: Simple Client State Without the Boilerplate

I’ve been building with Next.js for a while now, and there’s one question that keeps coming up in every project: how do I handle state on the client without it becoming a chore? I found myself defaulting to heavy libraries, adding layers of complexity for what should be simple. That’s why I started looking for something else, something that felt right for the way Next.js works. That’s how I found Zustand.

The goal is straightforward. You need to manage things like a user’s form inputs, a shopping cart, or whether a modal is open. This data lives in the browser, but it needs to be available across different components and pages. Have you ever dropped a complex feature because setting up the state felt like too much work? I have. With Zustand, that feeling goes away.

Let’s start with what makes Zustand different. It’s a small library. I mean really small. You create a store with a function, and it gives you a hook to use anywhere in your app. There’s no provider to wrap your entire application in. You just import the hook and use it. It feels less like adding a state management library and more like using a slightly supercharged React hook.

Here’s the basic idea. You create a store. Inside, you define your state and the functions that update it. It’s all in one place.

import { create } from 'zustand'

const useCartStore = create((set) => ({
  items: [],
  addItem: (product) =>
    set((state) => ({ items: [...state.items, product] })),
  clearCart: () => set({ items: [] }),
}))

To use it in a component, you call the hook and pick the pieces of state you need. This is my favorite part. The component only re-renders when the specific data it uses changes. If one component uses items and another uses addItem, they won’t affect each other’s performance.

function CartButton() {
  const items = useCartStore((state) => state.items)
  const addItem = useCartStore((state) => state.addItem)

  return (
    <button onClick={() => addItem({ id: 1, name: 'Book' })}>
      Add to Cart ({items.length})
    </button>
  )
}

So, where does Next.js come in? Next.js renders pages on the server. But Zustand stores exist only on the client. This creates a mismatch. The server might render a page showing an empty cart, but on the client, the cart might have items from a previous session. For a split second, the user sees the wrong state before JavaScript loads and corrects it. This flash of incorrect content is a real problem.

How do we fix this? We need to make sure the client and server start with the same information. This process is called hydration. With Zustand, we can create a store that can be initialized with data from the server.

The pattern involves passing data from getServerSideProps or getStaticProps to a special store initializer. We create a store that can accept this initial data.

// stores/cart-store.js
import { create } from 'zustand'

export const createCartStore = (initItems = []) =>
  create((set) => ({
    items: initItems, // Initialize with server data
    addItem: (product) =>
      set((state) => ({ items: [...state.items, product] })),
  }))

// A default store for client-side usage
const useCartStore = createCartStore()
export default useCartStore

Then, in a page, we fetch the data on the server and pass it to a component that will create a store instance just for that page.

// pages/index.js
export async function getServerSideProps() {
  // Fetch initial cart data from an API or database
  const initialCartData = await fetchCartFromDB()
  return { props: { initialCartData } }
}

export default function HomePage({ initialCartData }) {
  // Use the store initialized with server data
  const useStore = React.useMemo(
    () => createCartStore(initialCartData),
    [initialCartData]
  )
  const items = useStore((state) => state.items)

  return <div>Cart Items: {items.length}</div>
}

This approach ensures there is no flash of wrong content. The server-rendered HTML already has the correct cart count. When Zustand takes over on the client, it starts with that same data. It’s seamless.

But what about more complex needs? Zustand is minimal, but it’s not simplistic. You can work with async actions, persist state to localStorage, or even use middleware for logging or handling immutability. Its power comes from its simplicity. You build what you need, nothing more.

For example, persisting a user’s theme preference is trivial with the persist middleware.

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useThemeStore = create(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (newTheme) => set({ theme: newTheme }),
    }),
    {
      name: 'theme-storage', // unique name for localStorage key
    }
  )
)

The store will now save the theme value automatically. When the user returns, their preference is loaded. It’s this kind of utility that makes development faster. You spend time on features, not on wiring up state logic.

Why does this combination feel so right for Next.js? Because both tools value developer experience and performance. Next.js handles the server, the routes, and the build. Zustand handles the client state. They don’t step on each other’s toes. You get a full-stack framework with a state solution that doesn’t weigh it down.

Think about your last project. How much of your state was truly global, and how much was just shared between a few components? For many apps, a lightweight solution like Zustand is more than enough. It reduces boilerplate, keeps your bundle small, and integrates cleanly with Next.js’s hybrid rendering model.

The result is an application that feels responsive and cohesive. State updates are fast, the code is easy to follow, and you avoid the complexity of a larger library. It’s a practical choice that scales from a simple side project to a large, interactive application.

I encourage you to try it. Start a new Next.js project, or refactor a small part of an existing one. Create a store for a simple piece of UI state and see how it feels. I think you’ll be surprised by how much you can do with so little code.

If this approach to state management makes sense for your work, please share this article with a teammate or a friend who’s wrestling with similar problems. Have you used Zustand in a different way? I’d love to hear about your experience in the comments below. Let’s keep the conversation going.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: Next.js state management, Zustand, client state, hydration, React state



Similar Posts
Blog Image
How to Build an HLS Video Streaming Server with Node.js and FFmpeg

Learn how to create your own adaptive bitrate video streaming server using Node.js, FFmpeg, and HLS. Step-by-step guide included.

Blog Image
Build Multi-Tenant SaaS with NestJS, Prisma & PostgreSQL Row-Level Security: Complete Developer Guide

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, authentication & performance optimization.

Blog Image
Build Complete Event-Driven Architecture with RabbitMQ TypeScript Microservices Tutorial

Learn to build scalable event-driven microservices with RabbitMQ & TypeScript. Master event sourcing, CQRS, error handling & production deployment.

Blog Image
How to Build Production-Ready Event-Driven Microservices with NestJS, RabbitMQ, and Redis

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & Redis. Master async communication, caching, error handling & production deployment patterns.

Blog Image
Complete Guide: Integrating Next.js with Prisma ORM for Type-Safe Database Applications 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven applications. Build full-stack React apps with seamless data handling today.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Full-Stack TypeScript Applications

Learn how to integrate Next.js with Prisma ORM for powerful full-stack development. Build type-safe applications with seamless database management and API routes.