
ReactJS
Managing complex, interdependent state in Context requires careful architecture. Understanding normalization, circular dependencies, and state composition prevents common pitfalls in large applications.
Complex applications often require multiple levels of Context that depend on each other. Proper design prevents circular dependencies and maintains clean data flow:
import {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from "react";
// Define independent domain models
interface User {
id: string;
name: string;
email: string;
teamId: string;
}
interface Team {
id: string;
name: string;
members: string[]; // User IDs
ownerId: string;
}
interface Project {
id: string;
name: string;
teamId: string;
ownerId: string;
members: string[];
}
// Separate contexts for each domain
type UserContextType = {
users: Record<string, User>;
currentUserId: string | null;
setCurrentUser: (userId: string) => void;
addUser: (user: User) => void;
};
type TeamContextType = {
teams: Record<string, Team>;
addTeam: (team: Team) => void;
updateTeam: (teamId: string, team: Partial<Team>) => void;
};
type ProjectContextType = {
projects: Record<string, Project>;
addProject: (project: Project) => void;
getProjectsByTeam: (teamId: string) => Project[];
};
const UserContext = createContext<UserContextType | undefined>(undefined);
const TeamContext = createContext<TeamContextType | undefined>(undefined);
const ProjectContext = createContext<ProjectContextType | undefined>(undefined);
// Composite provider managing all contexts
function DomainProvider({ children }: { children: ReactNode }) {
// User state
const [users, setUsers] = useState<Record<string, User>>({});
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const userValue: UserContextType = {
users,
currentUserId,
setCurrentUser: setCurrentUserId,
addUser: useCallback((user: User) => {
setUsers((prev) => ({ ...prev, [user.id]: user }));
}, []),
};
// Team state
const [teams, setTeams] = useState<Record<string, Team>>({});
const teamValue: TeamContextType = {
teams,
addTeam: useCallback((team: Team) => {
setTeams((prev) => ({ ...prev, [team.id]: team }));
}, []),
updateTeam: useCallback((teamId: string, updates: Partial<Team>) => {
setTeams((prev) => ({
...prev,
[teamId]: { ...prev[teamId], ...updates },
}));
}, []),
};
// Project state
const [projects, setProjects] = useState<Record<string, Project>>({});
const projectValue: ProjectContextType = {
projects,
addProject: useCallback((project: Project) => {
setProjects((prev) => ({ ...prev, [project.id]: project }));
}, []),
getProjectsByTeam: useCallback(
(teamId: string) => {
return Object.values(projects).filter((p) => p.teamId === teamId);
},
[projects]
),
};
return (
<UserContext.Provider value={userValue}>
<TeamContext.Provider value={teamValue}>
<ProjectContext.Provider value={projectValue}>
{children}
</ProjectContext.Provider>
</TeamContext.Provider>
</UserContext.Provider>
);
}
// Consume with cross-context queries
function TeamMembersList() {
const userContext = useContext(UserContext)!;
const teamContext = useContext(TeamContext)!;
const currentTeam = Object.values(teamContext.teams)[0];
const members = currentTeam?.members
.map((memberId) => userContext.users[memberId])
.filter(Boolean);
return (
<ul>
{members?.map((member) => (
<li key={member.id}>{member.name}</li>
))}
</ul>
);
}Normalizing state prevents redundancy and makes updates safer:
// Denormalized (problematic)
type BadState = {
users: Array<{
id: string;
name: string;
teams: Team[]; // Redundant team data
}>;
};
// Normalized (better)
type GoodState = {
users: Record<string, User>;
teams: Record<string, Team>;
relationships: Record<string, string[]>; // userId -> teamIds
};
interface NormalizedContextType {
users: Record<string, User>;
teams: Record<string, Team>;
userTeams: Record<string, string[]>; // userId -> teamIds
getUser: (userId: string) => User | undefined;
getUserTeams: (userId: string) => Team[];
addUserToTeam: (userId: string, teamId: string) => void;
removeUserFromTeam: (userId: string, teamId: string) => void;
}
const NormalizedContext = createContext<NormalizedContextType | undefined>(
undefined
);
function NormalizedProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState({
users: {} as Record<string, User>,
teams: {} as Record<string, Team>,
userTeams: {} as Record<string, string[]>,
});
const value: NormalizedContextType = {
users: state.users,
teams: state.teams,
userTeams: state.userTeams,
getUser: useCallback(
(userId: string) => state.users[userId],
[state.users]
),
getUserTeams: useCallback((userId: string) => {
const teamIds = state.userTeams[userId] || [];
return teamIds
.map((teamId) => state.teams[teamId])
.filter(Boolean);
}, [state.teams, state.userTeams]),
addUserToTeam: useCallback((userId: string, teamId: string) => {
setState((prev) => ({
...prev,
userTeams: {
...prev.userTeams,
[userId]: Array.from(
new Set([...(prev.userTeams[userId] || []), teamId])
),
},
}));
}, []),
removeUserFromTeam: useCallback((userId: string, teamId: string) => {
setState((prev) => ({
...prev,
userTeams: {
...prev.userTeams,
[userId]: (prev.userTeams[userId] || []).filter(
(id) => id !== teamId
),
},
}));
}, []),
};
return (
<NormalizedContext.Provider value={value}>
{children}
</NormalizedContext.Provider>
);
}
// Usage is cleaner with normalized state
function UserTeams({ userId }: { userId: string }) {
const { getUserTeams } = useContext(NormalizedContext)!;
const teams = getUserTeams(userId);
return (
<ul>
{teams.map((team) => (
<li key={team.id}>{team.name}</li>
))}
</ul>
);
}<details>
<summary>📚 More Examples</summary>
// Example 1: Immutable update patterns for nested data
function updateNestedContext<T>(
state: T,
path: string[],
value: any
): T {
if (path.length === 0) return value;
const [head, ...tail] = path;
const nested = state?.[head as keyof T] ?? {};
return {
...state,
[head]: updateNestedContext(nested, tail, value),
};
}
// Example 2: Event-driven context updates
interface ContextEvent {
type: string;
payload: any;
}
function useContextEvents(
handler: (state: any, event: ContextEvent) => any,
initialState: any
) {
const [state, setState] = useState(initialState);
const listeners = new Set<() => void>();
const dispatch = useCallback((event: ContextEvent) => {
setState((prev) => {
const next = handler(prev, event);
listeners.forEach((listener) => listener());
return next;
});
}, [handler]);
return { state, dispatch, subscribe: (fn: () => void) => listeners.add(fn) };
}
// Example 3: Complex state machine in context
type OrderState = {
status: "pending" | "processing" | "completed" | "failed";
items: OrderItem[];
total: number;
error?: string;
};
type OrderAction =
| { type: "ADD_ITEM"; payload: OrderItem }
| { type: "REMOVE_ITEM"; payload: string }
| { type: "START_PROCESSING" }
| { type: "COMPLETE" }
| { type: "FAIL"; payload: string };
function orderReducer(state: OrderState, action: OrderAction): OrderState {
switch (action.type) {
case "ADD_ITEM":
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case "REMOVE_ITEM":
const item = state.items.find((i) => i.id === action.payload);
return {
...state,
items: state.items.filter((i) => i.id !== action.payload),
total: state.total - (item?.price || 0),
};
case "START_PROCESSING":
return { ...state, status: "processing" };
case "COMPLETE":
return { ...state, status: "completed" };
case "FAIL":
return { ...state, status: "failed", error: action.payload };
default:
return state;
}
}
const OrderContext = createContext<{
state: OrderState;
dispatch: (action: OrderAction) => void;
} | undefined>(undefined);
function OrderProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(orderReducer, {
status: "pending",
items: [],
total: 0,
});
return (
<OrderContext.Provider value={{ state, dispatch }}>
{children}
</OrderContext.Provider>
);
}</details>
Managing product catalog, shopping cart, and user preferences together:
// State shape for e-commerce app
type ECommerceState = {
catalog: {
products: Record<string, Product>;
categories: Record<string, Category>;
filters: FilterState;
};
cart: {
items: CartItem[];
total: number;
lastUpdated: number;
};
user: {
preferences: UserPreferences;
addresses: Address[];
savedCards: PaymentMethod[];
};
};
function ECommerceProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<ECommerceState>({
catalog: { products: {}, categories: {}, filters: {} },
cart: { items: [], total: 0, lastUpdated: 0 },
user: { preferences: {}, addresses: [], savedCards: [] },
});
const value = {
state,
// Catalog operations
addProduct: (product: Product) => {
setState((prev) => ({
...prev,
catalog: {
...prev.catalog,
products: { ...prev.catalog.products, [product.id]: product },
},
}));
},
// Cart operations
addToCart: (product: Product, quantity: number) => {
setState((prev) => {
const existing = prev.cart.items.find((i) => i.productId === product.id);
return {
...prev,
cart: {
...prev.cart,
items: existing
? prev.cart.items.map((i) =>
i.productId === product.id
? { ...i, quantity: i.quantity + quantity }
: i
)
: [
...prev.cart.items,
{ productId: product.id, quantity, price: product.price },
],
total: prev.cart.total + product.price * quantity,
},
};
});
},
};
return (
<ECommerceContext.Provider value={value}>
{children}
</ECommerceContext.Provider>
);
}| Pattern | Complexity | Use Case |
|---|---|---|
| Single context | Low | Simple apps |
| Normalized state | Medium | Related entities |
| Multiple contexts | High | Domain separation |
| State machine | Very High | Complex workflows |
Ready to practice? Challenges | Next: Context Optimization
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