Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

DEV Community

Cover image for Building a Custom React Context with Optimized Selectors (Without Re-Renders)
HexShift
HexShift

Posted on

Building a Custom React Context with Optimized Selectors (Without Re-Renders)

Global state in React can easily become a performance bottleneck. When one component updates, others often re-render unnecessarily. Let's build a custom Context setup that uses selectors to avoid those extra renders β€” no Redux, no extra libraries.

Why Avoid Default Context Re-Renders?

Using React's built-in Context API directly can trigger re-renders across all consumers whenever the provider value changes. This isn't ideal for fine-grained UI control or performance-critical apps.

Step 1: Create a Context with Subscriptions

We'll manually handle a subscription system to notify only interested components:

// store.js
import { createContext, useContext, useRef, useState, useEffect } from "react";

const StoreContext = createContext(null);

export function StoreProvider({ children }) {
  const subscribers = useRef(new Set());
  const [state, setState] = useState({ user: "Guest", theme: "light" });

  const update = (partial) => {
    setState(prev => { const next = { ...prev, ...partial }; 
      subscribers.current.forEach(cb => cb(next));
      return next;
    });
  };

  const subscribe = (cb) => {
    subscribers.current.add(cb);
    return () => subscribers.current.delete(cb);
  };

  const store = { getState: () => state, update, subscribe };

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

export function useStore(selector) {
  const store = useContext(StoreContext);
  const [selected, setSelected] = useState(() => selector(store.getState()));

  useEffect(() => {
    const checkForUpdates = (nextState) => {
      const nextSelected = selector(nextState);
      setSelected(prev => (prev !== nextSelected ? nextSelected : prev));
    };
    const unsubscribe = store.subscribe(checkForUpdates);
    return unsubscribe;
  }, [store, selector]);

  return selected;
}

Step 2: Using the Store in Components

Components can now subscribe to just the slice of state they care about:

// Profile.js
import { useStore } from "./store";

function Profile() {
  const user = useStore(state => state.user);

  return <div>Logged in as: {user}</div>;
}

export default Profile;
// ThemeToggle.js
import { useStore } from "./store";

function ThemeToggle() {
  const theme = useStore(state => state.theme);

  return <button>Theme: {theme}</button>;
}

export default ThemeToggle;

Step 3: Provider Setup

Wrap your app with the StoreProvider:

// App.js
import { StoreProvider } from "./store";
import Profile from "./Profile";
import ThemeToggle from "./ThemeToggle";

function App() {
  return (
    <StoreProvider>
      <Profile />
      <ThemeToggle />
    </StoreProvider>
  );
}

export default App;

Pros and Cons

βœ… Pros

  • Zero extra dependencies
  • Fine-grained re-render control
  • Fully React-native without Redux complexity

⚠️ Cons

  • More boilerplate for larger stores
  • Manually handling subscriptions adds maintenance overhead
  • Not ideal for extremely complex or normalized state trees

πŸš€ Alternatives

  • Recoil: Atomic state management
  • Jotai: Minimalist atom-based global state
  • Redux Toolkit: Still the king for massive apps

Summary

React Context isn’t slow β€” it’s how you use it. By building a subscription-aware selector system, you can keep your apps snappy without bloating them with third-party libraries. Great for small-to-medium projects that demand speed and simplicity.

If this was useful, you can support me here: buymeacoffee.com/hexshift

Top comments (0)