Skip to content

React Context Explained — Avoid Prop Drilling with Global State

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about React Context Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

React Context provides a way to pass data through the component tree without manually passing props at every level, solving prop drilling for shared application state.

What You'll Learn

  • What prop drilling is and how context solves it
  • How to create context with createContext
  • How to provide values with Context.Provider
  • How to consume context with useContext
  • When to use context vs other state management

Why It Matters

As your app grows, passing props through 5+ levels becomes tedious and brittle. Context lets you broadcast data to any component that needs it, keeping your code cleaner and more maintainable.

Real-World Use

Durga Antivirus Pro uses context for theme preferences, authenticated user data, notification settings, and application feature flags — data that many unrelated components need across the tree.

flowchart TD
    A[App Provider] --> B[Theme Context]
    A --> C[Auth Context]
    A --> D[Notification Context]
    B --> E[Header]
    B --> F[Sidebar]
    C --> G[Dashboard]
    C --> H[Settings]
    D --> G
    D --> I[Notification Badge]
    style A fill:#3b82f6,color:#fff

Creating and Providing Context

Create a context and wrap the component tree:

import { createContext, useContext, useState } from "react";

// 1. Create the context
const ThemeContext = createContext();

// 2. Create a provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  const toggleTheme = () => {
    setTheme(prev => prev === "light" ? "dark" : "light");
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Use the context in any child
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  const styles = {
    background: theme === "light" ? "#fff" : "#333",
    color: theme === "light" ? "#333" : "#fff",
    padding: "10px 20px",
    border: `1px solid ${theme === "light" ? "#ccc" : "#555"}`,
    borderRadius: "4px",
  };

  return (
    <button style={styles} onClick={toggleTheme}>
      Current: {theme} (Click to toggle)
    </button>
  );
}

// 4. Wrap your app
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

Expected output: A button that toggles between light and dark themes. The theme state lives in the provider and is accessible anywhere under it.

createContext creates a context object. The Provider component accepts a value prop that becomes available to all descendants. useContext reads the current value.

Multiple Contexts

Use multiple contexts for different concerns:

// Auth Context
const AuthContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (email, password) => {
    // Simulate login
    setUser({ email, name: email.split("@")[0] });
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Notification Context
const NotificationContext = createContext();

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  const addNotification = (message, type = "info") => {
    const id = Date.now();
    setNotifications(prev => [...prev, { id, message, type }]);
    setTimeout(() => {
      setNotifications(prev => prev.filter(n => n.id !== id));
    }, 5000);
  };

  return (
    <NotificationContext.Provider value={{ notifications, addNotification }}>
      {children}
    </NotificationContext.Provider>
  );
}

// Consumer component using both contexts
function DashboardHeader() {
  const { user, logout } = useContext(AuthContext);
  const { notifications, addNotification } = useContext(NotificationContext);

  const handleLogout = () => {
    addNotification("You have been logged out", "info");
    logout();
  };

  return (
    <header>
      <span>Welcome, {user?.name || "Guest"}</span>
      <span>Notifications: {notifications.length}</span>
      <button onClick={handleLogout}>Logout</button>
    </header>
  );
}

// App with nested providers
function App() {
  return (
    <AuthProvider>
      <NotificationProvider>
        <DashboardHeader />
      </NotificationProvider>
    </AuthProvider>
  );
}

Expected output: A header that shows user info and notification count, both from separate contexts. Notifications auto-dismiss after 5 seconds.

Multiple contexts can be nested. Each context manages one concern. This is better than one large context that holds unrelated state, because components only re-render when the specific context they consume changes.

Context with useReducer

Combine context with useReducer for complex state:

import { createContext, useContext, useReducer } from "react";

// Reducer
function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM": {
      const existing = state.items.find(i => i.id === action.item.id);
      if (existing) {
        return {
          ...state,
          items: state.items.map(i =>
            i.id === action.item.id
              ? { ...i, quantity: i.quantity + 1 }
              : i
          )
        };
      }
      return {
        ...state,
        items: [...state.items, { ...action.item, quantity: 1 }]
      };
    }
    case "REMOVE_ITEM":
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.id)
      };
    case "CLEAR_CART":
      return { ...state, items: [] };
    default:
      return state;
  }
}

// Context with state and dispatch
const CartContext = createContext();

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  return (
    <CartContext.Provider value={{ cart: state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

// Custom hook
function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error("useCart must be used within CartProvider");
  return context;
}

// Components using the cart
function AddToCartButton({ product }) {
  const { dispatch } = useCart();

  return (
    <button onClick={() => dispatch({ type: "ADD_ITEM", item: product })}>
      Add to Cart
    </button>
  );
}

function CartBadge() {
  const { cart } = useCart();
  const count = cart.items.reduce((sum, item) => sum + item.quantity, 0);

  return <span className="badge">{count > 0 ? count : ""}</span>;
}

Expected output: Clicking "Add to Cart" updates the badge count. The dispatch function is available to all components through the context.

useReducer + Context is a lightweight alternative to Redux. The context provides both the state and the dispatch function, giving child components full access to the state management pattern.

Context Performance

Memoize context values to prevent unnecessary re-renders:

import { createContext, useContext, useState, useMemo, useCallback } from "react";

const SearchContext = createContext();

function SearchProvider({ children }) {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const search = useCallback(async (q) => {
    setQuery(q);
    if (!q) { setResults([]); return; }
    const res = await fetch(`/api/search?q=${q}`);
    const data = await res.json();
    setResults(data);
  }, []);

  // Memoize the context value to prevent re-renders when parent re-renders
  const value = useMemo(() => ({
    query,
    results,
    search
  }), [query, results, search]);

  return (
    <SearchContext.Provider value={value}>
      {children}
    </SearchContext.Provider>
  );
}

Expected output: Components that consume SearchContext only re-render when query or results actually change, not when the provider's parent re-renders.

Without useMemo, every re-render of the provider creates a new context value object, causing all consumers to re-render regardless of whether the actual data changed.

Context with TypeScript

Typed contexts provide safety and autocompletion:

import { createContext, useContext, useState, ReactNode } from "react";

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user" | "viewer";
}

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    const response = await fetch("/api/auth/login", {
      method: "POST",
      body: JSON.stringify({ email, password })
    });
    const data = await response.json();
    setUser(data.user);
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

Expected output: TypeScript ensures you access valid properties. useAuth().user.name is typed as string. The error boundary catches misuse.

Creating a typed context and a custom hook with error checking is the recommended pattern. It provides autocompletion, type safety, and clear error messages when the context is used outside the provider.

Common Mistakes

  1. One massive context for all state — Creates unnecessary re-renders. Split contexts by concern: AuthContext, ThemeContext, CartContext.

  2. Not memoizing context values — Every parent render creates a new context object, re-rendering all consumers. Use useMemo.

  3. Default values that are not usefulcreateContext(null) makes TypeScript usage hard without error checking. Provide meaningful defaults or type with undefined and check.

  4. Nesting providers too deep — Too many provider wrappers make the component tree hard to read. Consider composition patterns to flatten.

  5. Using context for component-local state — Context is for shared state. Keep component-specific state local with useState.

Practice Questions

  1. What problem does React Context solve? Prop drilling — passing props through many levels of components that do not need them.

  2. How do you create a context? const MyContext = createContext(defaultValue). The default value is used when there is no Provider ancestor.

  3. How do you consume context in a component? Use the useContext(MyContext) hook. It returns the current context value from the nearest Provider.

  4. How do you prevent unnecessary re-renders with context? Memoize the context value with useMemo and split global state into separate contexts.

  5. When should you avoid context? For high-frequency updates (animations, real-time mouse position) where many components subscribe. Use other patterns like zustand or recoil.

Challenge

Build a MultiTabForm where context holds the form data across tabs. Each tab component reads and writes to the shared form context. A summary tab shows all data. Implement validation state per field in the context.

FAQ

Can I update context from a child component?

Yes, include setter functions in the context value. Children can call them to update shared state.

Does context replace Redux?

For many apps, yes. Context + useReducer replaces Redux for medium-sized apps. Redux adds tooling and middleware for complex state.

What happens if no Provider is found?

The component uses the default value passed to createContext(defaultValue).

Can I have multiple providers of the same context?

Yes, nested providers override outer ones. The closest provider wins.

Is context good for performance?

Used correctly, yes. Split by concern and memoize values. One large context can hurt performance.

Mini Project

Build a UserPreferencesApp with three contexts: ThemeContext (dark/light mode), LanguageContext (en/es/fr), and LayoutContext (sidebar collapsed, font size). Create a Settings panel where users can change any preference. All components (Header, Sidebar, Main Content) should react to changes instantly. Persist preferences to localStorage. Memoize all context values.

What's Next

Continue with reducers and memo:

React Reducers, React Memo, React Context

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro