Lovable projects work best with React Context for global state since it requires no extra packages. Create a context provider, wrap your App component with it, and use the useContext hook in child components to share state. Use Zustand only for complex cross-component state, and avoid Redux unless your app specifically requires its middleware ecosystem.
Why global state management needs extra setup in Lovable
Lovable generates components that manage their own state using useState and useEffect. This works well for isolated features, but breaks down when multiple components need to share the same data — like user authentication status, shopping cart contents, or theme preferences. Without a shared state solution, you end up passing props through five or six component layers (called 'prop drilling'), which makes the code fragile and hard to maintain. React Context API is built into React and requires no additional packages, making it the natural first choice for Lovable projects. It lets you create a 'provider' component at the top of your component tree that makes data available to any descendant component, no matter how deeply nested. Zustand is a lightweight alternative that works well when you need state that persists across route changes or when Context re-renders become a performance issue. Redux is the heaviest option and is generally overkill for Lovable projects unless you need its middleware (like Redux Saga for complex async flows) or already have a team experienced with it.
- Prop drilling — passing data through many component layers because there is no shared state
- Component isolation — Lovable generates self-contained components that do not share state by default
- Authentication state not accessible — child components cannot check if the user is logged in without passing session through props
- Theme or language preference lost on navigation — route changes remount components and local state is reset
- Cart or form data not shared between pages — each page manages its own state independently
Error messages you might see
TypeError: Cannot read properties of null (reading 'user')You are trying to access a context value outside of its Provider component. Make sure the component consuming the context is a child of the Provider in your component tree.
useContext(ThemeContext) returned undefinedThe context was created but the Provider was not added to the component tree above this component. Wrap your App component with the Provider.
Too many re-renders. React limits the number of renders to prevent an infinite loop.Your context value is being recreated on every render, causing all consumers to re-render in a loop. Wrap the context value object in useMemo to stabilize it.
Before you start
- A Lovable project where multiple components need to share the same data
- Understanding of which data needs to be global (auth state, theme, cart) versus local (form inputs, toggles)
- The project open in Dev Mode or the Lovable editor
How to fix it
Create a React Context with a typed provider
Context API is built into React and requires no extra packages — it is the simplest way to share state across Lovable components
Create a React Context with a typed provider
Context API is built into React and requires no extra packages — it is the simplest way to share state across Lovable components
Create a new file for your context. Define the shape of the shared state using a TypeScript interface, create the context with createContext, and build a Provider component that wraps useState to manage the actual data. Export both the Provider (to wrap your app) and a custom hook (to consume the context in child components).
// No shared state — each component fetches its own user data// src/components/Navbar.tsxfunction Navbar() { const [user, setUser] = useState(null); useEffect(() => { /* fetch user */ }, []); return <nav>{user?.name}</nav>;}// src/components/Sidebar.tsxfunction Sidebar() { const [user, setUser] = useState(null); useEffect(() => { /* fetch user again */ }, []); return <aside>{user?.role}</aside>;}// src/contexts/AuthContext.tsximport { createContext, useContext, useState, useEffect, ReactNode } from "react";import { supabase } from "@/integrations/supabase/client";import { User } from "@supabase/supabase-js";interface AuthContextType { user: User | null; isLoading: boolean;}const AuthContext = createContext<AuthContextType | undefined>(undefined);export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { supabase.auth.getSession().then(({ data: { session } }) => { setUser(session?.user ?? null); setIsLoading(false); }); const { data: { subscription } } = supabase.auth.onAuthStateChange( (_event, session) => setUser(session?.user ?? null) ); return () => subscription.unsubscribe(); }, []); return ( <AuthContext.Provider value={{ user, isLoading }}> {children} </AuthContext.Provider> );}export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context;}Expected result: A reusable AuthProvider that any component can consume via the useAuth() hook without prop drilling.
Wrap your App component with the Provider
The Provider must be at the top of the component tree so all routes and components can access the context
Wrap your App component with the Provider
The Provider must be at the top of the component tree so all routes and components can access the context
Open src/App.tsx and wrap the BrowserRouter (or your top-level layout) with the AuthProvider. Place it outside the router so both page components and navigation components can access the auth state. If you have multiple providers (auth, theme, cart), nest them from outermost to innermost.
function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Index />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </BrowserRouter> );}import { AuthProvider } from "@/contexts/AuthContext";function App() { return ( <AuthProvider> <BrowserRouter> <Routes> <Route path="/" element={<Index />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </BrowserRouter> </AuthProvider> );}Expected result: Every component inside the app can now call useAuth() to access the shared user state.
Consume the context in child components
Child components use the custom hook to read and update shared state without prop drilling
Consume the context in child components
Child components use the custom hook to read and update shared state without prop drilling
In any component that needs the shared state, import and call the useAuth hook. The hook returns the current user and loading state. Because the context is typed, your editor gives you autocomplete and type checking. No need to pass user as a prop from parent components anymore.
// Props drilled through multiple layersfunction Navbar({ user }: { user: User | null }) { return <nav>{user ? user.email : "Sign in"}</nav>;}import { useAuth } from "@/contexts/AuthContext";function Navbar() { // Access user directly from context — no props needed const { user, isLoading } = useAuth(); if (isLoading) return <nav>Loading...</nav>; return <nav>{user ? user.email : "Sign in"}</nav>;}Expected result: The Navbar component reads the user state directly from context, and it updates automatically when the user signs in or out.
Prevent unnecessary re-renders with useMemo
If the context value object is recreated on every render, every consumer re-renders even when the data has not changed
Prevent unnecessary re-renders with useMemo
If the context value object is recreated on every render, every consumer re-renders even when the data has not changed
When your context value is an object with multiple properties, React compares by reference — a new object means a 're-render all consumers' signal. Wrap the value in useMemo so it only changes when the actual data changes. This matters most when you have many context consumers or the provider re-renders frequently. For complex state management across multiple generated components, RapidDev's engineers have optimized Context performance in 600+ Lovable projects.
return ( <AuthContext.Provider value={{ user, isLoading }}> {children} </AuthContext.Provider>);import { useMemo } from "react";// Memoize the value so consumers only re-render when user or isLoading changesconst value = useMemo(() => ({ user, isLoading }), [user, isLoading]);return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider>);Expected result: Consumer components only re-render when the user or isLoading values actually change, not on every parent render.
Complete code example
1import { createContext, useContext, useState, useMemo, ReactNode } from "react";23type Theme = "light" | "dark";45interface ThemeContextType {6 theme: Theme;7 toggleTheme: () => void;8}910const ThemeContext = createContext<ThemeContextType | undefined>(undefined);1112export function ThemeProvider({ children }: { children: ReactNode }) {13 const [theme, setTheme] = useState<Theme>(() => {14 // Read saved preference from localStorage on initial load15 const saved = localStorage.getItem("theme");16 return (saved === "dark" ? "dark" : "light") as Theme;17 });1819 const toggleTheme = () => {20 setTheme(prev => {21 const next = prev === "light" ? "dark" : "light";22 localStorage.setItem("theme", next);23 // Update the HTML element class for Tailwind dark mode24 document.documentElement.classList.toggle("dark", next === "dark");25 return next;26 });27 };2829 // Stabilize the value object to prevent unnecessary consumer re-renders30 const value = useMemo(() => ({ theme, toggleTheme }), [theme]);3132 return (33 <ThemeContext.Provider value={value}>34 {children}35 </ThemeContext.Provider>36 );37}3839export function useTheme() {40 const context = useContext(ThemeContext);41 if (context === undefined) {42 throw new Error("useTheme must be used within a ThemeProvider");43 }44 return context;45}Best practices to prevent this
- Start with React Context for simple shared state like auth, theme, and language — it is built into React and requires no extra packages
- Always throw an error in your custom hook if the context is undefined — this catches missing Providers early instead of causing cryptic 'cannot read property' errors
- Wrap context values in useMemo to prevent unnecessary re-renders in consumer components
- Keep each context focused on one concern (auth, theme, cart) instead of creating a single giant context for everything
- Use Zustand instead of Context when you need state that persists across route changes without a Provider wrapper
- Avoid Redux in Lovable projects unless you specifically need its middleware ecosystem — it adds complexity with no benefit for typical use cases
- Place Providers in App.tsx outside the BrowserRouter so both route components and navigation components can access the shared state
- Type your context with TypeScript interfaces so the useContext hook provides autocomplete and compile-time safety
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Lovable.dev project (React + TypeScript + Tailwind) and I need to share state between multiple components. Currently I am passing props through 4-5 layers of components. Here is my current component structure: [paste your component tree or key components here] The data I need to share globally is: [describe the data, e.g., user auth state, cart items, theme preference] Please help me: 1. Decide whether I should use React Context, Zustand, or Redux for this use case 2. Create the context/store with proper TypeScript types 3. Show me how to wrap my App component with the provider 4. Show me how to consume the state in my child components
I need to share [user authentication state / theme preference / cart data] across multiple components without prop drilling. Create a React Context in @src/contexts/[ContextName].tsx with a typed provider and a custom hook. Wrap the App component in @src/App.tsx with the new provider. Update the components that currently receive this data as props to use the new hook instead.
Frequently asked questions
Should I use Context API or Redux in a Lovable project?
Use React Context for most Lovable projects. It is built into React, requires no extra packages, and handles common needs like auth state, theme, and user preferences. Redux adds significant complexity and is only justified if you need its middleware ecosystem for complex async patterns.
When should I use Zustand instead of React Context?
Use Zustand when you have state that is accessed by many components and you notice performance issues from Context re-renders. Zustand does not require a Provider wrapper and only re-renders the specific components that read the changed value, not all consumers.
Why do I get 'useContext returned undefined' in my Lovable app?
This means the component calling useContext is not wrapped inside the corresponding Provider component. Check your App.tsx file to make sure the Provider wraps the BrowserRouter and all routes. If the Provider is missing or placed inside a route, components outside that route cannot access the context.
How do I add Zustand to a Lovable project?
Prompt Lovable: 'Add the zustand package and create a store in src/stores/useCartStore.ts for managing shopping cart state with addItem, removeItem, and clearCart actions.' Lovable will install the package and generate a typed store file you can import in any component.
Does React Context cause performance problems?
Context re-renders every consumer when the value changes. For frequently changing values like mouse position or animation frames, this can cause lag. For values that change occasionally (auth state, theme toggle), Context is perfectly fine. Wrap the value in useMemo to minimize unnecessary re-renders.
Can I use multiple contexts in the same Lovable app?
Yes, and you should. Keep each context focused on one concern — an AuthContext for user sessions, a ThemeContext for appearance, a CartContext for e-commerce. Nest the Providers in App.tsx from outermost to innermost. Each context is independent.
What if I can't fix this myself?
If your app needs complex state management across many generated components — like syncing real-time data, optimistic updates, or cross-tab state — RapidDev's engineers have architected these patterns across 600+ Lovable projects and can implement a clean solution.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation