Back to home

Context API

The Context API provides a way to share values between components without explicitly passing props.

What is Context?

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

jsx
// Without Context (prop drilling)
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <Header theme={theme} />
  );
}

function Header({ theme }) {
  return (
    <nav>
      <Logo />
      <UserMenu theme={theme} />
    </nav>
  );
}

function UserMenu({ theme }) {
  return (
    <div className={theme === 'dark' ? 'dark-menu' : 'light-menu'}>
      {/* Menu content */}
    </div>
  );
}

Creating Context

You can create a context using React.createContext() and provide a default value.

jsx
import { createContext } from 'react';

// Create a context with a default value
const ThemeContext = createContext('light');

// You can also create a context with a more complex default value
const UserContext = createContext({
  name: '',
  isAuthenticated: false,
  preferences: {}
});

Context Provider

The Provider component allows consuming components to subscribe to context changes.

jsx
import { createContext, useState } from 'react';

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

// Create a provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  
  // The value prop will be available to any component that consumes this context
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Wrap your app with the provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <MainContent />
      <Footer />
    </ThemeProvider>
  );
}

Consuming Context

There are several ways to consume context in your components.

jsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

// Method 1: Using the useContext hook (recommended)
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button
      className={theme === 'dark' ? 'dark-btn' : 'light-btn'}
      onClick={toggleTheme}
    >
      Toggle Theme
    </button>
  );
}

// Method 2: Using Context.Consumer
function ThemedHeader() {
  return (
    <ThemeContext.Consumer>
      {({ theme }) => (
        <header className={theme === 'dark' ? 'dark-header' : 'light-header'}>
          <h1>My App</h1>
        </header>
      )}
    </ThemeContext.Consumer>
  );
}

// Method 3: Static contextType (only for class components)
class ThemedFooter extends React.Component {
  static contextType = ThemeContext;
  
  render() {
    const { theme } = this.context;
    return (
      <footer className={theme === 'dark' ? 'dark-footer' : 'light-footer'}>
        © 2023 My App
      </footer>
    );
  }
}

Multiple Contexts

You can use multiple contexts in your application by nesting providers.

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

// Create contexts
const ThemeContext = createContext();
const UserContext = createContext();

// Create providers
function AppProviders({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        {children}
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Custom hooks to use contexts
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function useUser() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// Usage in components
function ProfilePage() {
  const { theme } = useTheme();
  const { user } = useUser();
  
  if (!user) {
    return <p>Please log in</p>;
  }
  
  return (
    <div className={theme === 'dark' ? 'dark-profile' : 'light-profile'}>
      <h2>{user.name}'s Profile</h2>
      {/* Profile content */}
    </div>
  );
}

Context with Reducer

Combining context with useReducer for more complex state management.

jsx
import { createContext, useReducer, useContext } from 'react';

// Define initial state
const initialState = {
  todos: [],
  loading: false,
  error: null
};

// Create reducer function
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };
    case 'SET_LOADING':
      return {
        ...state,
        loading: action.payload
      };
    case 'SET_ERROR':
      return {
        ...state,
        error: action.payload
      };
    default:
      return state;
  }
}

// Create context
const TodoContext = createContext();

// Create provider
function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  // Actions
  const addTodo = (todo) => {
    dispatch({ type: 'ADD_TODO', payload: todo });
  };
  
  const removeTodo = (id) => {
    dispatch({ type: 'REMOVE_TODO', payload: id });
  };
  
  const fetchTodos = async () => {
    dispatch({ type: 'SET_LOADING', payload: true });
    try {
      const response = await fetch('https://api.example.com/todos');
      const data = await response.json();
      
      data.forEach(todo => {
        addTodo(todo);
      });
      
      dispatch({ type: 'SET_ERROR', payload: null });
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message });
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };
  
  return (
    <TodoContext.Provider value={{ 
      todos: state.todos,
      loading: state.loading,
      error: state.error,
      addTodo,
      removeTodo,
      fetchTodos
    }}>
      {children}
    </TodoContext.Provider>
  );
}

// Custom hook to use the context
function useTodos() {
  const context = useContext(TodoContext);
  if (context === undefined) {
    throw new Error('useTodos must be used within a TodoProvider');
  }
  return context;
}

// Usage in components
function TodoList() {
  const { todos, loading, error, fetchTodos, removeTodo } = useTodos();
  
  useEffect(() => {
    fetchTodos();
  }, []);
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

Common Interview Questions

  • What problem does the Context API solve?
  • When should you use Context versus props?
  • What are the performance implications of using Context?
  • How does Context compare to state management libraries like Redux?
  • What are the limitations of the Context API?