Explain the Context API in detail. How do you avoid performance issues with Context?

4 minintermediatereactcontextapidetailavoid

Quick Answer

The Context API provides a way to share data between components without prop drilling. It's built into React and consists of createContext, Provider, and useContext.

Detailed Answer

The Context API provides a way to share data between components without prop drilling. It's built into React and consists of createContext, Provider, and useContext.

Basic Context Implementation:

// 1. Create Context
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// 2. Create Provider Component
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []);

  const value = useMemo(() => ({
    theme,
    toggleTheme
  }), [theme, toggleTheme]);

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

// 3. Custom Hook for Using Context
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// 4. Using the Context
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header style={{ background: theme === 'light' ? 'white' : 'black' }}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} theme
      </button>
    </header>
  );
}

Performance Issues and Solutions:

Problem 1: Unnecessary Re-renders

// BAD - Causes all consumers to re-render when any value changes
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // This object is recreated on every render!
  const value = {
    user,
    setUser,
    theme,
    setTheme,
    notifications,
    setNotifications
  };

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

// GOOD - Split contexts by domain
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const value = useMemo(() => ({
    user,
    setUser
  }), [user]);

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

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const value = useMemo(() => ({
    theme,
    setTheme
  }), [theme]);

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

Problem 2: Expensive Context Values

// BAD - Expensive computation on every render
function DataProvider({ children }) {
  const [rawData, setRawData] = useState([]);
  
  // This runs on every render!
  const processedData = rawData.map(item => ({
    ...item,
    computed: expensiveCalculation(item)
  }));

  const value = { processedData, setRawData };
  
  return (
    <DataContext.Provider value={value}>
      {children}
    </DataContext.Provider>
  );
}

// GOOD - Memoize expensive computations
function DataProvider({ children }) {
  const [rawData, setRawData] = useState([]);
  
  const processedData = useMemo(() => {
    return rawData.map(item => ({
      ...item,
      computed: expensiveCalculation(item)
    }));
  }, [rawData]);

  const value = useMemo(() => ({
    processedData,
    setRawData
  }), [processedData]);

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

Advanced Pattern: Context with Selectors

// Create a context that only re-renders when specific values change
function createSelectorContext<T>() {
  const Context = createContext<T | undefined>(undefined);
  
  const Provider = ({ value, children }: { value: T; children: React.ReactNode }) => {
    return <Context.Provider value={value}>{children}</Context.Provider>;
  };
  
  const useSelector = <R>(selector: (value: T) => R): R => {
    const context = useContext(Context);
    if (context === undefined) {
      throw new Error('useSelector must be used within Provider');
    }
    
    const selectedValue = useMemo(() => selector(context), [context, selector]);
    return selectedValue;
  };
  
  return { Provider, useSelector };
}

// Usage
interface AppState {
  user: User | null;
  theme: string;
  notifications: Notification[];
}

const { Provider: AppProvider, useSelector: useAppSelector } = createSelectorContext<AppState>();

function UserProfile() {
  // Only re-renders when user changes, not when theme or notifications change
  const user = useAppSelector(state => state.user);
  
  return <div>{user?.name}</div>;
}

function ThemeButton() {
  // Only re-renders when theme changes
  const theme = useAppSelector(state => state.theme);
  
  return <button>Current theme: {theme}</button>;
}

Best Practices:

  1. Split contexts by domain - Don't put everything in one context
  2. Memoize context values - Use useMemo for the value object
  3. Use custom hooks - Create useContext wrappers with error handling
  4. Consider alternatives - For complex state, consider Redux or Zustand
  5. Avoid frequent updates - Batch updates when possible