Back to Blogs
May 5, 2026

Managing Global State in Next.js App Router (Without Breaking Server Components)

#Zustand#React Context#State Management#Next.js 15
Managing Global State in Next.js App Router (Without Breaking Server Components)

Managing state in traditional React Single Page Applications was straightforward: wrap your app in a Redux Provider or React Context, and you were done.

However, in the Next.js App Router, you cannot use React Context inside a Server Component. If you try, you'll get an error. Furthermore, pushing a Context Provider to the very top of your layout.tsx forces large chunks of your application to become Client Components.

So, how do we handle global state elegantly today? Here are the two best patterns for modern Next.js.

Pattern 1: URL Search Parameters (The Server-Safe Way)

Before reaching for a state management library, ask yourself: Should this state be shareable?

Things like active tabs, pagination, search queries, and modal toggles should live in the URL. Why? Because the server can read the URL, meaning you can keep your components as Server Components!

typescript
// page.tsx (Server Component)
import Link from 'next/link';

export default function ProductsPage({
  searchParams,
}: {
  searchParams: { category?: string };
}) {
  const activeCategory = searchParams.category || 'all';

  return (
    <div>
      <nav className="flex gap-4">
        {/* State changes happen via URL routing! */}
        <Link href="/products?category=tech">Tech</Link>
        <Link href="/products?category=home">Home</Link>
      </nav>
      
      <h1>Showing {activeCategory} products</h1>
      {/* Fetch data securely on the server based on the URL state */}
    </div>
  );
}

Pattern 2: Zustand for Complex Client State

For client-side state that doesn't belong in a URL (like a shopping cart, a multi-step form, or a complex UI toggle), Zustand is the industry standard for Next.js.

Unlike React Context, Zustand doesn't require a Provider wrapper, meaning it won't force your parent components into becoming Client Components. It is incredibly lightweight and simple to set up.

First, install it: npm install zustand

Then, create your store:

typescript
// store/useCartStore.ts
import { create } from 'zustand';

interface CartState {
  items: string[];
  addItem: (item: string) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clearCart: () => set({ items: [] }),
}));

Finally, use it only in the specific "leaf" components that need to be interactive:

typescript
// components/CartButton.tsx (Client Component)
'use client';
import { useCartStore } from '@/store/useCartStore';

export default function CartButton() {
  // Component only re-renders when 'items' changes
  const items = useCartStore((state) => state.items);
  const addItem = useCartStore((state) => state.addItem);

  return (
    <button onClick={() => addItem('New Product')}>
      Cart ({items.length})
    </button>
  );
}

By leveraging URL parameters for server-readable state and Zustand for complex client state, you can build highly scalable, interactive applications without sacrificing the performance of the App Router.


Muhammad Asad

Muhammad Asad

Senior Full-Stack Engineer & Consultant

Share / Connect:
Next Steps

Need help implementing this?

I partner with founders and technical teams to architect scalable, high-performance solutions.