A TypeScript implementation of createSelectableContext that provides a useContextSelector hook, allowing components to subscribe to specific slices of context state and re-render only when those slices change.
import React, { createContext, useContext, useRef, useCallback, useSyncExternalStore, ReactNode } from 'react';
type Listener = () => void;
interface Store<T> {
getState: () => T;
subscribe: (listener: Listener) => () => void;
setState: (newState: T | ((prev: T) => T)) => void;
}
interface SelectableContextValue<T> {
store: Store<T>;
}
interface SelectableContext<T> {
Provider: React.FC<{ value: T; children: ReactNode }>;
useSelector: <S>(selector: (state: T) => S) => S;
useContextValue: () => T;
useContextSetter: () => (newState: T | ((prev: T) => T)) => void;
}
function createStore<T>(initialState: T): Store<T> {
let state = initialState;
const listeners = new Set<Listener>();
return {
getState: () => state,
subscribe: (listener: Listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
setState: (newState: T | ((prev: T) => T)) => {
const nextState = typeof newState === 'function'
? (newState as (prev: T) => T)(state)
: newState;
if (!Object.is(state, nextState)) {
state = nextState;
listeners.forEach(listener => listener());
}
}
};
}
export function createSelectableContext<T>(): SelectableContext<T> {
const Context = createContext<SelectableContextValue<T> | null>(null);
const Provider: React.FC<{ value: T; children: ReactNode }> = ({ value, children }) => {
const storeRef = useRef<Store<T> | null>(null);
if (storeRef.current === null) {
storeRef.current = createStore(value);
}
const isFirstRender = useRef(true);
if (!isFirstRender.current) {
storeRef.current.setState(value);
}
isFirstRender.current = false;
return React.createElement(
Context.Provider,
{ value: { store: storeRef.current } },
children
);
};
function useStore(): Store<T> {
const contextValue = useContext(Context);
if (contextValue === null) {
throw new Error('useContextSelector must be used within a SelectableContext Provider');
}
return contextValue.store;
}
function useSelector<S>(selector: (state: T) => S): S {
const store = useStore();
const getSnapshot = useCallback(() => {
return selector(store.getState());
}, [store, selector]);
return useSyncExternalStore(
store.subscribe,
getSnapshot,
getSnapshot
);
}
function useContextValue(): T {
const store = useStore();
return useSyncExternalStore(
store.subscribe,
store.getState,
store.getState
);
}
function useContextSetter(): (newState: T | ((prev: T) => T)) => void {
const store = useStore();
return store.setState;
}
return {
Provider,
useSelector,
useContextValue,
useContextSetter
};
}
// Usage Example
interface AppState {
user: { name: string; email: string };
theme: 'light' | 'dark';
notifications: number;
}
const AppContext = createSelectableContext<AppState>();
// Component that only re-renders when theme changes
function ThemeDisplay() {
const theme = AppContext.useSelector(state => state.theme);
console.log('ThemeDisplay rendered');
return React.createElement('div', null, `Current theme: ${theme}`);
}
// Component that only re-renders when user changes
function UserDisplay() {
const user = AppContext.useSelector(state => state.user);
console.log('UserDisplay rendered');
return React.createElement('div', null, `User: ${user.name} (${user.email})`);
}
// Component that only re-renders when notifications change
function NotificationBadge() {
const notifications = AppContext.useSelector(state => state.notifications);
console.log('NotificationBadge rendered');
return React.createElement('span', { className: 'badge' }, notifications);
}
// App component with full state access
function App() {
const initialState: AppState = {
user: { name: 'John Doe', email: 'john@example.com' },
theme: 'light',
notifications: 5
};
return React.createElement(
AppContext.Provider,
{ value: initialState },
React.createElement('div', null,
React.createElement(ThemeDisplay, null),
React.createElement(UserDisplay, null),
React.createElement(NotificationBadge, null)
)
);
}
export { AppContext, ThemeDisplay, UserDisplay, NotificationBadge, App };This implementation creates a selectable context pattern that solves one of React's most common performance issues: context consumers re-rendering whenever any part of the context value changes, even if they only use a small slice of that data.
The core of this pattern is the createStore function, which implements a simple pub/sub mechanism. It maintains internal state and a set of listeners that get notified whenever state changes. The setState function uses Object.is comparison to prevent unnecessary notifications when the state reference hasn't actually changed. This is crucial for performance because it means listeners only get called when there's a genuine state update.
The createSelectableContext function returns a Provider component and several hooks. The Provider creates a store instance on first render using useRef to maintain referential stability across re-renders. When the parent passes a new value prop, the store's state is updated, which triggers only the listeners that care about the changed data. The isFirstRender ref prevents double-setting on mount.
The useSelector hook is where the magic happens. It leverages React 18's useSyncExternalStore hook, which is specifically designed for subscribing to external data sources. The selector function extracts just the piece of state the component needs, and useSyncExternalStore handles the subscription lifecycle automatically. Components will only re-render when their selected slice of state actually changes, not when other parts of the context update.
This pattern is ideal for large applications with complex state where different components need different pieces of data. It's particularly useful when you have frequently updating values (like notifications or timers) that shouldn't cause unrelated components to re-render. However, for simple contexts with few consumers or contexts that rarely change, this adds unnecessary complexity. Also note that selectors should be memoized with useCallback when defined inline, or defined outside the component to maintain referential stability and prevent the useSyncExternalStore from resubscribing on every render.