useContext Hook

Difficulty: Intermediate

The useContext hook provides a way to pass data through the component tree without manually threading props through every level. It solves the problem known as 'prop drilling' - where intermediate components receive and forward props they don't actually use, just so deeply nested components can access them. Context is ideal for global or semi-global data like themes, authentication state, locale preferences, and UI settings.

To use context, you first create one with React.createContext(defaultValue). This returns an object with two components: Provider and Consumer (though the Consumer is rarely used now that useContext exists). The Provider wraps a portion of the component tree and accepts a value prop. Any component inside that Provider subtree can read the context value using useContext(MyContext).

The defaultValue passed to createContext is only used when a component calls useContext but there is no matching Provider above it in the tree. This is useful for testing or for standalone component usage. In most applications, the Provider always exists, so the default value acts as a fallback or type hint.

When the Provider's value changes, all components consuming that context re-render. This is an important performance consideration. If the value is an object created inline in the Provider's render (e.g., value={{ user, theme }}), a new object is created every render, causing all consumers to re-render even if the data hasn't changed. The solution is to memoize the value with useMemo.

Context is not a replacement for all state management. It works best for data that changes infrequently (theme, auth, locale). For frequently changing data or complex state logic, dedicated state management libraries or useReducer combined with context may be more appropriate. Context + useReducer is often called 'poor man's Redux' and is sufficient for many applications.

Code examples

Creating and consuming context

const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = React.useContext(ThemeContext);
  console.log('Button theme:', theme);
  return null;
}

function App() {
  console.log('App rendering');
  // Provider passes 'dark' to all descendants
  // In real JSX: <ThemeContext.Provider value="dark"><ThemedButton /></ThemeContext.Provider>
  return null;
}

// Without Provider, useContext returns the default value
ThemedButton();

Without a Provider, useContext returns the defaultValue passed to createContext. When wrapped in a Provider, it returns the Provider's value prop instead.

Context with state management

const UserContext = React.createContext(null);

function createUserProvider() {
  const user = { name: 'Alice', role: 'admin' };
  console.log('UserProvider created with:', user.name);
  return user;
}

function ProfileHeader(user) {
  console.log('ProfileHeader - name:', user.name);
  console.log('ProfileHeader - role:', user.role);
  return null;
}

const user = createUserProvider();
ProfileHeader(user);

In a real app, the Provider would wrap the tree and useContext would read the value. Here we simulate the data flow to show how context values are consumed by deeply nested components.

Multiple contexts

const ThemeContext = React.createContext('light');
const LanguageContext = React.createContext('en');

function StatusBar() {
  const theme = React.useContext(ThemeContext);
  const language = React.useContext(LanguageContext);
  console.log('Theme:', theme);
  console.log('Language:', language);
  console.log('Contexts are independent: true');
  return null;
}

StatusBar();

Components can consume multiple contexts. Each useContext call subscribes to one specific context. Contexts are independent - updating one does not affect consumers of another.

Key points

Concepts covered

useContext, createContext, Provider, context consumers, prop drilling