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:
- Split contexts by domain - Don't put everything in one context
- Memoize context values - Use
useMemofor the value object - Use custom hooks - Create
useContextwrappers with error handling - Consider alternatives - For complex state, consider Redux or Zustand
- Avoid frequent updates - Batch updates when possible