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

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

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