DEV Community

Cover image for 🧠 Mastering React Context API: From Basics to Scalable Architectures
Abhinav
Abhinav

Posted on

🧠 Mastering React Context API: From Basics to Scalable Architectures

🚩 Why Use Context API?

Before diving in, let’s ground ourselves:

The Context API is React’s built-in dependency injection system β€” it lets us pass data deeply through a tree without prop drilling.

It’s great for:

  • Global themes
  • Auth/session data
  • Localization (i18n)
  • Feature flags
  • UI state (modals, drawers)

But it's not a state management library like Redux or Zustand.


🟒 Level 1: The Basics β€” What is Context?

πŸ›  Creating Context

// theme-context.js
import { createContext } from 'react';

export const ThemeContext = createContext('light');
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ Providing Context

// App.jsx
import { ThemeContext } from './theme-context';

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

πŸ“¬ Consuming Context

// Layout.jsx
import { useContext } from 'react';
import { ThemeContext } from './theme-context';

function Layout() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>Hello</div>;
}
Enter fullscreen mode Exit fullscreen mode

🟑 Level 2: Context with State

Pass a dynamic state (e.g. toggle theme).

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

const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));

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

export const useTheme = () => useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode
// App.jsx
import { ThemeProvider, useTheme } from './ThemeProvider';

function ThemeToggler() {
  const { toggle } = useTheme();
  return <button onClick={toggle}>Toggle Theme</button>;
}

function App() {
  return (
    <ThemeProvider>
      <ThemeToggler />
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

βœ… This pattern (provider + custom hook) is scalable and clean.


🟠 Level 3: Performance Gotchas

❌ Problem

Any component consuming the context will re-render when the context value changes β€” even if it only uses part of it.

<ThemeContext.Provider value={{ theme, toggle }} />
Enter fullscreen mode Exit fullscreen mode

This object is re-created on every render β†’ causes unnecessary re-renders.

βœ… Solution

Memoize the value:

const value = useMemo(() => ({ theme, toggle }), [theme]);
<ThemeContext.Provider value={value} />
Enter fullscreen mode Exit fullscreen mode

πŸ”΅ Level 4: Splitting Context for Performance

If you have a large object:

<Context.Provider value={{ user, setUser, theme, setTheme }} />
Enter fullscreen mode Exit fullscreen mode

Then all consumers of theme will re-render when user changes 😞

βœ… Solution: Split contexts

const UserContext = createContext();
const ThemeContext = createContext();

// Provide separately
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <App />
  </ThemeContext.Provider>
</UserContext.Provider>
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ One responsibility per context.


πŸ”΄ Level 5: Composing Context Providers

When your app needs multiple global states (auth, theme, lang), nesting gets ugly.

βœ… Solution: composeProviders utility

// ComposeProviders.js
export const composeProviders = (...providers) =>
  providers.reduce(
    (AccumulatedProviders, CurrentProvider) =>
      ({ children }) =>
        (
          <AccumulatedProviders>
            <CurrentProvider>{children}</CurrentProvider>
          </AccumulatedProviders>
        ),
    ({ children }) => <>{children}</>
  );
Enter fullscreen mode Exit fullscreen mode
// App.jsx
import { AuthProvider } from './auth';
import { ThemeProvider } from './theme';
import { composeProviders } from './ComposeProviders';

const AppProviders = composeProviders(AuthProvider, ThemeProvider);

function App() {
  return (
    <AppProviders>
      <Main />
    </AppProviders>
  );
}
Enter fullscreen mode Exit fullscreen mode

βœ… Now your App.jsx stays clean and scalable.


🧩 Level 6: Context vs Redux/MobX/Zustand

Feature Context API Zustand / Redux
Built-in βœ… ❌ (external)
Async logic ❌ (needs hooks) βœ…
DevTools support ❌ βœ…
Performance on large state ❌ (manual split needed) βœ…
Boilerplate Minimal Medium-High (Redux)

Use Context API for static or small shared state.
Use state libraries when you need time-travel debugging, middleware, or large-scale updates.


🧠 Level 7: Custom Hook Patterns with Context

// ModalContext.js
const ModalContext = createContext();

export const ModalProvider = ({ children }) => {
  const [modal, setModal] = useState(null);

  const show = (name) => setModal(name);
  const hide = () => setModal(null);

  const value = useMemo(() => ({ modal, show, hide }), [modal]);

  return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>;
};

export const useModal = () => {
  const context = useContext(ModalContext);
  if (!context) throw new Error('useModal must be inside ModalProvider');
  return context;
};
Enter fullscreen mode Exit fullscreen mode

This guards against misuse and provides a clean interface.


βš™οΈ Advanced Tips

βœ… Use React.memo with context consumers

const ThemeButton = React.memo(() => {
  const { theme } = useTheme();
  return <button className={theme}>Click</button>;
});
Enter fullscreen mode Exit fullscreen mode

βœ… Use selectors (with Zustand or Redux)

But if sticking to Context:

const ThemeValueContext = createContext();
const ThemeActionsContext = createContext();

function ThemeProvider() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeValueContext.Provider value={theme}>
      <ThemeActionsContext.Provider value={{ setTheme }}>
        {children}
      </ThemeActionsContext.Provider>
    </ThemeValueContext.Provider>
  );
}

// useThemeValue(), useThemeActions()...
Enter fullscreen mode Exit fullscreen mode

🏁 Final Thought: When to Use Context

βœ… Good for:

  • Auth, theme, language
  • Configuration objects
  • Toggling UI elements
  • Cross-cutting app-level state

❌ Avoid for:

  • Frequently changing local UI state
  • Large shared state with complex updates

πŸ“˜ Summary Cheatsheet

Pattern Purpose
createContext + useContext Basic pattern
Custom hook (useXyz) Encapsulation & safety
useMemo on value Avoid re-renders
Split contexts Isolate state updates
Compose providers Cleaner root tree

Top comments (0)