A Beginner-Friendly Guide to the Context API in React
ReactContext APIState Management

A Beginner-Friendly Guide to the Context API in React

January 29, 2025
10 min read
Salman Izhar

A Beginner-Friendly Guide to the Context API in React

You've built a few React components. Things are going well.

Then you need to pass some data from your top-level component down to a component buried three levels deep.

So you pass it as a prop. Then pass it again. And again.


  
    
      
    
  

The components in between don't even USE that data. They're just passing it along like a game of hot potato.

This is called prop drilling. And it's annoying.

That's exactly what the Context API solves.

The Prop Drilling Problem

Let me show you why this gets messy fast.

Say you're building a theme switcher. You need theme data in your Header (for the logo color) and in your Footer (for background color).

Without Context:

function App() {
  const [theme, setTheme] = useState('light')
  
  return (
    
) } function Main({ theme }) { return (
) } function Sidebar({ theme }) { return (
) } // Nav finally uses it function Nav({ theme }) { return }

Look at all that passing! Main and Sidebar don't care about theme. They're just middlemen.

Now imagine you need to add more data. User info. Language preference. Notifications.

Every component becomes a prop relay station.

How Context API Works

Context is like a broadcast system. Instead of passing data through every level, you broadcast it once. Any component can tune in and listen.

Here's the same theme example with Context:

import { createContext, useContext, useState } from 'react'

// 1. Create a context
const ThemeContext = createContext()

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

// 3. Create a hook for easy access
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

// 4. Wrap your app
function App() {
  return (
    
      
) } // 5. Use it anywhere, without prop drilling! function Nav() { const { theme } = useTheme() return } function ThemeToggle() { const { theme, setTheme } = useTheme() return ( ) }

See the difference? Main and Sidebar completely disappear from the data flow. Only the components that actually need the theme access it directly.

Building Your First Context: A Real Example

Let's build something practical-a shopping cart context.

Step 1: Create the Context

import { createContext, useContext, useState } from 'react'

const CartContext = createContext()

Simple. This creates an empty context object.

Step 2: Build the Provider

This is where your state lives.

function CartProvider({ children }) {
  const [items, setItems] = useState([])
  
  const addItem = (product) => {
    setItems(prev => {
      // Check if item already exists
      const existing = prev.find(item => item.id === product.id)
      
      if (existing) {
        // Increase quantity
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      }
      
      // Add new item
      return [...prev, { ...product, quantity: 1 }]
    })
  }
  
  const removeItem = (productId) => {
    setItems(prev => prev.filter(item => item.id !== productId))
  }
  
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeItem(productId)
      return
    }
    
    setItems(prev =>
      prev.map(item =>
        item.id === productId ? { ...item, quantity } : item
      )
    )
  }
  
  const clearCart = () => setItems([])
  
  const total = items.reduce((sum, item) => {
    return sum + (item.price * item.quantity)
  }, 0)
  
  const itemCount = items.reduce((count, item) => count + item.quantity, 0)
  
  const value = {
    items,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    total,
    itemCount,
  }
  
  return (
    
      {children}
    
  )
}

This provider:

  • Holds the cart state
  • Provides functions to modify it
  • Calculates derived values (total, itemCount)
  • Makes everything available through value

Step 3: Create a Custom Hook

This makes the API nicer to use.

function useCart() {
  const context = useContext(CartContext)
  
  if (context === undefined) {
    throw new Error('useCart must be used within CartProvider')
  }
  
  return context
}

The error check is important. It catches mistakes where you forget to wrap components in the provider.

Step 4: Wrap Your App

function App() {
  return (
    
      
) }

Everything inside CartProvider can now access the cart.

Step 5: Use It Anywhere

function ProductCard(props) {
  const product = props.product
  const addItem = useCart((state) => state.addItem)
  
  return (
    

Product Name

Price

) } function CartButton() { const itemCount = useCart((state) => state.itemCount) return ( ) } function CartSummary() { const items = useCart((state) => state.items) const total = useCart((state) => state.total) const removeItem = useCart((state) => state.removeItem) return (
Cart items display here
Total amount
) }

Look how clean this is! No props. No drilling. Just direct access to cart data and functions.

Another Example: Authentication Context

This is probably the most common use case for Context.

const AuthContext = createContext()

function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  
  // Check if user is logged in on mount
  useEffect(() => {
    const checkAuth = async () => {
      try {
        const response = await fetch('/api/me')
        if (response.ok) {
          const userData = await response.json()
          setUser(userData)
        }
      } catch (error) {
        console.error('Auth check failed:', error)
      } finally {
        setLoading(false)
      }
    }
    
    checkAuth()
  }, [])
  
  const login = async (email, password) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })
    
    if (response.ok) {
      const userData = await response.json()
      setUser(userData)
      return { success: true }
    }
    
    return { success: false, error: 'Invalid credentials' }
  }
  
  const logout = async () => {
    await fetch('/api/logout', { method: 'POST' })
    setUser(null)
  }
  
  const value = {
    user,
    loading,
    login,
    logout,
    isAuthenticated: !!user,
  }
  
  return (
    
      {children}
    
  )
}

function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

// Usage
function LoginButton() {
  const { isAuthenticated, logout } = useAuth()
  
  if (isAuthenticated) {
    return 
  }
  
  return 
}

function Profile() {
  const { user, loading } = useAuth()
  
  if (loading) return 
Loading...
if (!user) return
Please log in
return
Welcome, {user.name}!
}

Now any component can check auth status or trigger login/logout. No prop drilling required.

Best Practices

1. One Context, One Purpose

Don't dump everything into a single context.

Bad:

const AppContext = createContext() // user, theme, cart, everything!

Good:

const AuthContext = createContext()
const ThemeContext = createContext()
const CartContext = createContext()

2. Always Create a Custom Hook

Makes the API cleaner and catches mistakes.

function useCart() {
  const context = useContext(CartContext)
  if (!context) {
    throw new Error('useCart must be used within CartProvider')
  }
  return context
}

3. Keep Provider Logic Clean

If your provider gets complex, extract logic to custom hooks.

function CartProvider({ children }) {
  const cart = useCartLogic() // Custom hook with all the logic
  
  return (
    
      {children}
    
  )
}

4. Don't Overuse Context

Context is great for:

  • Theme data
  • User authentication
  • Language/locale
  • Global UI state (modals, notifications)

Context is NOT great for:

  • Data that changes frequently
  • Data only used by a few components
  • Complex state logic (use Zustand or Redux instead)

Common Mistakes to Avoid

Mistake 1: Forgetting the Provider

// This will error!
function MyComponent() {
  const { theme } = useTheme() // Error: used outside provider
  return 
...
}

Always wrap your app (or the part that needs it) in the provider.

Mistake 2: Creating New Objects in Value

// Bad - creates new object on every render

Every render creates a new object, causing all consumers to re-render.

Fix: Use useMemo or put it in state
const value = useMemo(() => ({ theme, setTheme }), [theme])

Mistake 3: Too Many Re-renders

If your context value changes often, all consumers re-render.

Solution: Split contexts or use state management libraries for frequently-changing data.

When Context Clicks

I remember building my first real app with Context. A dashboard with user data, settings, and notifications.

Before Context, I was passing props through 7 components. It was a nightmare to refactor.

With Context:


  
    
      
    
  

Any component could access what it needed. Adding new features became easy. The code felt clean again.

That's when Context clicked for me.

The Bottom Line

Context API solves prop drilling. That's its job.

It's not a full state management solution. It's not Redux. It's not Zustand.

It's a way to share data without passing props.

For theme, auth, and UI state? Perfect.

For complex app state? Consider other options.

But for most projects, Context is exactly what you need.

---

Want to see Context in action? Check out my guide on building authentication in Next.js using Context API.
Get More Like This

Want articles like this in your inbox?

Join developers and founders who get practical insights on frontend, SaaS, and building better products.

S

Written by Salman Izhar

Frontend Developer specializing in React, Next.js, and building high-converting web applications.

Learn More