React Context API and Global State

1. createContext and Provider Components

Feature Syntax Description Use Case
createContext const Ctx = createContext(defaultValue) Create context object Define shared state container
Default Value createContext(initialValue) Fallback when no provider exists Testing, development defaults
Provider Component <Ctx.Provider value={...}> Provide context value to children Share data down component tree
Provider Value value={{ state, actions }} Object with state and updater functions Complete state management
Nested Providers <A><B><C /></B></A> Stack multiple providers Compose contexts
Provider Pattern Wrap App with providers Make context available globally App-wide state

Example: Creating and providing context

import { createContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
});

// 2. Create provider component
export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  const value = {
    theme,
    toggleTheme
  };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
};

// 3. Create auth provider
const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  const login = async (credentials) => {
    setLoading(true);
    const userData = await api.login(credentials);
    setUser(userData);
    setLoading(false);
  };
  
  const logout = () => {
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

// 4. Compose providers in App
const App = () => (
  <ThemeProvider>
    <AuthProvider>
      <Router>
        <Layout />
      </Router>
    </AuthProvider>
  </ThemeProvider>
);

// 5. Provider composition helper
const ComposeProviders = ({ providers, children }) => {
  return providers.reduceRight(
    (acc, Provider) => <Provider>{acc}</Provider>,
    children
  );
};

// Usage
<ComposeProviders providers={[ThemeProvider, AuthProvider, RouterProvider]}>
  <App />
</ComposeProviders>

2. useContext Hook and Context Consumption

Pattern Syntax Description Best Practice
useContext Hook const value = useContext(Context) Access context value in component Must be inside Provider
Destructure Value const { state, action } = useContext(Ctx) Extract specific values from context Cleaner code, clear dependencies
Custom Hook const useMyContext = () => useContext(Ctx) Wrap useContext in custom hook Better error handling, abstraction
Null Check if (!context) throw Error Ensure provider exists Catch missing provider early
Multiple Contexts Call useContext multiple times Consume different contexts Separate concerns

Example: Consuming context with custom hooks

import { createContext, useContext, useState } from 'react';

// Context setup
const UserContext = createContext(null);

export const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
};

// ✓ Good: Custom hook with error handling
export const useUser = () => {
  const context = useContext(UserContext);
  
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  
  return context;
};

// Usage in components
const Profile = () => {
  const { user, setUser } = useUser();
  
  if (!user) return <Login />;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => setUser(null)}>Logout</button>
    </div>
  );
};

// Multiple contexts example
const ThemeContext = createContext('light');
const LanguageContext = createContext('en');

const useTheme = () => useContext(ThemeContext);
const useLanguage = () => useContext(LanguageContext);

const Header = () => {
  const theme = useTheme();
  const language = useLanguage();
  const { user } = useUser();
  
  return (
    <header className={theme}>
      <span>{language === 'en' ? 'Welcome' : 'Bienvenido'}</span>
      {user && <span>{user.name}</span>}
    </header>
  );
};

// Selective context consumption
const Button = () => {
  // Only subscribes to theme, not user
  const theme = useTheme();
  return <button className={theme}>Click</button>;
};

// Pattern: Context + Reducer
const TodoContext = createContext(null);

export const TodoProvider = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
};

export const useTodos = () => {
  const context = useContext(TodoContext);
  if (!context) throw new Error('useTodos requires TodoProvider');
  return context;
};

3. Multiple Contexts and Context Composition

Pattern Description Benefit Example
Separate Contexts Different contexts for different concerns Clear separation, selective updates Theme, Auth, Language contexts
Nested Providers Stack providers in component tree Layer functionality Auth wraps Theme wraps App
Context Composition Combine multiple contexts in one hook Convenient API, related data useApp() returns theme + auth
Scoped Contexts Context for specific feature area Isolated state, no global pollution Form context, Modal context
Context Hierarchy Parent contexts provide to children Natural data flow App → Feature → Component

Example: Multiple contexts and composition

// Separate contexts for different concerns
const ThemeContext = createContext('light');
const AuthContext = createContext(null);
const NotificationContext = createContext(null);

// Provider setup
const App = () => (
  <ThemeProvider>
    <AuthProvider>
      <NotificationProvider>
        <Router />
      </NotificationProvider>
    </AuthProvider>
  </ThemeProvider>
);

// Composed hook for convenience
const useAppContext = () => {
  const theme = useContext(ThemeContext);
  const auth = useContext(AuthContext);
  const notifications = useContext(NotificationContext);
  
  return { theme, auth, notifications };
};

// Usage
const Dashboard = () => {
  const { theme, auth, notifications } = useAppContext();
  
  return (
    <div className={theme}>
      <h1>Welcome {auth.user.name}</h1>
      {notifications.messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  );
};

// Scoped context for features
const FormContext = createContext(null);

const FormProvider = ({ children, initialValues }) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  
  const setValue = (field, value) => {
    setValues(prev => ({ ...prev, [field]: value }));
  };
  
  return (
    <FormContext.Provider value={{ values, errors, setValue }}>
      {children}
    </FormContext.Provider>
  );
};

// Form uses scoped context
const MyForm = () => (
  <FormProvider initialValues={{ name: '', email: '' }}>
    <FormFields />
    <SubmitButton />
  </FormProvider>
);

// Hierarchical contexts
const AppContext = createContext(null);
const FeatureContext = createContext(null);

const FeatureArea = () => {
  const appData = useContext(AppContext);
  const [featureState, setFeatureState] = useState({});
  
  return (
    <FeatureContext.Provider value={{ appData, featureState, setFeatureState }}>
      <FeatureComponents />
    </FeatureContext.Provider>
  );
};

// Provider composition utility
const combineProviders = (...providers) => {
  return ({ children }) => {
    return providers.reduce(
      (acc, [Provider, props]) => (
        <Provider {...props}>{acc}</Provider>
      ),
      children
    );
  };
};

// Usage
const AppProviders = combineProviders(
  [ThemeProvider, { defaultTheme: 'light' }],
  [AuthProvider, {}],
  [NotificationProvider, { position: 'top-right' }]
);

<AppProviders>
  <App />
</AppProviders>

4. Context Performance and Re-render Optimization

Issue Cause Solution Pattern
Unnecessary Re-renders Provider value changes on every render Memoize provider value useMemo(() => ({ state }), [state])
All Consumers Update Any context change triggers all consumers Split contexts by update frequency Separate read-only from mutable state
Large Context Objects Single context with many properties Multiple smaller contexts Theme, Auth, Settings as separate
Function References New functions created every render useCallback for updater functions useCallback(() => {}, [])
Deep Component Trees Many components between provider/consumer React.memo on intermediate components Prevent propagation of re-renders

Example: Context performance optimization

// ❌ Bad: New object every render
const BadProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  
  // This creates a new object on every render!
  return (
    <Context.Provider value={{ count, setCount }}>
      {children}
    </Context.Provider>
  );
};

// ✓ Good: Memoized value
const GoodProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  
  const value = useMemo(
    () => ({ count, setCount }),
    [count] // Only recreate when count changes
  );
  
  return (
    <Context.Provider value={value}>
      {children}
    </Context.Provider>
  );
};

// ✓ Better: Split contexts
const CountContext = createContext(0);
const CountDispatchContext = createContext(null);

const CountProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  
  // State and dispatch are separate contexts
  return (
    <CountContext.Provider value={count}>
      <CountDispatchContext.Provider value={setCount}>
        {children}
      </CountDispatchContext.Provider>
    </CountContext.Provider>
  );
};

// Components can subscribe to only what they need
const DisplayCount = () => {
  const count = useContext(CountContext); // Only re-renders when count changes
  return <div>{count}</div>;
};

const IncrementButton = () => {
  const setCount = useContext(CountDispatchContext); // Never re-renders!
  return <button onClick={() => setCount(c => c + 1)}>+</button>;
};

// ✓ Good: Memoized callbacks
const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  
  const login = useCallback(async (credentials) => {
    const userData = await api.login(credentials);
    setUser(userData);
  }, []);
  
  const logout = useCallback(() => {
    setUser(null);
  }, []);
  
  const value = useMemo(
    () => ({ user, login, logout }),
    [user, login, logout]
  );
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
};

// Memo intermediate components
const Layout = memo(({ children }) => {
  return (
    <div className="layout">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
});

// Split by update frequency
const StaticConfigContext = createContext(null); // Rarely changes
const DynamicDataContext = createContext(null);  // Changes often

const App = () => (
  <StaticConfigContext.Provider value={config}>
    <DynamicDataProvider>
      <Routes />
    </DynamicDataProvider>
  </StaticConfigContext.Provider>
);
Warning: Context triggers re-renders in ALL consumers when value changes. Use useMemo for provider values, useCallback for functions, and split contexts to minimize unnecessary updates.

5. Context vs Prop Drilling Trade-offs

Approach Pros Cons When to Use
Props Explicit, type-safe, easy to trace, testable Verbose with deep nesting, refactoring burden 2-3 levels deep, clear data flow needed
Context Avoid drilling, cleaner intermediate components Implicit dependencies, harder to trace, re-render issues Deep nesting, many consumers, global state
Composition Avoid both drilling and context, flexible Requires component structure planning Layout components, wrapper abstraction
State Management Powerful features, DevTools, middleware Boilerplate, learning curve, bundle size Complex apps, time-travel debugging
URL State Shareable, persistent, no drilling Limited to serializable data, URL length Filters, pagination, tabs

Example: Comparing approaches

// Approach 1: Props (explicit but verbose)
const App = () => {
  const [theme, setTheme] = useState('light');
  return <Layout theme={theme} setTheme={setTheme} />;
};

const Layout = ({ theme, setTheme }) => (
  <div>
    <Header theme={theme} setTheme={setTheme} />
    <Content theme={theme} />
  </div>
);

const Header = ({ theme, setTheme }) => (
  <header>
    <ThemeToggle theme={theme} setTheme={setTheme} />
  </header>
);

// Approach 2: Context (implicit but cleaner)
const ThemeContext = createContext('light');

const App = () => {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
};

const Layout = () => (
  <div>
    <Header />
    <Content />
  </div>
);

const ThemeToggle = () => {
  const { theme, setTheme } = useContext(ThemeContext);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
};

// Approach 3: Composition (flexible)
const App = () => {
  const [theme, setTheme] = useState('light');
  return (
    <Layout
      header={<Header theme={theme} setTheme={setTheme} />}
      content={<Content theme={theme} />}
    />
  );
};

const Layout = ({ header, content }) => (
  <div>
    {header}
    {content}
  </div>
);

// Decision guide:
// Props: 1-3 levels, clear data flow, explicit dependencies
// Context: 4+ levels, many consumers, truly global state
// Composition: Layout components, avoid wrapper drilling
// State library: Complex state, time-travel, middleware needed
Note: Start with props. Add context when drilling becomes painful (usually 4+ levels). Consider composition before reaching for context. Reserve state management libraries for truly complex apps.

6. Custom Context Hooks and Abstractions

Pattern Implementation Benefit Example
Custom Hook Wrap useContext in function Better error messages, validation useAuth() instead of useContext(AuthContext)
Error Handling Throw if provider missing Catch mistakes early Better DX, clear error messages
Selector Pattern Custom hooks for subsets of context Optimized updates, clearer API useUserName() only subscribes to name
Action Creators Provide functions not raw dispatch Type-safe, encapsulated logic login() instead of dispatch({ type: 'LOGIN' })
Computed Values Derive values in custom hooks Reusable logic, memoization useIsAuthenticated() derives from user state

Example: Custom context hooks and abstractions

// Advanced context pattern with custom hooks
const AuthContext = createContext(null);

// Provider with reducer
export const AuthProvider = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, {
    user: null,
    loading: true,
    error: null
  });
  
  const value = useMemo(() => ({ state, dispatch }), [state]);
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

// Base hook with error handling
const useAuthContext = () => {
  const context = useContext(AuthContext);
  
  if (!context) {
    throw new Error('useAuth hooks must be used within AuthProvider');
  }
  
  return context;
};

// Public API - action creators
export const useAuth = () => {
  const { state, dispatch } = useAuthContext();
  
  const login = useCallback(async (credentials) => {
    dispatch({ type: 'LOGIN_START' });
    try {
      const user = await api.login(credentials);
      dispatch({ type: 'LOGIN_SUCCESS', payload: user });
    } catch (error) {
      dispatch({ type: 'LOGIN_ERROR', payload: error.message });
    }
  }, [dispatch]);
  
  const logout = useCallback(() => {
    dispatch({ type: 'LOGOUT' });
  }, [dispatch]);
  
  return {
    user: state.user,
    loading: state.loading,
    error: state.error,
    login,
    logout
  };
};

// Selector hooks for performance
export const useUser = () => {
  const { state } = useAuthContext();
  return state.user;
};

export const useIsAuthenticated = () => {
  const { state } = useAuthContext();
  return state.user !== null;
};

export const useAuthLoading = () => {
  const { state } = useAuthContext();
  return state.loading;
};

// Usage in components
const Profile = () => {
  const { user, logout } = useAuth();
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={logout}>Logout</button>
    </div>
  );
};

const LoginButton = () => {
  const isAuthenticated = useIsAuthenticated();
  const { login } = useAuth();
  
  if (isAuthenticated) return null;
  
  return <button onClick={() => login({ email, password })}>Login</button>;
};

// Advanced: Factory for creating context hooks
const createContextHook = (Context, name) => {
  return () => {
    const context = useContext(Context);
    if (!context) {
      throw new Error(\`use\${name} must be used within \${name}Provider\`);
    }
    return context;
  };
};

// Usage
const useTheme = createContextHook(ThemeContext, 'Theme');
const useSettings = createContextHook(SettingsContext, 'Settings');

Context API Best Practices

  • Custom hooks: Always wrap useContext in custom hooks with error handling
  • Memoization: Use useMemo for provider values and useCallback for functions
  • Split contexts: Separate by concern and update frequency to optimize re-renders
  • Action creators: Provide high-level APIs, hide implementation details
  • Start simple: Use props first, context for deep drilling (4+ levels)
  • Composition over context: Consider component composition before context