Lightweight React state management library built on Context API and useSyncExternalStore. Type-safe, SSR-ready, and optimized for minimal re-renders.
Perfect for managing state in specific sections of your app without prop drilling through multiple component layers.
- 🪶 Lightweight — Minimal footprint with zero dependencies
- 🔒 Type-safe — Full TypeScript support out of the box
- ⚡ Optimized — Shallow comparison prevents unnecessary re-renders
- 🌐 SSR Ready — Built-in support for server-side rendering
- 🎯 Selective Updates — Components only re-render when their selected state changes
- 🧩 Scoped State — Manage state for specific features or sections, not just global app state
This library shines when you have a feature or section of your app where:
- Multiple components at different nesting levels need access to the same state
- You want to avoid passing props through intermediate components (prop drilling)
- The state is scoped to that feature, not the entire application
Examples:
- A multi-step checkout flow
- A complex form with multiple sections
- A dashboard panel with interconnected widgets
- A modal or drawer with nested interactive components
npm install react-selective-contextLet's build a checkout form where multiple nested components need access to cart and shipping data — without passing props through every layer.
import {
createSelectiveContext,
SelectiveProvider,
createContextSelector,
createContextSetter,
} from 'react-selective-context'
// Define your feature state type
type CheckoutState = {
items: Array<{ id: string; name: string; price: number; quantity: number }>
shipping: { address: string; city: string; zipCode: string }
promoCode: string | null
}
// Create a typed context for this feature
const CheckoutContext = createSelectiveContext<CheckoutState>()
// Create hooks bound to this context
const useCheckoutSelector = createContextSelector(CheckoutContext)
const useCheckoutSetter = createContextSetter(CheckoutContext)function CheckoutPage() {
const initialState: CheckoutState = {
items: [],
shipping: { address: '', city: '', zipCode: '' },
promoCode: null,
}
return (
<SelectiveProvider context={CheckoutContext} initialState={initialState}>
<CheckoutLayout>
{/* These components can be deeply nested - no prop drilling needed */}
<CartSummary />
<ShippingForm />
<PromoCodeInput />
<OrderTotal />
</CheckoutLayout>
</SelectiveProvider>
)
}// Deep inside CartSummary component tree
function CartItemCount() {
// Only re-renders when itemCount changes
const itemCount = useCheckoutSelector((state) =>
state.items.reduce((sum, item) => sum + item.quantity, 0)
)
return <span className='badge'>{itemCount}</span>
}
// Inside ShippingForm, nested several levels deep
function ShippingAddressPreview() {
// Only re-renders when shipping changes
const shipping = useCheckoutSelector((state) => state.shipping)
return (
<p>
{shipping.address}, {shipping.city} {shipping.zipCode}
</p>
)
}// Deeply nested in the shipping form
function ShippingAddressInput() {
const setCheckoutState = useCheckoutSetter()
const address = useCheckoutSelector((state) => state.shipping.address)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCheckoutState((prev) => ({
...prev,
shipping: { ...prev.shipping, address: e.target.value },
}))
}
return <input value={address} onChange={handleChange} placeholder='Address' />
}
// In a completely different branch of the component tree
function ApplyPromoButton({ code }: { code: string }) {
const setCheckoutState = useCheckoutSetter()
const applyPromo = () => {
setCheckoutState((prev) => ({ ...prev, promoCode: code }))
}
return <button onClick={applyPromo}>Apply</button>
}Creates a typed React Context for your feature state.
const CheckoutContext = createSelectiveContext<CheckoutState>()Provider component that initializes and manages the store for a specific section of your app.
<SelectiveProvider context={CheckoutContext} initialState={initialCheckoutState}>
<CheckoutFlow />
</SelectiveProvider>Props:
| Prop | Type | Description |
|---|---|---|
context |
SelectiveContext<TState> |
The context created by createSelectiveContext |
initialState |
TState |
The initial state value |
children |
ReactNode |
Child components |
Hook to select and subscribe to a slice of state.
const shipping = useContextSelector(CheckoutContext, (state) => state.shipping)Parameters:
| Parameter | Type | Description |
|---|---|---|
context |
SelectiveContext<TState> |
The state context |
selector |
(state: TState) => TSlice |
Function to select a slice of state |
compare |
(a: TSlice, b: TSlice) => boolean |
Optional comparison function (defaults to shallow equality) |
Hook to get a setter function for updating state.
const setState = useContextSetter(CheckoutContext)
// Update with a function (recommended)
setState((prev) => ({ ...prev, promoCode: 'SAVE20' }))
// Or update with a new state object
setState({ ...newState })Factory function that creates a pre-bound selector hook for a specific context. Recommended for cleaner component code.
const useCheckoutSelector = createContextSelector(CheckoutContext)
// Later in components:
const items = useCheckoutSelector((state) => state.items)Factory function that creates a pre-bound setter hook for a specific context. Recommended for cleaner component code.
const useCheckoutSetter = createContextSetter(CheckoutContext)
// Later in components:
const setCheckoutState = useCheckoutSetter()By default, useContextSelector uses shallow equality to determine if the selected value has changed. You can provide a custom comparison function:
// Deep equality for complex nested objects
const items = useCheckoutSelector(
(state) => state.items,
(a, b) => JSON.stringify(a) === JSON.stringify(b)
)
// Strict equality for primitives
const promoCode = useCheckoutSelector((state) => state.promoCode, Object.is)You can create separate contexts for different parts of your application. Each feature manages its own state independently:
// features/checkout/context.ts
type CheckoutState = { items: CartItem[]; shipping: ShippingInfo }
export const CheckoutContext = createSelectiveContext<CheckoutState>()
export const useCheckoutSelector = createContextSelector(CheckoutContext)
export const useCheckoutSetter = createContextSetter(CheckoutContext)
// features/user-settings/context.ts
type SettingsState = { theme: 'light' | 'dark'; notifications: boolean }
export const SettingsContext = createSelectiveContext<SettingsState>()
export const useSettingsSelector = createContextSelector(SettingsContext)
export const useSettingsSetter = createContextSetter(SettingsContext)
// Each feature wraps only its own section
function CheckoutPage() {
return (
<SelectiveProvider context={CheckoutContext} initialState={checkoutInitialState}>
<CheckoutFlow />
</SelectiveProvider>
)
}
function SettingsPanel() {
return (
<SelectiveProvider context={SettingsContext} initialState={settingsInitialState}>
<SettingsForm />
</SelectiveProvider>
)
}The library is written in TypeScript and provides full type inference:
type CheckoutState = {
items: Array<{ id: string; price: number }>
promoCode: string | null
}
const CheckoutContext = createSelectiveContext<CheckoutState>()
const useCheckoutSelector = createContextSelector(CheckoutContext)
const useCheckoutSetter = createContextSetter(CheckoutContext)
// ✅ Type-safe selector
const items = useCheckoutSelector((state) => state.items) // type: Array<{ id: string; price: number }>
const promoCode = useCheckoutSelector((state) => state.promoCode) // type: string | null
// ✅ Type-safe setters
const setState = useCheckoutSetter()
setCheckoutState((prev) => ({ ...prev, promoCode: 'SAVE20' })) // ✓ valid
setCheckoutState((prev) => ({ ...prev, invalid: true })) // ✗ TypeScript errorThis library combines React's Context API with useSyncExternalStore to provide efficient state management:
- Store Creation — When
SelectiveProvidermounts, it creates a store withgetState,setState, andsubscribemethods - Subscription —
useContextSelectorsubscribes to the store usinguseSyncExternalStore - Selective Updates — The selector function extracts only the needed state slice
- Shallow Comparison — Changes are detected using shallow equality, preventing unnecessary re-renders
- SSR Support —
getServerSnapshotprovides a consistent snapshot during server-side rendering