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
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.
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.
// V0 generated — separate state in each component// components/product-card.tsxconst [cartCount, setCartCount] = useState(0)// components/header.tsxconst [cartCount, setCartCount] = useState(0) // Different state!// lib/store.tsimport { 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.
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.
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.
"use client"import { useState } from "react"export function CartButton() { const [count, setCount] = useState(0) // Local only return <span>Cart ({count})</span>}"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.
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.
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.
// app/layout.tsx — no state sharingexport default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html><body>{children}</body></html> )}// 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.tsximport { 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.
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.
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.
export const useCartStore = create<CartStore>((set, get) => ({ items: [], // ... actions}))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
1import { create } from "zustand"2import { persist } from "zustand/middleware"34interface CartItem {5 id: string6 name: string7 price: number8 quantity: number9}1011interface CartStore {12 items: CartItem[]13 addItem: (item: Omit<CartItem, "quantity">) => void14 removeItem: (id: string) => void15 updateQuantity: (id: string, quantity: number) => void16 clearCart: () => void17 totalItems: () => number18 totalPrice: () => number19}2021export 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.id32 ? { ...i, quantity: i.quantity + 1 }33 : i34 ),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) } : i47 ).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 056 ),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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation