Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
🌍 Context API Basics📦 Creating Context🔌 Context.Provider🔍 useContext Hook🎯 Passing Data with Context🔄 Context with useState⚡ Context Performance🏗️ Context Best Practices
Reactjs/Context/Context Api Basics

🌍 Advanced Context Patterns — Composing Complex State Management

At an advanced level, Context transcends simple data passing. Understanding composition patterns, selector hooks, and middleware patterns allows you to build sophisticated state management systems that scale across large applications.


🎯 Context Composition and Middleware Patterns

Context composition involves combining multiple Contexts to create composable, reusable state management layers. This pattern is fundamental to building flexible applications that can adapt to different requirements.

The middleware pattern allows you to intercept and transform Context updates, enabling features like logging, persistence, and undo/redo functionality without tightly coupling these concerns to your business logic.

// Advanced composition with middleware
import { createContext, useContext, useState, useCallback, ReactNode } from "react";

type Action<T> = {
  type: string;
  payload?: any;
};

type Middleware<T> = (
  getState: () => T,
  action: Action<T>,
  next: () => void
) => void;

interface ComposableContextConfig<T> {
  initialState: T;
  reducer: (state: T, action: Action<T>) => T;
  middlewares?: Middleware<T>[];
}

// Factory function to create composable contexts
function createComposableContext<T>(config: ComposableContextConfig<T>) {
  const Context = createContext<{
    state: T;
    dispatch: (action: Action<T>) => void;
  } | undefined>(undefined);

  function Provider({ children }: { children: ReactNode }) {
    const [state, setState] = useState<T>(config.initialState);

    const dispatch = useCallback((action: Action<T>) => {
      let nextState = state;
      let isDirty = false;

      const next = () => {
        if (!isDirty) {
          nextState = config.reducer(state, action);
          isDirty = true;
        }
      };

      if (config.middlewares) {
        for (const middleware of config.middlewares) {
          middleware(() => state, action, next);
        }
      }

      next();

      if (isDirty) {
        setState(nextState);
      }
    }, [state]);

    return (
      <Context.Provider value={{ state, dispatch }}>
        {children}
      </Context.Provider>
    );
  }

  function useContext_() {
    const context = useContext(Context);
    if (!context) {
      throw new Error("useContext must be within Provider");
    }
    return context;
  }

  return { Provider, useContext: useContext_ };
}

// Example: Logger middleware
function loggerMiddleware<T>(
  getState: () => T,
  action: Action<T>,
  next: () => void
) {
  console.log("Action dispatched:", action.type);
  console.log("Previous state:", getState());
  next();
}

// Example: Persistence middleware
function persistenceMiddleware<T>(
  getState: () => T,
  action: Action<T>,
  next: () => void
) {
  next();
  localStorage.setItem("appState", JSON.stringify(getState()));
}

// Create a composable counter context
type CounterState = { count: number };
type CounterAction = Action<CounterState>;

const counterReducer = (state: CounterState, action: CounterAction) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return { count: 0 };
    default:
      return state;
  }
};

const CounterContext = createComposableContext<CounterState>({
  initialState: { count: 0 },
  reducer: counterReducer,
  middlewares: [loggerMiddleware, persistenceMiddleware],
});

function CounterComponent() {
  const { state, dispatch } = CounterContext.useContext();

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
    </div>
  );
}

export { CounterContext };

💡 Context Selectors for Performance Optimization

Context selectors are a powerful pattern that allows components to subscribe to only the specific parts of the Context they need, preventing unnecessary re-renders when unrelated state changes.

The selector pattern is particularly valuable in large applications where multiple pieces of state evolve independently. Without selectors, any state change triggers a re-render of all consumers.

// Context with selector support
import { createContext, useContext, useState, ReactNode, useMemo } from "react";

interface AppState {
  user: { id: string; name: string };
  theme: { mode: "light" | "dark" };
  notifications: { count: number; items: string[] };
  ui: { sidebarOpen: boolean; modalVisible: boolean };
}

type Selector<T> = (state: AppState) => T;
type StateListener<T> = (selected: T, prevSelected: T) => void;

interface StoreContextType {
  getState: () => AppState;
  subscribe: <T,>(selector: Selector<T>, listener: StateListener<T>) => () => void;
  setState: (partial: Partial<AppState>) => void;
}

const StoreContext = createContext<StoreContextType | undefined>(undefined);

// Store provider with selector support
function StoreProvider({ children }: { children: ReactNode }) {
  const [state, setStateInternal] = useState<AppState>({
    user: { id: "1", name: "Alice" },
    theme: { mode: "light" },
    notifications: { count: 0, items: [] },
    ui: { sidebarOpen: true, modalVisible: false },
  });

  const listeners = new Map<
    Selector<any>,
    Array<{ listener: StateListener<any>; selected: any }>
  >();

  const setState = (partial: Partial<AppState>) => {
    const newState = { ...state, ...partial };
    setStateInternal(newState);

    // Notify listeners about changes
    listeners.forEach((subscriberList, selector) => {
      subscriberList.forEach(({ listener, selected: prevSelected }) => {
        const newSelected = selector(newState);
        if (newSelected !== prevSelected) {
          listener(newSelected, prevSelected);
        }
      });
    });
  };

  const subscribe = <T,>(
    selector: Selector<T>,
    listener: StateListener<T>
  ): (() => void) => {
    if (!listeners.has(selector)) {
      listeners.set(selector, []);
    }

    const subscriberList = listeners.get(selector)!;
    const selected = selector(state);
    subscriberList.push({ listener, selected });

    return () => {
      const index = subscriberList.findIndex((s) => s.listener === listener);
      if (index > -1) {
        subscriberList.splice(index, 1);
      }
    };
  };

  const value: StoreContextType = {
    getState: () => state,
    subscribe,
    setState,
  };

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

// Hook to subscribe to specific state slices
function useSelector<T,>(selector: Selector<T>): T {
  const store = useContext(StoreContext);
  if (!store) {
    throw new Error("useSelector must be within StoreProvider");
  }

  const [selected, setSelected] = useState(() => selector(store.getState()));

  useMemo(() => {
    const unsubscribe = store.subscribe(selector, (newSelected) => {
      setSelected(newSelected);
    });
    return unsubscribe;
  }, [store, selector]);

  return selected;
}

// Components only re-render when their selected slice changes
function UserComponent() {
  const user = useSelector((state) => state.user);
  return <div>User: {user.name}</div>;
}

function ThemeComponent() {
  const theme = useSelector((state) => state.theme);
  return <div>Theme: {theme.mode}</div>;
}

function NotificationComponent() {
  const notificationCount = useSelector((state) => state.notifications.count);
  return <div>Notifications: {notificationCount}</div>;
}

// When user changes, only UserComponent re-renders
// When theme changes, only ThemeComponent re-renders
// NotificationComponent only re-renders when notification count changes

<details>

<summary>📚 More Examples</summary>

// Example 1: Redux-like context with devtools support
interface ReduxContext<S, A> {
  state: S;
  dispatch: (action: A) => void;
  subscribe: (listener: () => void) => () => void;
}

function createReduxContext<S, A>(
  reducer: (state: S, action: A) => S,
  initialState: S
) {
  const Context = createContext<ReduxContext<S, A> | undefined>(undefined);

  function Provider({ children }: { children: ReactNode }) {
    const [state, setState] = useState(initialState);
    const listeners = new Set<() => void>();

    const dispatch = (action: A) => {
      setState((prevState) => {
        const newState = reducer(prevState, action);
        // Notify all listeners of state change
        listeners.forEach((listener) => listener());
        return newState;
      });
    };

    const subscribe = (listener: () => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    };

    return (
      <Context.Provider value={{ state, dispatch, subscribe }}>
        {children}
      </Context.Provider>
    );
  }

  return { Provider, Context };
}

// Example 2: Combining multiple selectors
function useSelectorMultiple<T extends Record<string, any>>(
  selectors: { [K in keyof T]: Selector<T[K]> }
): T {
  const store = useContext(StoreContext);
  if (!store) throw new Error("useSelector must be within StoreProvider");

  const entries = Object.entries(selectors);
  const values = entries.map(([_, selector]) =>
    useSelector(selector as any)
  );

  return useMemo(() => {
    const result = {} as T;
    entries.forEach(([key], index) => {
      result[key as keyof T] = values[index];
    });
    return result;
  }, [values]);
}

// Usage: fetch multiple selectors with one hook
const { user, theme } = useSelectorMultiple({
  user: (state) => state.user,
  theme: (state) => state.theme,
});

</details>

🎨 Real-World Pattern: Feature-Based Context Architecture

Large applications benefit from organizing Contexts around features rather than data types:

// contexts/auth/AuthContext.tsx
interface AuthState {
  user: { id: string; email: string } | null;
  isLoading: boolean;
  error: Error | null;
}

interface AuthActions {
  login(email: string, password: string): Promise<void>;
  logout(): void;
  register(email: string, password: string): Promise<void>;
}

type AuthContextType = AuthState & AuthActions;

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<AuthState>({
    user: null,
    isLoading: false,
    error: null,
  });

  const login = async (email: string, password: string) => {
    setState((prev) => ({ ...prev, isLoading: true }));
    try {
      const response = await fetch("/api/login", {
        method: "POST",
        body: JSON.stringify({ email, password }),
      });
      const user = await response.json();
      setState((prev) => ({ ...prev, user, isLoading: false }));
    } catch (error) {
      setState((prev) => ({
        ...prev,
        error: error as Error,
        isLoading: false,
      }));
    }
  };

  const logout = () => {
    setState((prev) => ({ ...prev, user: null }));
  };

  const register = async (email: string, password: string) => {
    setState((prev) => ({ ...prev, isLoading: true }));
    try {
      const response = await fetch("/api/register", {
        method: "POST",
        body: JSON.stringify({ email, password }),
      });
      const user = await response.json();
      setState((prev) => ({ ...prev, user, isLoading: false }));
    } catch (error) {
      setState((prev) => ({
        ...prev,
        error: error as Error,
        isLoading: false,
      }));
    }
  };

  const value: AuthContextType = { ...state, login, logout, register };

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

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be within AuthProvider");
  }
  return context;
}

// Selector hooks for specific auth state
export function useUser() {
  const { user } = useAuth();
  return user;
}

export function useAuthLoading() {
  const { isLoading } = useAuth();
  return isLoading;
}

📊 Advanced Context Patterns Summary

PatternUse CaseBenefit
MiddlewareLogging, persistenceSeparates concerns
SelectorsPartial state accessPrevents unnecessary re-renders
CompositionCombining contextsFlexibility and reusability
Feature-basedOrganizing by domainScalability
Redux-likeComplex stateFamiliar for Redux users

🔑 Key Takeaways

  • ✅ Use middleware patterns to intercept and transform Context updates
  • ✅ Implement selectors to allow components to subscribe only to relevant state
  • ✅ Compose multiple Contexts to create flexible architectures
  • ✅ Organize Contexts by features rather than data types
  • ✅ Support listener patterns for reactive state management
  • ✅ Leverage Context factories for reusable, type-safe patterns
  • ✅ Consider Redux when Context selectors become complex
  • ✅ Profile and monitor Context performance in production

Ready to practice? Challenges | Next: Advanced Context Design


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn