
ReactJS
Advanced Context design involves creating reusable, type-safe context factories using TypeScript generics. This allows you to build a consistent context pattern across your application while maintaining strict type safety.
A context factory is a function that generates a Context, Provider, and hook with full type safety. This pattern eliminates boilerplate and ensures consistency:
import { createContext, useContext, useState, ReactNode } from "react";
// Generic factory function
function createContextModule<T>(defaultValue: T) {
const Context = createContext<T | undefined>(undefined);
function Provider({ children, value }: { children: ReactNode; value: T }) {
return <Context.Provider value={value}>{children}</Context.Provider>;
}
function useContextModule() {
const context = useContext(Context);
if (context === undefined) {
throw new Error("useContextModule must be used within Provider");
}
return context;
}
return { Provider, useContextModule, Context };
}
// Example: Create theme context with factory
interface ThemeConfig {
primaryColor: string;
secondaryColor: string;
fontSize: number;
isDark: boolean;
}
const ThemeContextModule = createContextModule<ThemeConfig>({
primaryColor: "#007bff",
secondaryColor: "#6c757d",
fontSize: 16,
isDark: false,
});
function App() {
return (
<ThemeContextModule.Provider
value={{
primaryColor: "#007bff",
secondaryColor: "#6c757d",
fontSize: 16,
isDark: false,
}}
>
<Header />
</ThemeContextModule.Provider>
);
}
function Header() {
const theme = ThemeContextModule.useContextModule();
return <h1 style={{ color: theme.primaryColor }}>Header</h1>;
}For large applications, you often need multiple Contexts that work together. The composition pattern allows you to create a unified state management layer:
import {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from "react";
// Define separate state shapes
interface UserState {
id: string;
name: string;
email: string;
}
interface UIState {
sidebarOpen: boolean;
themeMode: "light" | "dark";
selectedTab: string;
}
interface AppContextType {
user: UserState;
ui: UIState;
setUser: (user: UserState) => void;
updateUI: (partial: Partial<UIState>) => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
// Unified provider managing multiple state slices
export function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<UserState>({
id: "1",
name: "Alice",
email: "alice@example.com",
});
const [ui, setUI] = useState<UIState>({
sidebarOpen: true,
themeMode: "light",
selectedTab: "home",
});
const updateUI = useCallback((partial: Partial<UIState>) => {
setUI((prev) => ({ ...prev, ...partial }));
}, []);
const value: AppContextType = {
user,
ui,
setUser,
updateUI,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// Single hook to access the entire context
export function useAppContext() {
const context = useContext(AppContext);
if (!context) {
throw new Error("useAppContext must be within AppProvider");
}
return context;
}
// Selector hooks for specific slices (prevents unnecessary re-renders)
export function useUser() {
const { user } = useAppContext();
return user;
}
export function useUI() {
const { ui } = useAppContext();
return ui;
}
export function useThemeMode() {
const { ui, updateUI } = useAppContext();
return {
themeMode: ui.themeMode,
setThemeMode: (mode: "light" | "dark") => updateUI({ themeMode: mode }),
};
}<details>
<summary>📚 More Examples</summary>
// Example 1: Generic state context factory with actions
interface Action<T> {
type: string;
payload?: any;
}
function createStateContext<T, A extends Action<T>>(
reducer: (state: T, action: A) => T,
initialState: T
) {
const Context = createContext<{ state: T; dispatch: (action: A) => void } | undefined>(
undefined
);
function Provider({ children }: { children: ReactNode }) {
const [state, dispatch] = useState(initialState);
const handleDispatch = (action: A) => {
dispatch((prev) => reducer(prev, action));
};
return (
<Context.Provider value={{ state, dispatch: handleDispatch }}>
{children}
</Context.Provider>
);
}
function useContextState() {
const context = useContext(Context);
if (!context) {
throw new Error("useContextState must be within Provider");
}
return context;
}
return { Provider, useContextState };
}
// Example usage
type CounterState = { count: number; step: number };
type CounterAction =
| { type: "INCREMENT"; payload?: void }
| { type: "DECREMENT"; payload?: void }
| { type: "SET_STEP"; payload: number };
const counterReducer = (state: CounterState, action: CounterAction) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + state.step };
case "DECREMENT":
return { ...state, count: state.count - state.step };
case "SET_STEP":
return { ...state, step: action.payload };
default:
return state;
}
};
const CounterContext = createStateContext(counterReducer, {
count: 0,
step: 1,
});
function Counter() {
const { state, dispatch } = CounterContext.useContextState();
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<input
type="number"
value={state.step}
onChange={(e) =>
dispatch({ type: "SET_STEP", payload: Number(e.target.value) })
}
/>
</div>
);
}
// Example 2: Async context operations
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function createAsyncContext<T>(defaultValue: T) {
const Context = createContext<
| {
state: AsyncState<T>;
fetchData: (fn: () => Promise<T>) => Promise<void>;
reset: () => void;
}
| undefined
>(undefined);
function Provider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AsyncState<T>>({
data: defaultValue,
loading: false,
error: null,
});
const fetchData = async (fn: () => Promise<T>) => {
setState({ data: null, loading: true, error: null });
try {
const data = await fn();
setState({ data, loading: false, error: null });
} catch (error) {
setState({ data: null, loading: false, error: error as Error });
}
};
const reset = () => {
setState({ data: defaultValue, loading: false, error: null });
};
return (
<Context.Provider value={{ state, fetchData, reset }}>
{children}
</Context.Provider>
);
}
function useAsyncContext() {
const context = useContext(Context);
if (!context) {
throw new Error("useAsyncContext must be within Provider");
}
return context;
}
return { Provider, useAsyncContext };
}
// Usage
const UserDataContext = createAsyncContext<UserState>({
id: "",
name: "",
email: "",
});
function UserProfile() {
const { state, fetchData } = UserDataContext.useAsyncContext();
if (state.loading) return <p>Loading...</p>;
if (state.error) return <p>Error: {state.error.message}</p>;
if (!state.data) return <p>No data</p>;
return <div>{state.data.name}</div>;
}</details>
A production-ready context system organizing multiple features:
// types/context.ts
export interface ContextModule<T> {
Provider: (props: { children: ReactNode; value?: T }) => JSX.Element;
useModule: () => T;
Context: React.Context<T | undefined>;
}
// contexts/createContextModule.ts
import { createContext, useContext, ReactNode } from "react";
export function createContextModule<T>(name: string): ContextModule<T> {
const Context = createContext<T | undefined>(undefined);
function Provider({ children, value }: { children: ReactNode; value?: T }) {
if (!value) {
throw new Error(`${name} Provider requires a value prop`);
}
return <Context.Provider value={value}>{children}</Context.Provider>;
}
function useModule(): T {
const context = useContext(Context);
if (!context) {
throw new Error(`${name} hook used outside of its Provider`);
}
return context;
}
return { Provider, useModule, Context };
}
// contexts/auth/types.ts
export interface AuthContextValue {
user: { id: string; email: string } | null;
login(email: string, password: string): Promise<void>;
logout(): void;
isLoading: boolean;
}
// contexts/auth/AuthContext.tsx
import { createContextModule } from "../createContextModule";
import { AuthContextValue } from "./types";
export const AuthContext = createContextModule<AuthContextValue>("Auth");
// hooks/useAuth.ts
export function useAuth() {
return AuthContext.useModule();
}
// contexts/ui/types.ts
export interface UIContextValue {
isDarkMode: boolean;
toggleDarkMode: () => void;
sidebarOpen: boolean;
toggleSidebar: () => void;
}
// contexts/ui/UIContext.tsx
import { createContextModule } from "../createContextModule";
import { UIContextValue } from "./types";
export const UIContext = createContextModule<UIContextValue>("UI");
// hooks/useUI.ts
export function useUI() {
return UIContext.useModule();
}
// Root App with all contexts composed
function App() {
const [authState, setAuthState] = useState<AuthContextValue>({
user: null,
login: async (email, password) => {
// Login logic
},
logout: () => {},
isLoading: false,
});
const [uiState, setUIState] = useState<UIContextValue>({
isDarkMode: false,
toggleDarkMode: () =>
setUIState((prev) => ({ ...prev, isDarkMode: !prev.isDarkMode })),
sidebarOpen: true,
toggleSidebar: () =>
setUIState((prev) => ({ ...prev, sidebarOpen: !prev.sidebarOpen })),
});
return (
<AuthContext.Provider value={authState}>
<UIContext.Provider value={uiState}>
<MainApp />
</UIContext.Provider>
</AuthContext.Provider>
);
}
// Using in components
function Header() {
const { user } = useAuth();
const { isDarkMode, toggleDarkMode } = useUI();
return (
<header style={{ background: isDarkMode ? "#000" : "#fff" }}>
<h1>{user?.email}</h1>
<button onClick={toggleDarkMode}>Toggle Theme</button>
</header>
);
}| Pattern | Complexity | Flexibility | Type Safety |
|---|---|---|---|
| Simple Context | Low | Medium | Good |
| Factory Pattern | Medium | High | Excellent |
| Multi-feature | High | Very High | Excellent |
| Redux-like | Very High | Maximum | Excellent |
Ready to practice? Challenges | Next: Multiple Providers
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward