Skip to main content
ReactState ManagementRxJSWeb DevelopmentPerformance Optimization

Beyond Context: Architecting High-Performance Fine-Grained Reactivity in React

elango
elango

Full Stack Engineer

Updated
6 min read
Beyond Context: Architecting High-Performance Fine-Grained Reactivity in React

The React Context Performance Trap

I hit Context re-render storms on this portfolio when ThemeContext and ProfileContext wrapped the entire tree. On elangodev.com I split providers, moved high-churn UI state local, and kept Context for slow-changing profile data — the pattern I use across blog, dashboard, and elangodev.com routes today.

React Context is one of the most widely misunderstood features in the React ecosystem. Commonly treated as a state management tool, Context is actually a dependency injection mechanism. Its primary role is to pass down values to deep child components without explicit prop-drilling. When state changes inside a Context Provider, React triggers a re-render of the entire descendant tree from that Provider downward, unless extreme precautions are taken. This happens regardless of whether individual components in that tree actually consume the specific slice of state that changed.

For applications handling high-frequency state updates—such as real-time WebSocket feeds, collaborative drag-and-drop boards, complex multi-step forms, or gaming dashboards—relying solely on Context for global state leads to severe rendering bottlenecks, degrading the Interaction to Next Paint (INP) metric and causing visible interface lag.

💡 Pro Tip: Do not split your Context Provider into dozens of mini-providers unless they have zero conceptual intersection. Instead, pivot to a subscription-based state model that separates react-rendering from state mutability.

The Push vs. Pull Architecture: Bypassing the VDOM

To scale application reactivity, we must move from a Pull-based model (where React continuously diffs the Virtual DOM to check what changed) to a Push-based model (where the state container directly notifies only the specific components that need to change). This relies on the classic Publish-Subscribe (Pub/Sub) pattern.

By storing state outside the React component tree (in a plain JavaScript closure) and establishing a subscription architecture, we can selectively update components. React 18 formalized this paradigm with the introduction of useSyncExternalStore. This hook guarantees tear-free, concurrent-safe access to external data sources by hooking directly into React\'s internal fiber scheduler.

Building a Custom Subscription-Based Store from Scratch

Let\'s demystify how libraries like Zustand and Redux function under the hood by building our own lightweight, type-safe reactive store with zero third-party dependencies.

First, we define our store container which maintains state, holds a registry of listeners, and exposes a mechanism to trigger updates selectively:

type Listener = () => void; 

export function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<Listener>();

  return {
    getState: () => state,
    setState: (nextState: Partial<T> | ((prev: T) => Partial<T>)) => {
      const partial = typeof nextState === "function" ? nextState(state) : nextState;
      state = { ...state, ...partial };
      listeners.forEach((listener) => listener());
    },
    subscribe: (listener: Listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

With this plain vanilla JS store, we need a custom hook to connect it back to React. The useSyncExternalStore hook requires two arguments (and an optional third for SSR): the subscription function and a function returning the snapshot of the state. We will also integrate a selector function to enable fine-grained, component-level state slicing.

import { useSyncExternalStore } from "react";

export function useStore<State, Slice>(
  store: ReturnType<typeof createStore<State>>,
  selector: (state: State) => Slice
): Slice {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState())
  );
}

Let\'s see this in action. Suppose we have a global store representing a dynamic canvas board:

interface BoardState {
  x: number;
  y: number;
  zoom: number;
  title: string;
}

const canvasStore = createStore<BoardState>({
  x: 0,
  y: 0,
  zoom: 1.0,
  title: "My Collaborative Canvas",
});

Now, we can consume only the zoom level in a ZoomIndicator component. This component will only re-render when the zoom value changes, remaining completely unaffected when x, y, or title are updated:

function ZoomIndicator() {
  const zoom = useStore(canvasStore, (state) => state.zoom);
  return <div>Zoom Scale: {zoom * 100}%</div>;
}

Harnessing RxJS for Complex, Time-Based Reactivity

While basic custom stores are perfect for synchronous, UI-driven states, modern web applications often deal with highly asynchronous, complex, and declarative events (e.g., chat streams, mouse-tracking drag operations, network polling). For these scenarios, **RxJS** and reactive programming streams provide unparalleled power.

We can write a generic React hook that bridges the gap between an asynchronous RxJS Observable stream and standard React component state:

import { useState, useEffect } from "react";
import { Observable } from "rxjs";

export function useObservable<T>(observable$: Observable<T>, initialValue: T): T {
  const [value, setValue] = useState<T>(initialValue);

  useEffect(() => {
    const subscription = observable$.subscribe({
      next: (val) => setValue(val),
      error: (err) => console.error("Stream error:", err),
    });
    return () => subscription.unsubscribe();
  }, [observable$]);

  return value;
}

Using this pattern, we can easily orchestrate intricate debouncing logic. For instance, creating an auto-saving input stream that only sends requests to the server after 500ms of user typing inactivity, while showing a "saving" status dynamically to the UI, is straightforward with RxJS operators:

import { Subject } from "rxjs";
import { debounceTime, switchMap, map } from "rxjs/operators";

// A stream representing user input events
const inputSubject$ = new Subject<string>();

// Derived stream capturing saving states and operations
const savingStatus$ = inputSubject$.pipe(
  debounceTime(500),
  switchMap(async (text) => {
    // Simulated API Call
    await fetch("/api/save", { method: "POST", body: JSON.stringify({ text }) });
    return "Saved Successfully!";
  })
);

Architectural Matrix: Selecting Your Reactivity Model

Choosing the correct state architecture depends strictly on your use cases. The table below outlines how to map application patterns to specific architectures:

  • React Context: Ideal for static configuration values, themes, user session setups, and localized dependency injection. Avoid for high-frequency updates.
  • Custom Pub/Sub Stores (using useSyncExternalStore): Ideal for micro-frontend integrations, localized shared states, and high-frequency animations or real-time parameters without the overhead of external libraries.
  • Zustand/Jotai: Recommended default for complex application states, dashboards, and enterprise-grade tools requiring built-in optimization tools and middleware.
  • RxJS Observables: Best choice for highly asynchronous applications, telemetry, multi-channel WebSockets, and heavy, time-based event processing pipeline systems.

What I ship on elangodev.com today

Theme and profile stay in Context because they change rarely. Chat scroll position, dashboard filters, and animation clocks live in external stores or local component state. That split keeps blog reading smooth while the dashboard stays interactive — the same tradeoff I document in the site handbook when I explain React performance for portfolio apps.

If you are debugging Context storms, start by counting how often your provider value identity changes, then move only the high-frequency slices out. Full library migrations are optional; selective subscriptions usually fix the INP regressions first.

elango

Written by

elango

Full Stack Engineer & Software Architect specializing in React, Next.js, and high-performance web applications.

Share on:

Discover More

View blog