Skip to main content
RapidDev - Software Development Agency
v0-issues

Managing global state in V0 using Zustand or Context API

V0 generates components with local useState that cannot share data across pages or sibling components. Fix this by implementing Zustand for client-side global state (simpler, no provider wrapper needed) or React Context for server-component-friendly state that needs to be shared across a layout tree. Both require the "use client" directive on consuming components in Next.js App Router.

Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read15-25 minutesV0 with Next.js App Router, Zustand 4+, React 18+March 2026RapidDev Engineering Team
TL;DR

V0 generates components with local useState that cannot share data across pages or sibling components. Fix this by implementing Zustand for client-side global state (simpler, no provider wrapper needed) or React Context for server-component-friendly state that needs to be shared across a layout tree. Both require the "use client" directive on consuming components in Next.js App Router.

Why V0 apps struggle with shared state

V0 generates each component in isolation with its own useState calls. This works for self-contained UI elements, but breaks down when multiple components need to share data — like a shopping cart that updates from product pages and displays in the header, or user preferences that affect the entire app. V0 does not set up state management libraries or Context providers unless explicitly asked. Additionally, Next.js App Router adds complexity because Server Components cannot use hooks, meaning state management must be carefully scoped to "use client" boundaries. V0 frequently confuses this boundary, putting state logic in Server Components where it crashes.

  • V0 uses local useState in every component instead of lifting state up or using a global store
  • No Zustand store or Context provider is set up — each component manages its own independent copy of the data
  • V0 puts state management hooks in Server Components where they are not available
  • Missing "use client" directive on components that consume global state via hooks
  • State resets on page navigation because it lives in page-level components that unmount

Error messages you might see

Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it.

V0 placed a state hook in a Server Component. State management hooks (useState, useContext, Zustand's useStore) only work in Client Components marked with "use client".

Error: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it.

V0 tried to create a React Context in a Server Component. The provider component must be a Client Component, though it can wrap Server Components as children.

TypeError: Cannot read properties of null (reading 'user')

The component tries to read from a Context that has no Provider ancestor. V0 forgot to wrap the component tree in the Context Provider, or the Provider is in a different layout segment.

Before you start

  • A V0 project where multiple components need to share state (cart, auth, theme, filters)
  • The "use client" directive on all components that consume state
  • Zustand or React Context chosen as the state management approach

How to fix it

1

Set up a Zustand store for global client state

Zustand is the simplest global state solution for V0 apps. It does not require a Provider wrapper, works with Next.js App Router's client/server split, and V0 can generate components that use it cleanly. It is significantly less boilerplate than Redux or Context+Reducer patterns.

Create a Zustand store file that defines your shared state and actions. Import the store hook in any "use client" component that needs access to the shared state. No Provider wrapper is needed.

Before
typescript
// V0 generated — separate state in each component
// components/product-card.tsx
const [cartCount, setCartCount] = useState(0)
// components/header.tsx
const [cartCount, setCartCount] = useState(0) // Different state!
After
typescript
// lib/store.ts
import { create } from "zustand"
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartStore {
items: CartItem[]
addItem: (item: Omit<CartItem, "quantity">) => void
removeItem: (id: string) => void
clearCart: () => void
totalItems: () => number
totalPrice: () => number
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id)
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
}
}
return { items: [...state.items, { ...item, quantity: 1 }] }
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
clearCart: () => set({ items: [] }),
totalItems: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}))

Expected result: Both the product card and the header read from the same Zustand store. Adding an item from the product page instantly updates the cart count in the header.

2

Consume the Zustand store in client components

Zustand hooks work in any "use client" component without a Provider. This makes them easy to add to V0 components — just import the hook and use it.

Import the store hook in each component that needs shared state. Use selector functions to only subscribe to the specific state slices each component needs, preventing unnecessary re-renders.

Before
typescript
"use client"
import { useState } from "react"
export function CartButton() {
const [count, setCount] = useState(0) // Local only
return <span>Cart ({count})</span>
}
After
typescript
"use client"
import { useCartStore } from "@/lib/store"
export function CartButton() {
const totalItems = useCartStore((state) => state.totalItems())
return <span>Cart ({totalItems})</span>
}

Expected result: The cart button shows the real total from the global store. It updates instantly when items are added from any page.

3

Use React Context for state that wraps Server Components

When you need state that is available to an entire layout tree (including Server Component children that render Client Component leaves), a Context Provider in the root layout is the correct pattern. The Provider itself is a Client Component, but it can wrap Server Components.

Create a Context provider as a "use client" component. Import and wrap it around {children} in your layout.tsx. Server Components inside the layout do not use the context directly — only their Client Component children consume it via useContext.

Before
typescript
// app/layout.tsx — no state sharing
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html><body>{children}</body></html>
)
}
After
typescript
// providers/theme-provider.tsx
"use client"
import { createContext, useContext, useState } from "react"
type Theme = "light" | "dark"
const ThemeContext = createContext<{
theme: Theme
setTheme: (t: Theme) => void
}>({ theme: "light", setTheme: () => {} })
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light")
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={theme === "dark" ? "dark" : ""}>{children}</div>
</ThemeContext.Provider>
)
}
export const useTheme = () => useContext(ThemeContext)
// app/layout.tsx
import { ThemeProvider } from "@/providers/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html><body><ThemeProvider>{children}</ThemeProvider></body></html>
)
}

Expected result: All pages and components in the app can access the theme state. Toggling dark mode in any component updates the entire app instantly.

4

Persist state across page navigations with Zustand middleware

Zustand state resets when the user refreshes the page because it lives in memory. For state like cart items or user preferences, you need persistence via localStorage. Zustand's persist middleware handles this automatically.

Wrap your Zustand store with the persist middleware to save state to localStorage. The middleware handles serialization, deserialization, and hydration automatically. Add a hydration check to avoid SSR mismatches.

Before
typescript
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
// ... actions
}))
After
typescript
import { persist } from "zustand/middleware"
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
// ... same actions as before
}),
{
name: "cart-storage",
}
)
)

Expected result: Cart items persist across page refreshes and browser restarts. The cart is restored from localStorage when the user returns to the app.

Complete code example

lib/store.ts
1import { create } from "zustand"
2import { persist } from "zustand/middleware"
3
4interface CartItem {
5 id: string
6 name: string
7 price: number
8 quantity: number
9}
10
11interface CartStore {
12 items: CartItem[]
13 addItem: (item: Omit<CartItem, "quantity">) => void
14 removeItem: (id: string) => void
15 updateQuantity: (id: string, quantity: number) => void
16 clearCart: () => void
17 totalItems: () => number
18 totalPrice: () => number
19}
20
21export const useCartStore = create<CartStore>()(
22 persist(
23 (set, get) => ({
24 items: [],
25 addItem: (item) =>
26 set((state) => {
27 const existing = state.items.find((i) => i.id === item.id)
28 if (existing) {
29 return {
30 items: state.items.map((i) =>
31 i.id === item.id
32 ? { ...i, quantity: i.quantity + 1 }
33 : i
34 ),
35 }
36 }
37 return { items: [...state.items, { ...item, quantity: 1 }] }
38 }),
39 removeItem: (id) =>
40 set((state) => ({
41 items: state.items.filter((i) => i.id !== id),
42 })),
43 updateQuantity: (id, quantity) =>
44 set((state) => ({
45 items: state.items.map((i) =>
46 i.id === id ? { ...i, quantity: Math.max(0, quantity) } : i
47 ).filter((i) => i.quantity > 0),
48 })),
49 clearCart: () => set({ items: [] }),
50 totalItems: () =>
51 get().items.reduce((sum, i) => sum + i.quantity, 0),
52 totalPrice: () =>
53 get().items.reduce(
54 (sum, i) => sum + i.price * i.quantity,
55 0
56 ),
57 }),
58 { name: "cart-storage" }
59 )
60)

Best practices to prevent this

  • Use Zustand for most global state needs — it requires no Provider and works simply with Next.js App Router
  • Use React Context only when state needs to wrap Server Components or integrates with layout.tsx providers
  • Always add "use client" to every component that consumes state via hooks (useState, useContext, Zustand stores)
  • Use selector functions with Zustand (useStore(state => state.field)) to prevent unnecessary re-renders
  • Add Zustand persist middleware for state that should survive page refreshes (cart, preferences, auth)
  • Keep store files in lib/ or stores/ directory so V0 does not overwrite them during UI regeneration
  • Define TypeScript interfaces for your store state to get autocompletion and type safety in consuming components

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

My V0 Next.js App Router project needs global state management for a shopping cart. Multiple components on different pages need to read and write cart data. Should I use Zustand or React Context? Show me the implementation with TypeScript, persistence, and proper "use client" boundaries.

Frequently asked questions

Should I use Zustand or React Context in my V0 app?

Use Zustand for most cases. It is simpler (no Provider wrapper), more performant (built-in selector optimization), and has persistence middleware. Use Context only when you need to provide values from a Server Component layout or when the state is tightly coupled to the component tree structure.

Why does my Zustand store reset on page navigation?

By default, Zustand stores live in memory and reset on full page refreshes (but persist across client-side navigation). If your state resets on every navigation, you may have a full page reload instead of client-side routing. Ensure you use next/link for navigation. For refresh persistence, add the persist middleware.

Can I use Redux with V0?

Yes, but it is not recommended. Redux requires significantly more boilerplate (store, slices, Provider, dispatch) that V0 often generates incorrectly. Zustand achieves the same result with much less code and is easier for V0 to scaffold correctly.

How do I handle SSR with Zustand in Next.js?

Zustand works client-side by default. For SSR-safe state, avoid reading Zustand values during server rendering. Use the persist middleware's onRehydrateStorage callback to know when client state is ready. In components, conditionally render based on a mounted state.

Why does V0 put state hooks in Server Components?

V0 does not always distinguish between Server and Client Components in Next.js App Router. When it generates state management code, it may place hooks in files without the "use client" directive. Always verify that any component using hooks has this directive.

Can RapidDev set up state management architecture for my V0 project?

Yes. For apps with complex state needs (multi-entity data, real-time sync, optimistic updates), RapidDev engineers can design and implement a scalable state management architecture using Zustand, React Query, or a combination that works cleanly with Next.js App Router.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your issue.

Book a free consultation

Need help with your Lovable project?

Our experts have built 600+ apps and can solve your issue fast. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.