Tell us about the useContext hook, how it works, where it's applied and for what purpose? When is it better to use it, and when is it better to refuse?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React #Hooks

Brief Answer

useContext is a React hook that allows you to access values from React Context It provides a way to pass data deep into the component tree without prop drilling.

Key features:

  • Simplifies access to context compared to Consumer
  • Re-renders component when context value changes
  • Doesn’t replace Redux or other state management systems
  • Works only with contexts created through React.createContext()

Usage example:

import React, { createContext, useContext } from 'react';
 
const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}
 
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
 
function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  return (
    <button className={theme}>
      Button with theme {theme}
    </button>
  );
}

Full Answer

The useContext hook is one of React’s built-in hooks that provides a way to subscribe to context and get its values in functional components. It’s a modern alternative to the old Consumer API, making code more readable and concise. 🌟

How useContext works?

useContext takes a context object (the value returned from React.createContext) and returns the current context value for that context:

const value = useContext(MyContext);

A component calling useContext will always re-render when the context value changes. React takes the current context value from the nearest matching Provider above the component in the tree.

Creating and using context

import React, { createContext, useContext } from 'react';
 
// 1. Create context with initial value
const UserContext = createContext({
  name: 'Guest',
  isLoggedIn: false
});
 
// 2. Provider component
function UserProvider({ children }) {
  const [user, setUser] = React.useState({
    name: 'Alexander',
    isLoggedIn: true
  });
  
  return (
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
}
 
// 3. Component using context
function UserProfile() {
  const user = useContext(UserContext);
  
  return (
    <div>
      <h1>Hello, {user.name}!</h1>
      <p>Status: {user.isLoggedIn ? 'Logged in' : 'Guest'}</p>
    </div>
  );
}
 
// 4. Usage in application
function App() {
  return (
    <UserProvider>
      <UserProfile />
    </UserProvider>
  );
}

Main useContext applications

1. Application theming

import React, { createContext, useContext, useState } from 'react';
 
const ThemeContext = createContext();
 
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>
  );
}
 
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button 
      className={`btn btn-${theme}`}
      onClick={toggleTheme}
    >
      Switch to {theme === 'light' ? 'dark' : 'light'} theme
    </button>
  );
}
 
function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <h1>App with theming</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

2. Global user state

import React, { createContext, useContext, useReducer } from 'react';
 
const AuthContext = createContext();
 
const authReducer = (state, action) => {
  switch (action.type) {
    case 'LOGIN':
      return {
        user: action.payload,
        isAuthenticated: true
      };
    case 'LOGOUT':
      return {
        user: null,
        isAuthenticated: false
      };
    default:
      return state;
  }
};
 
function AuthProvider({ children }) {
  const [state, dispatch] = useReducer(authReducer, {
    user: null,
    isAuthenticated: false
  });
  
  const login = (userData) => {
    dispatch({ type: 'LOGIN', payload: userData });
  };
  
  const logout = () => {
    dispatch({ type: 'LOGOUT' });
  };
  
  return (
    <AuthContext.Provider value={{ ...state, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
 
function UserProfile() {
  const { user, isAuthenticated, logout } = useContext(AuthContext);
  
  if (!isAuthenticated) {
    return <div>Please log in</div>;
  }
  
  return (
    <div>
      <h2>Hello, {user.name}!</h2>
      <button onClick={logout}>Log out</button>
    </div>
  );
}

3. Localization/translations

import React, { createContext, useContext } from 'react';
 
const translations = {
  en: {
    greeting: 'Hello',
    welcome: 'Welcome to our app'
  },
  ru: {
    greeting: 'Привет',
    welcome: 'Добро пожаловать в наше приложение'
  }
};
 
const LanguageContext = createContext();
 
function LanguageProvider({ children, defaultLanguage = 'en' }) {
  const [language, setLanguage] = React.useState(defaultLanguage);
  
  const t = (key) => {
    return translations[language]?.[key] || key;
  };
  
  return (
    <LanguageContext.Provider value={{ language, setLanguage, t }}>
      {children}
    </LanguageContext.Provider>
  );
}
 
function Greeting() {
  const { t, language, setLanguage } = useContext(LanguageContext);
  
  return (
    <div>
      <h1>{t('greeting')}!</h1>
      <p>{t('welcome')}</p>
      <select 
        value={language} 
        onChange={(e) => setLanguage(e.target.value)}
      >
        <option value="en">English</option>
        <option value="ru">Русский</option>
      </select>
    </div>
  );
}

useContext advantages

1. Eliminating prop drilling

// ❌ Without context - prop drilling
function App() {
  const theme = 'dark';
  return <Layout theme={theme} />;
}
 
function Layout({ theme }) {
  return <Header theme={theme} />;
}
 
function Header({ theme }) {
  return <Button theme={theme} />;
}
 
function Button({ theme }) {
  return <button className={`btn-${theme}`}>Button</button>;
}
 
// ✅ With context - direct access
const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  );
}
 
function Layout() {
  return <Header />;
}
 
function Header() {
  return <Button />;
}
 
function Button() {
  const theme = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>Button</button>;
}

2. Simplifying component APIs

// ❌ Complex API with many props
function ComplexComponent({ 
  theme, 
  locale, 
  permissions, 
  userSettings,
  // ... 10 more props
}) {
  // ...
}
 
// ✅ Clean API with context
function CleanComponent() {
  // Get all needed data from contexts
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);
  const permissions = useContext(PermissionsContext);
  const userSettings = useContext(UserSettingsContext);
  
  // ...
}

When to use useContext

Use useContext when:

  1. Deep data passing - when you need to pass data many levels down the component tree
  2. Global state - themes, user data, application settings
  3. State between sibling components - when multiple components need the same data
  4. Simple application - for small applications without complex state management logic
// Good useContext application
const AppSettingsContext = createContext();
 
function App() {
  return (
    <AppSettingsContext.Provider value={{
      theme: 'dark',
      language: 'en',
      notifications: true
    }}>
      <MainLayout />
    </AppSettingsContext.Provider>
  );
}
 
function NotificationPanel() {
  const { notifications } = useContext(AppSettingsContext);
  
  if (!notifications) return null;
  
  return <div>Notification panel</div>;
}

When to avoid useContext

Avoid useContext when:

  1. Complex state management - for applications with complex logic, multiple state updates, better use Redux, Zustand or other libraries
  2. Frequent updates - if context updates frequently, it can cause unnecessary re-renders of many components
  3. Only for passing functions - if you’re only passing functions through context, consider other approaches
  4. Simple hierarchy - if data is passed maximum 2-3 levels down, props might be simpler
// Bad useContext application
const ExpensiveDataContext = createContext();
 
// ❌ Context updates too frequently
function DataProvider({ children }) {
  const [data, setData] = useState([]);
  
  // Updating data every second
  useEffect(() => {
    const interval = setInterval(() => {
      setData(prev => [...prev, Date.now()]);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <ExpensiveDataContext.Provider value={data}>
      {children}
    </ExpensiveDataContext.Provider>
  );
}
 
// All components will re-render every second
function ExpensiveComponent() {
  const data = useContext(ExpensiveDataContext);
  // Expensive operation with data
  return <div>{JSON.stringify(data)}</div>;
}

Optimizing useContext

1. Splitting contexts

// ❌ One large context
const AppContext = createContext();
 
function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);
  
  return (
    <AppContext.Provider value={{
      theme, setTheme,
      user, setUser,
      notifications, setNotifications
    }}>
      {children}
    </AppContext.Provider>
  );
}
 
// ✅ Split contexts
const ThemeContext = createContext();
const UserContext = createContext();
const NotificationsContext = createContext();
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}
 
// Components re-render only when their data changes
function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  // Re-renders only when theme changes
}
 
function UserProfile() {
  const { user } = useContext(UserContext);
  // Re-renders only when user changes
}

2. Memoizing context values

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [accentColor, setAccentColor] = useState('blue');
  
  // ❌ New object value on every render
  const contextValue = {
    theme,
    setTheme,
    accentColor,
    setAccentColor
  };
  
  // ✅ Memoizing context value
  const contextValue = React.useMemo(() => ({
    theme,
    setTheme,
    accentColor,
    setAccentColor
  }), [theme, setTheme, accentColor, setAccentColor]);
  
  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

3. Creating custom hooks

// Custom hook for working with context
function useTheme() {
  const context = useContext(ThemeContext);
  
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  
  return context;
}
 
// Usage
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button 
      className={`btn-${theme}`}
      onClick={toggleTheme}
    >
      Toggle theme
    </button>
  );
}

Common Mistakes

1. Using useContext without Provider

// ❌ Error: using context without provider
const ThemeContext = createContext('light');
 
function Button() {
  const theme = useContext(ThemeContext); // Will be 'light' (default value)
  return <button className={theme}>Button</button>;
}
 
// ✅ Correctly: using with provider
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  );
}

2. Directly mutating context object

// ❌ Error: directly mutating context object
const UserContext = createContext();
 
function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  // Incorrect object mutation
  const updateUser = (field, value) => {
    user[field] = value; // Mutates state directly
    setUser(user); // Won't trigger re-render
  };
  
  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {children}
    </UserContext.Provider>
  );
}
 
// ✅ Correctly: creating new object
function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  const updateUser = (field, value) => {
    setUser(prev => ({ ...prev, [field]: value })); // Creates new object
  };
  
  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {children}
    </UserContext.Provider>
  );
}

3. Creating context inside component

// ❌ Error: creating context inside component
function App() {
  // Context is created on every render
  const ThemeContext = createContext('light');
  
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  );
}
 
// ✅ Correctly: creating context at top level
const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  );
}

4. Using useContext in conditional expressions

// ❌ Error: using hook in condition
function Component() {
  if (someCondition) {
    const value = useContext(MyContext); // Hook rules error!
    // ...
  }
  
  // ...
}
 
// ✅ Correctly: always call hooks at top level
function Component() {
  const value = useContext(MyContext); // Correctly
  
  if (someCondition) {
    // Use value inside condition
    // ...
  }
  
  // ...
}

Comparison with other approaches

useContext vs Redux

CriterionuseContextRedux
ComplexitySimpleComplex
Bundle size0 (built-in)~2KB
DebuggingLimitedExtended (Redux DevTools)
ScalabilityLimitedHigh
Learning curveLowHigh
Suitable forSmall applicationsComplex applications
// useContext for simple application
const AppStateContext = createContext();
 
function AppStateProvider({ children }) {
  const [state, setState] = useState({
    user: null,
    theme: 'light',
    notifications: []
  });
  
  return (
    <AppStateContext.Provider value={[state, setState]}>
      {children}
    </AppStateContext.Provider>
  );
}
 
// Redux for complex application
import { createStore } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
 
const store = createStore(rootReducer);
 
function UserProfile() {
  const user = useSelector(state => state.user);
  const dispatch = useDispatch();
  
  // ...
}

useContext vs Props

CriterionuseContextProps
ReadabilityHigh (less code)Medium (many props)
ExplicitnessLow (dependencies hidden)High (everything passed explicitly)
FlexibilityLimitedHigh
PerformanceMediumHigh
MaintainabilityMediumHigh
// Props - explicit data passing
function Parent() {
  const theme = 'dark';
  return <Child theme={theme} />;
}
 
function Child({ theme }) {
  return <GrandChild theme={theme} />;
}
 
function GrandChild({ theme }) {
  return <button className={theme}>Button</button>;
}
 
// useContext - implicit data passing
const ThemeContext = createContext('light');
 
function Parent() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  );
}
 
function Child() {
  return <GrandChild />;
}
 
function GrandChild() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Button</button>;
}

Summary

useContext is a React hook for:

  • Accessing context values in functional components
  • Eliminating prop drilling when passing data deep down the tree
  • Simplifying component APIs by removing intermediate props

When to use:

  • Deep data passing (3+ levels)
  • Global state (themes, user data)
  • State between sibling components
  • Simple applications without complex state management logic

When to avoid:

  • Complex state management (better Redux/MobX)
  • Frequent context updates (may cause performance issues)
  • When data is passed 1-2 levels down (props are simpler)
  • Only for passing functions (consider other approaches)

Best practices:

  • Split contexts by data types
  • Use useMemo to memoize context values
  • Create custom hooks for working with contexts
  • Check for provider presence in custom hooks
  • Don’t create contexts inside components

useContext is a powerful tool for state management in React applications, helping to avoid prop drilling and make code more readable. However, it’s important to understand when to use it and when to prefer more specialized solutions. 🚀


Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪