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

Using Context API or Redux with Lovable Components

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.

Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read~15 minAll Lovable projects (React 18+)March 2026RapidDev Engineering Team
TL;DR

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 undefined

The 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

1

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).

Before
typescript
// No shared state — each component fetches its own user data
// src/components/Navbar.tsx
function Navbar() {
const [user, setUser] = useState(null);
useEffect(() => { /* fetch user */ }, []);
return <nav>{user?.name}</nav>;
}
// src/components/Sidebar.tsx
function Sidebar() {
const [user, setUser] = useState(null);
useEffect(() => { /* fetch user again */ }, []);
return <aside>{user?.role}</aside>;
}
After
typescript
// src/contexts/AuthContext.tsx
import { 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.

2

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.

Before
typescript
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
After
typescript
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.

3

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.

Before
typescript
// Props drilled through multiple layers
function Navbar({ user }: { user: User | null }) {
return <nav>{user ? user.email : "Sign in"}</nav>;
}
After
typescript
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.

4

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.

Before
typescript
return (
<AuthContext.Provider value={{ user, isLoading }}>
{children}
</AuthContext.Provider>
);
After
typescript
import { useMemo } from "react";
// Memoize the value so consumers only re-render when user or isLoading changes
const 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

src/contexts/ThemeContext.tsx
1import { createContext, useContext, useState, useMemo, ReactNode } from "react";
2
3type Theme = "light" | "dark";
4
5interface ThemeContextType {
6 theme: Theme;
7 toggleTheme: () => void;
8}
9
10const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
11
12export function ThemeProvider({ children }: { children: ReactNode }) {
13 const [theme, setTheme] = useState<Theme>(() => {
14 // Read saved preference from localStorage on initial load
15 const saved = localStorage.getItem("theme");
16 return (saved === "dark" ? "dark" : "light") as Theme;
17 });
18
19 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 mode
24 document.documentElement.classList.toggle("dark", next === "dark");
25 return next;
26 });
27 };
28
29 // Stabilize the value object to prevent unnecessary consumer re-renders
30 const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
31
32 return (
33 <ThemeContext.Provider value={value}>
34 {children}
35 </ThemeContext.Provider>
36 );
37}
38
39export 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.

ChatGPT Prompt

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

Lovable Prompt

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.

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.