React Context Pattern Guide
When to use this skill ✅
- •Create a reusable Context implementation that avoids unnecessary re-renders.
- •Add a standardized pattern for
Context+ selector hooks across a repository. - •Provide examples, templates, or scripts for implementing
useSyncExternalStore-based contexts. - •Share best practices for performance-sensitive state management and selector optimization.
- •Teach the agent to apply this pattern during code reviews, refactors, or feature scaffolding.
Overview
This guide provides comprehensive instructions for AI agents to generate professional, type-safe React Context implementations using the Subscription Pattern with useSyncExternalStore. This pattern avoids unnecessary re-renders and provides optimal performance for state management.
Pattern Architecture
This Context Pattern uses the following key concepts:
- •useRef for State Storage: State is stored in
useRefinstead ofuseStateto prevent re-renders - •Subscription Pattern: Components subscribe to state changes manually
- •useSyncExternalStore: React 18+ hook for subscribing to external stores with automatic re-rendering
- •Type Safety: Full TypeScript support with explicit type definitions
- •Separation of Concerns: Separate files for types, context, hooks, and selectors
Why This Pattern?
- •Re-renders all consumers on any state change
- •Only re-renders components that use changed values
- •Uses
useStatecausing cascading updates - •Uses
useRef+ subscriptions for granular control - •Performance issues with frequent updates
- •Performance issues with frequent updates
- •Optimal performance with
useSyncExternalStore
File Structure
architecture
src/
└── context/
└── FeatureName/
├── Context.tsx # Context creation and Provider component
├── types.ts # TypeScript type definitions
├── useContext.ts # Custom hook to access context
├── useSelectors.ts # Selector hooks using useSyncExternalStore
└── index.ts # Public exports
Naming Conventions
- •Folder: Use PascalCase feature name (e.g.,
CreateTask,UserProfile,ShoppingCart) - •Files: Use exact names:
Context.tsx,types.ts,useContext.ts,useSelectors.ts - •Exports: Export from
index.tsfor clean imports
Type Definitions
File: types.ts
Define all TypeScript types for the context in a separate file.
code
// types.ts
import type { PropsWithChildren } from "react";
/**
* Props for the Provider component
* Uses React's PropsWithChildren for automatic children typing
*/
export type TProviderProps = PropsWithChildren;
/**
* Callback type for subscribers
* Called when state changes occur
*/
export type TSubscriberCallback = () => void;
/**
* Context value type definition
* Contains all getters, setters, and subscribe function
*/
export type TContext = {
// Getters - return current state values
getIsOpen: () => boolean;
getLatestTask: () => TTask | undefined;
// Setters - update state values
setIsOpen: (value: boolean) => void;
setLatestTask: (value: TTask | undefined) => void;
// Subscribe function - for useSyncExternalStore integration
subscribe: (callback: TSubscriberCallback) => () => void;
};
Type Definition Rules
- •Always define
TProviderPropsusingPropsWithChildren - •Always define
TSubscriberCallbackas() => void - •Always define
TContextwith:- •Getter functions:
get{StateName}: () => StateType - •Setter functions:
set{StateName}: (value: StateType) => void - •Subscribe function:
subscribe: (callback: TSubscriberCallback) => () => void
- •Getter functions:
Context Provider Implementation
File: Context.tsx
code
// Context.tsx
import { createContext, useCallback, useRef } from "react";
import type { TContext, TProviderProps, TSubscriberCallback } from "./types";
/**
* Create context with null as initial value
* This allows type checking when context is not available
*/
export const Context = createContext<TContext | null>(null);
/**
* Provider component that manages state and subscriptions
*/
export function Provider({ children }: TProviderProps) {
// ============================================
// STATE REFS - Store state without causing re-renders
// ============================================
const isOpen = useRef<boolean>(false);
const latestTask = useRef<TTask | undefined>(undefined);
// ============================================
// SUBSCRIBERS SET - Track all subscribed callbacks
// ============================================
const subscribers = useRef<Set<TSubscriberCallback>>(new Set());
// ============================================
// GETTERS - Return current state values
// ============================================
const getIsOpen = useCallback((): boolean => {
return isOpen.current;
}, []);
const getLatestTask = useCallback((): TTask | undefined => {
return latestTask. current;
}, []);
// ============================================
// SETTERS - Update state and notify subscribers
// ============================================
const setIsOpen = useCallback((value: boolean): void => {
// Optimization: skip if value hasn't changed
if (isOpen.current === value) {
return;
}
isOpen.current = value;
// Notify all subscribers about the change
subscribers.current. forEach((callback) => callback());
}, []);
const setLatestTask = useCallback((value: TTask | undefined): void => {
latestTask.current = value;
subscribers.current.forEach((callback) => callback());
}, []);
// ============================================
// SUBSCRIBE - Allow components to subscribe to changes
// ============================================
const subscribe = useCallback((callback: TSubscriberCallback) => {
subscribers.current. add(callback);
// Return unsubscribe function for cleanup
return (): void => {
subscribers. current.delete(callback);
};
}, []);
// ============================================
// RENDER PROVIDER
// ============================================
return (
<Context. Provider
value={{
getIsOpen,
setIsOpen,
getLatestTask,
setLatestTask,
subscribe,
}}
>
{children}
</Context.Provider>
);
}
Provider Implementation Rules
- •Create context with
null:createContext<TContext | null>(null) - •Use
useReffor state: Never useuseStatefor stored values - •Use
useRef<Set<TSubscriberCallback>>for subscribers: Initialize withnew Set() - •Wrap all functions in
useCallback: With empty dependency array[] - •Getters: Simply return
ref.current - •Setters:
- •Optionally check if value changed (for optimization)
- •Update
ref.current - •Call all subscribers:
subscribers.current.forEach((callback) => callback())
- •Subscribe function:
- •Add callback to Set
- •Return cleanup function that removes callback from Set
Custom Hook Implementation
File: useContext.ts
code
// useContext.ts
import { useContext } from "react";
import { Context } from "./Context";
import type { TContext } from "./types";
/**
* Custom hook to access the context
* Throws an error if used outside of Provider
*
* @returns TContext - The context value with all methods
* @throws Error if context is not available
*/
export const useCreateTaskContext = (): TContext => {
const context = useContext(Context);
if (context === null) {
throw new Error("CreateTask context is not available");
}
return context;
};
Hook Implementation Rules
- •Name the hook:
use{FeatureName}Context(e.g.,useCreateTaskContext,useUserProfileContext) - •Import React's
useContextfrom "react" - •Import
Contextfrom local "./Context" - •Import
TContexttype from "./types" - •Check for null: Always throw descriptive error if context is null
- •Return typed context: Function must return
TContext
Selectors Implementation
File: useSelectors.ts
code
// useSelectors.ts
import { useSyncExternalStore } from "react";
import { useCreateTaskContext } from "./useContext";
// ============================================
// SELECTOR HOOKS - Subscribe to specific state slices
// ============================================
/**
* Selector hook for isOpen state
* Only re-renders when isOpen changes
*
* @returns boolean - Current isOpen value
*/
export const useCreateTaskModalOpenSelector = (): boolean => {
const { getIsOpen, subscribe } = useCreateTaskContext();
return useSyncExternalStore(subscribe, getIsOpen, getIsOpen);
};
/**
* Selector hook for latestTask state
* Only re-renders when latestTask changes
*
* @returns TTask | undefined - Current latestTask value
*/
export const useLatestCreatedTaskSelector = (): TTask | undefined => {
const { getLatestTask, subscribe } = useCreateTaskContext();
return useSyncExternalStore(subscribe, getLatestTask, getLatestTask);
};
// ============================================
// DISPATCH HOOKS - Return setter functions directly
// ============================================
/**
* Dispatch hook for setIsOpen
* Returns the setter function for external use
*
* @returns (value: boolean) => void
*/
export const useSetCreateTaskModalOpenDispatch = () => {
return useCreateTaskContext().setIsOpen;
};
/**
* Dispatch hook for setLatestTask
* Returns the setter function for external use
*
* @returns (value: TTask | undefined) => void
*/
export const useSetLatestCreatedTaskDispatch = () => {
return useCreateTaskContext().setLatestTask;
};
Selector Implementation Rules
Selector Hooks (for reading state)
- •Name pattern:
use{FeatureName}{StateName}Selector - •Use
useSyncExternalStore: Import from "react" - •Destructure
subscribeand getter:const { get{State}, subscribe } = useContext() - •Return:
useSyncExternalStore(subscribe, getter, getter)- •First param: subscribe function
- •Second param: getSnapshot (client)
- •Third param: getServerSnapshot (SSR - use same getter)
Dispatch Hooks (for updating state)
- •Name pattern:
useSet{FeatureName}{StateName}Dispatch - •Simply return the setter:
return useContext().set{StateName} - •No need for
useSyncExternalStore: Dispatchers don't subscribe to changes
Best Practices
1. Always Use This File Structure
code
context/
└── FeatureName/
├── Context.tsx
├── types.ts
├── useContext.ts
├── useSelectors.ts
└── index.ts
2. Export Pattern in index.ts
code
// index.ts
export { Provider } from "./Context";
export { useCreateTaskContext } from "./useContext";
export {
useCreateTaskModalOpenSelector,
useLatestCreatedTaskSelector,
useSetCreateTaskModalOpenDispatch,
useSetLatestCreatedTaskDispatch,
} from "./useSelectors";
export type { TContext, TProviderProps } from "./types";
3. Optimization Check in Setters
code
const setIsOpen = useCallback((value: boolean): void => {
// Skip update if value hasn't changed
if (isOpen.current === value) {
return;
}
isOpen.current = value;
subscribers.current.forEach((callback) => callback());
}, []);
4. Type Safety
- •Always define explicit return types
- •Use
TContext | nullfor createContext - •Export types from
types.ts
5. Naming Conventions
- •Context folder: Use PascalCase. For example:
CreateTaskorUserProfile. - •Provider: Always name the file or component Provider.
- •Context hook: Use the word "use", then the feature name, then "Context". Example:
useCreateTaskContext. - •Selector hook: Use "use", then the feature, then the state name, and "Selector". Example:
useCreateTaskModalOpenSelector - •Dispatch hook: Use "useSet", then the feature, then the state, and "Dispatch". Example:
useSetCreateTaskModalOpenDispatch. - •Context type: Use the letter "T" and the word "Context". Example: TContext.
- •Subscriber callback: Use "T", then "SubscriberCallback". Example: TSubscriberCallback.
Complete Example
Feature: ShoppingCart
types.ts
code
import type { PropsWithChildren } from "react";
export type TProviderProps = PropsWithChildren;
export type TSubscriberCallback = () => void;
export type TCartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
export type TContext = {
// Getters
getItems: () => TCartItem[];
getIsCartOpen: () => boolean;
getTotalPrice: () => number;
// Setters
setItems: (value: TCartItem[]) => void;
setIsCartOpen: (value: boolean) => void;
// Subscribe
subscribe: (callback: TSubscriberCallback) => () => void;
};
Context.tsx
code
import { createContext, useCallback, useRef } from "react";
import type { TContext, TProviderProps, TSubscriberCallback, TCartItem } from "./types";
export const Context = createContext<TContext | null>(null);
export function Provider({ children }: TProviderProps) {
// State refs
const items = useRef<TCartItem[]>([]);
const isCartOpen = useRef<boolean>(false);
// Subscribers
const subscribers = useRef<Set<TSubscriberCallback>>(new Set());
// Getters
const getItems = useCallback((): TCartItem[] => {
return items.current;
}, []);
const getIsCartOpen = useCallback((): boolean => {
return isCartOpen. current;
}, []);
const getTotalPrice = useCallback((): number => {
return items.current.reduce(
(total, item) => total + item.price * item. quantity,
0
);
}, []);
// Setters
const setItems = useCallback((value: TCartItem[]): void => {
items. current = value;
subscribers.current.forEach((callback) => callback());
}, []);
const setIsCartOpen = useCallback((value: boolean): void => {
if (isCartOpen.current === value) {
return;
}
isCartOpen.current = value;
subscribers.current.forEach((callback) => callback());
}, []);
// Subscribe
const subscribe = useCallback((callback: TSubscriberCallback) => {
subscribers.current.add(callback);
return (): void => {
subscribers.current.delete(callback);
};
}, []);
return (
<Context.Provider
value={{
getItems,
getIsCartOpen,
getTotalPrice,
setItems,
setIsCartOpen,
subscribe,
}}
>
{children}
</Context.Provider>
);
}
useContext.ts
code
import { useContext } from "react";
import { Context } from "./Context";
import type { TContext } from "./types";
export const useShoppingCartContext = (): TContext => {
const context = useContext(Context);
if (context === null) {
throw new Error("ShoppingCart context is not available");
}
return context;
};
useSelectors.ts
code
import { useSyncExternalStore } from "react";
import { useShoppingCartContext } from "./useContext";
import type { TCartItem } from "./types";
// Selectors
export const useCartItemsSelector = (): TCartItem[] => {
const { getItems, subscribe } = useShoppingCartContext();
return useSyncExternalStore(subscribe, getItems, getItems);
};
export const useCartOpenSelector = (): boolean => {
const { getIsCartOpen, subscribe } = useShoppingCartContext();
return useSyncExternalStore(subscribe, getIsCartOpen, getIsCartOpen);
};
export const useCartTotalPriceSelector = (): number => {
const { getTotalPrice, subscribe } = useShoppingCartContext();
return useSyncExternalStore(subscribe, getTotalPrice, getTotalPrice);
};
// Dispatchers
export const useSetCartItemsDispatch = () => {
return useShoppingCartContext().setItems;
};
export const useSetCartOpenDispatch = () => {
return useShoppingCartContext().setIsCartOpen;
};
index.ts
code
export { Provider as ShoppingCartProvider } from "./Context";
export { useShoppingCartContext } from "./useContext";
export {
useCartItemsSelector,
useCartOpenSelector,
useCartTotalPriceSelector,
useSetCartItemsDispatch,
useSetCartOpenDispatch,
} from "./useSelectors";
export type { TContext, TCartItem } from "./types";
AI Agent Checklist
When generating Context Pattern, ensure you:
- •Create all 4 files:
Context.tsx,types.ts,useContext.ts,useSelectors.ts - •Use
useReffor state storage (NOTuseState) - •Use
useRef<Set<TSubscriberCallback>>for subscribers - •Wrap all functions in
useCallbackwith empty dependency array - •Create getter for each state:
get{StateName} - •Create setter for each state:
set{StateName} - •Setters must notify all subscribers:
subscribers.current.forEach((callback) => callback()) - •Subscribe function must return unsubscribe cleanup function
- •Custom hook throws error when context is null
- •Selectors use
useSyncExternalStore(subscribe, getter, getter) - •Dispatchers return setter functions directly
- •Follow naming conventions for all hooks and types
- •Export everything from
index.ts - •Add TypeScript types for all functions and variables
useSyncExternalStore API Reference
code
const state = useSyncExternalStore( subscribe, // (callback) => unsubscribe function getSnapshot, // () => current state (client) getServerSnapshot // () => current state (server/SSR) - optional );
Parameters
- •subscribe: This is a function to watch the store. When the data changes, it calls a "callback". It also returns a function to stop watching.
- •getSnapshot: This is a function to get the current data from the store. Use this to see the state right now.
- •getServerSnapshot: This is an optional function for SSR (Server-Side Rendering). Usually, you can use the same function as getSnapshot.