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 {children}; } 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 Logged in as: {user}; } export default Profile; // ThemeToggle.js import { useStore } from "./store"; function ThemeToggle() { const theme = useStore(state => state.theme); return Theme: {theme}; } 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 ( ); } 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
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 {children} ;
}
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 Logged in as: {user};
}
export default Profile;
// ThemeToggle.js
import { useStore } from "./store";
function ThemeToggle() {
const theme = useStore(state => state.theme);
return ;
}
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 (
);
}
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