Manage global state in Lovable applications by choosing the right tool for each type of state: React Context for auth and theme data, Zustand for complex shared application state, and Supabase real-time subscriptions as a state source for data that needs to sync across users. Avoid prop drilling beyond two component levels by lifting shared state into a provider or store. This guide covers architecture decisions, not just implementation — see the Context API page for detailed implementation.
Why state management becomes inconsistent in Lovable apps
As Lovable projects grow, state that was initially local to one component starts being needed in multiple places. A user's auth status needs to be checked by the navigation bar, the dashboard page, and API call functions. A shopping cart needs to be accessible from the product listing, the cart sidebar, and the checkout page. Without a global state strategy, you end up with prop drilling — passing state through 5+ levels of components just to get it where it is needed. Lovable's AI often generates prop drilling patterns because it solves the immediate problem of 'get this data to that component.' But prop drilling makes the code fragile: adding a new component in the middle of the chain requires updating every component's props. The choice between React Context, Zustand, and Supabase real-time depends on the type of state. Authentication and theme are infrequently changing data used everywhere — perfect for Context. Shopping carts, filters, and UI state change frequently and benefit from Zustand's selective re-rendering. Data that must sync across multiple users in real time should use Supabase subscriptions.
- Props are being drilled through 3+ component levels to share state between distant components
- Multiple components maintain their own copy of the same data, leading to inconsistencies
- Authentication state is checked differently in different parts of the app
- No central store exists, so shared data like user preferences or cart items is scattered
- Lovable generates local state patterns even when global state would be more appropriate
Error messages you might see
Cannot read properties of undefined (reading 'user')A component is trying to access auth context but is rendered outside the AuthProvider. Make sure your context providers wrap the entire app in App.tsx or main.tsx.
Too many re-renders. React limits the number of renders to prevent an infinite loop.A state update in a context provider triggers re-renders in all consuming components, which triggers another update. Use Zustand for frequently changing state or memoize context values.
Cannot update a component while rendering a different componentA state update is happening during the render phase of another component. This often occurs when reading from a store triggers a side effect. Move the state update to a useEffect.
Before you start
- A Lovable project where multiple components need access to the same data
- Understanding of which data is shared across your app (auth, cart, preferences, etc.)
- Familiarity with React hooks (useState, useEffect, useContext)
How to fix it
Use React Context for auth and theme state
Auth and theme change infrequently and are needed everywhere — Context is the simplest solution for this pattern
Use React Context for auth and theme state
Auth and theme change infrequently and are needed everywhere — Context is the simplest solution for this pattern
Create a context provider that wraps your entire app. Store the current user, auth status, and theme in the context. Any component can access this data using useContext without prop drilling. Context re-renders all consuming components when the value changes, which is fine for auth/theme because they change rarely (on login/logout or theme toggle). Do not use Context for frequently changing data like form inputs or search results.
// Prop drilling auth through 4 levels:// App → Layout → Header → UserMenu → Avatar// Every component in the chain needs auth props// Context: any component reads auth directly// AuthProvider wraps App// Header uses useAuth() → no props needed// UserMenu uses useAuth() → no props needed// Dashboard uses useAuth() → no props neededExpected result: Any component can access auth state by calling useAuth() without receiving it through props.
Use Zustand for complex shared application state
Zustand provides selective re-rendering — only components that use a specific slice of state re-render when it changes
Use Zustand for complex shared application state
Zustand provides selective re-rendering — only components that use a specific slice of state re-render when it changes
For state that changes frequently and is used by multiple components (shopping cart, filters, form wizard steps), use Zustand. Ask Lovable to install Zustand and create a store. Unlike Context, Zustand only re-renders components that select the specific data that changed. A cart item count update only re-renders the cart badge, not the product listing. Zustand stores are also accessible outside React components, which is useful for API call functions.
// Context for cart state — causes unnecessary re-renders:// When cartItems changes, EVERY component using CartContext re-renders// Even components that only read totalItems// Zustand store — selective re-rendering:import { create } from 'zustand';const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), totalItems: () => useCartStore.getState().items.length,}));// In CartBadge: only re-renders when items.length changesconst count = useCartStore((state) => state.items.length);Expected result: Only the components that use the changed data re-render. The rest of the app is unaffected.
Use Supabase real-time as a state source for multi-user data
Data that needs to sync across multiple users in real time should be driven by the database, not local state
Use Supabase real-time as a state source for multi-user data
Data that needs to sync across multiple users in real time should be driven by the database, not local state
For data like chat messages, collaborative documents, or live dashboards, use Supabase real-time subscriptions as the state source. Subscribe to database changes in a useEffect, and update local state when the subscription fires. This way, when any user makes a change, all connected users see the update instantly. The subscription replaces manual polling and keeps the UI in sync. If implementing real-time state synchronization across multiple components gets complex, RapidDev's engineers have built this pattern across 600+ Lovable projects.
// Polling for updates every 5 seconds:useEffect(() => { const interval = setInterval(async () => { const { data } = await supabase.from('messages').select(); setMessages(data); }, 5000); return () => clearInterval(interval);}, []);// Real-time subscription — instant updates:useEffect(() => { // Fetch initial data supabase.from('messages').select().then(({ data }) => setMessages(data ?? [])); // Subscribe to changes const channel = supabase .channel('messages') .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, (payload) => { if (payload.eventType === 'INSERT') { setMessages((prev) => [...prev, payload.new]); } }) .subscribe(); return () => { supabase.removeChannel(channel); };}, []);Expected result: New messages appear instantly for all connected users without polling. The UI updates in real time.
Choose the right state management tool for each data type
Using the wrong tool causes performance problems (Context for fast-changing data) or unnecessary complexity (Zustand for simple auth)
Choose the right state management tool for each data type
Using the wrong tool causes performance problems (Context for fast-changing data) or unnecessary complexity (Zustand for simple auth)
Map each piece of shared data to the right tool. Auth status, current user, and theme go in React Context. Shopping cart, UI filters, notification counts, and form wizard state go in Zustand. Chat messages, collaborative edits, and live dashboards use Supabase real-time. Local component state (form inputs, toggle visibility) stays in useState. This architecture scales well as your app grows and avoids the common trap of putting everything in one giant Context.
// Everything in one context — causes cascading re-renders:// AuthContext contains: user, theme, cart, notifications, filters// Any change re-renders ALL components using this context// Separated by concern:// AuthContext → user, isAuthenticated, login/logout (React Context)// ThemeContext → theme, toggleDarkMode (React Context)// useCartStore → items, addItem, removeItem (Zustand)// useFilterStore → searchTerm, category, sortBy (Zustand)// messages → real-time subscription (Supabase)// formData → local useState (component-scoped)Expected result: Each type of state uses the most appropriate tool. Re-renders are minimized and the code is organized by concern.
Complete code example
1import { create } from "zustand";23type CartItem = {4 id: string;5 name: string;6 price: number;7 quantity: number;8};910type CartStore = {11 items: CartItem[];12 addItem: (item: Omit<CartItem, "quantity">) => void;13 removeItem: (id: string) => void;14 updateQuantity: (id: string, quantity: number) => void;15 clearCart: () => void;16 totalPrice: () => number;17 totalItems: () => number;18};1920export const useCartStore = create<CartStore>((set, get) => ({21 items: [],2223 addItem: (item) =>24 set((state) => {25 const existing = state.items.find((i) => i.id === item.id);26 if (existing) {27 return {28 items: state.items.map((i) =>29 i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i30 ),31 };32 }33 return { items: [...state.items, { ...item, quantity: 1 }] };34 }),3536 removeItem: (id) =>37 set((state) => ({ items: state.items.filter((i) => i.id !== id) })),3839 updateQuantity: (id, quantity) =>40 set((state) => ({41 items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),42 })),4344 clearCart: () => set({ items: [] }),4546 totalPrice: () =>47 get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),4849 totalItems: () =>50 get().items.reduce((sum, item) => sum + item.quantity, 0),51}));Best practices to prevent this
- Use React Context only for infrequently changing data (auth, theme) — it re-renders all consumers on every change
- Use Zustand for frequently changing shared state (cart, filters, notifications) — it supports selective re-rendering
- Use Supabase real-time subscriptions for data that must sync across multiple users in real time
- Keep local component state (form inputs, toggle visibility) in useState — not every piece of state needs to be global
- Wrap Context providers around the entire app in App.tsx to ensure all components can access them
- With Zustand, use selectors (state => state.items.length) to re-render only when the selected value changes
- Always clean up Supabase subscriptions in useEffect return functions to prevent memory leaks
- Do not put everything in one store or context — separate by concern for maintainability
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Lovable project has state management issues. Multiple components need access to the same data and I am currently passing it through props. Here is my component tree: [describe which components need which data] Please: 1. Recommend whether to use React Context, Zustand, or Supabase real-time for each piece of shared data 2. Create the appropriate stores/providers for each 3. Show me how to consume the state in my components without prop drilling 4. Ensure selective re-rendering where possible
Refactor the cart state in my app. Currently @src/pages/ProductList.tsx passes cart data through props to multiple child components. Create a Zustand store at @src/hooks/useCartStore.ts with items, addItem, removeItem, updateQuantity, clearCart, totalPrice, and totalItems. Update @src/components/CartBadge.tsx, @src/components/CartSidebar.tsx, and @src/pages/Checkout.tsx to use the store directly instead of receiving cart props. Remove cart prop drilling from @src/pages/ProductList.tsx.
Frequently asked questions
When should I use React Context vs Zustand in Lovable?
Use React Context for data that changes infrequently and is needed everywhere, like auth state and theme. Use Zustand for data that changes often and is used by specific components, like shopping cart items and search filters. Zustand supports selective re-rendering, while Context re-renders all consumers.
How do I install Zustand in my Lovable project?
Prompt Lovable: 'Install the zustand npm package and create a store at src/hooks/useCartStore.ts with these actions: addItem, removeItem, clearCart.' Lovable handles the installation and creates the store file.
Can I use Supabase as a state management solution?
Yes, for data that needs to sync across users. Use Supabase real-time subscriptions to listen for database changes and update local state when changes arrive. This is ideal for chat messages, collaborative features, and live dashboards.
How do I avoid prop drilling in Lovable?
If you are passing data through more than 2 component levels, move it to a context provider or Zustand store. Any component that needs the data can access it directly using useContext or the Zustand hook without receiving it through props.
Why does my app re-render constantly when using Context?
React Context re-renders every consuming component whenever the context value changes. If you store fast-changing data in Context, it triggers cascading re-renders. Move frequently changing state to Zustand, which only re-renders components that select the changed data.
What if I can't fix this myself?
If your app has a tangled state management setup with prop drilling across many components, RapidDev's engineers can refactor it into a clean architecture. They have restructured state management across 600+ Lovable projects.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation