
ReactJS
As applications scale, managing multiple interdependent state variables becomes complex. Strategic state structure prevents bugs, improves performance, and makes code maintainable.
When multiple state values must stay in sync, naive approaches create inconsistent states. Use compound state or careful structure to ensure atomicity.
Related state should update together. If changing one value requires updating others, they should be tied together to prevent intermediate invalid states.
import { useState } from 'react';
// β BAD: Independent state variables that should be linked
function BadOrderForm() {
const [items, setItems] = useState<string[]>([]);
const [total, setTotal] = useState(0);
const [itemCount, setItemCount] = useState(0);
const addItem = (item: string, price: number) => {
// Inconsistency window: items updated, but total and count aren't yet
setItems([...items, item]);
setTotal(total + price); // What if this fails?
setItemCount(itemCount + 1); // What if this fails?
};
}
// β
GOOD: Compound state structure
function GoodOrderForm() {
interface Order {
items: string[];
prices: number[];
total: number;
}
const [order, setOrder] = useState<Order>({
items: [],
prices: [],
total: 0,
});
const addItem = (item: string, price: number) => {
// Atomic update - all or nothing
setOrder((prev) => {
const newItems = [...prev.items, item];
const newPrices = [...prev.prices, price];
const newTotal = prev.total + price;
return {
items: newItems,
prices: newPrices,
total: newTotal,
};
});
};
return (
<div>
<p>Items: {order.items.length}</p>
<p>Total: ${order.total}</p>
<button onClick={() => addItem('Apple', 1.99)}>Add Apple</button>
</div>
);
}<details>
<summary>π More Examples</summary>
// Example: Complex form state with interdependent fields
interface FormState {
email: string;
password: string;
confirmPassword: string;
isValid: boolean;
errors: Record<string, string>;
touched: Record<string, boolean>;
isDirty: boolean;
}
function ComplexForm() {
const [form, setForm] = useState<FormState>({
email: '',
password: '',
confirmPassword: '',
isValid: false,
errors: {},
touched: {},
isDirty: false,
});
const updateField = (field: string, value: string) => {
setForm((prev) => {
const newForm = { ...prev };
newForm[field as keyof FormState] = value as any;
newForm.touched = { ...prev.touched, [field]: true };
newForm.isDirty = true;
// Validate interdependent fields
const newErrors = { ...prev.errors };
if (field === 'password' || field === 'confirmPassword') {
if (newForm.password !== newForm.confirmPassword) {
newErrors.confirmPassword = 'Passwords must match';
} else {
delete newErrors.confirmPassword;
}
}
newForm.errors = newErrors;
newForm.isValid = Object.keys(newErrors).length === 0 && newForm.isDirty;
return newForm;
});
};
return (
<form>
<input
value={form.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={form.password}
onChange={(e) => updateField('password', e.target.value)}
placeholder="Password"
/>
<input
type="password"
value={form.confirmPassword}
onChange={(e) => updateField('confirmPassword', e.target.value)}
placeholder="Confirm Password"
/>
{form.errors.confirmPassword && (
<p style={{ color: 'red' }}>{form.errors.confirmPassword}</p>
)}
<button disabled={!form.isValid} type="submit">
Submit
</button>
</form>
);
}</details>
Normalized state is flat, preventing deep nesting. Denormalized state includes duplicated data for easier access. Choose based on your data relationships.
Normalized state reduces re-renders and prevents inconsistency. Denormalized state simplifies access patterns. Most applications use a mix.
import { useState, useMemo } from 'react';
// Deeply nested (problematic)
interface NestedUsers {
user1: {
profile: {
name: string;
settings: { theme: 'light' | 'dark' };
};
posts: Array<{ id: string; title: string }>;
};
}
// Normalized structure
interface NormalizedState {
users: Record<string, { id: string; name: string }>;
userSettings: Record<string, { theme: 'light' | 'dark' }>;
posts: Record<string, { id: string; title: string; userId: string }>;
userPostIds: Record<string, string[]>;
}
function UserManager() {
const [state, setState] = useState<NormalizedState>({
users: {
'1': { id: '1', name: 'Alice' },
},
userSettings: {
'1': { theme: 'dark' },
},
posts: {
'p1': { id: 'p1', title: 'Post 1', userId: '1' },
},
userPostIds: {
'1': ['p1'],
},
});
// Denormalize on demand for display
const userProfile = useMemo(() => {
const userId = '1';
const user = state.users[userId];
const settings = state.userSettings[userId];
const postIds = state.userPostIds[userId] || [];
return {
...user,
settings,
posts: postIds.map((id) => state.posts[id]),
};
}, [state]);
const updateUserTheme = (userId: string, theme: 'light' | 'dark') => {
setState((prev) => ({
...prev,
userSettings: {
...prev.userSettings,
[userId]: { theme },
},
}));
};
return (
<div>
<p>User: {userProfile.name}</p>
<p>Theme: {userProfile.settings.theme}</p>
<p>Posts: {userProfile.posts.length}</p>
<button onClick={() => updateUserTheme('1', 'light')}>
Switch Theme
</button>
</div>
);
}<details>
<summary>π More Examples</summary>
// Example: Selector pattern for consistent denormalization
interface AppState {
users: Record<string, { id: string; name: string; email: string }>;
posts: Record<string, { id: string; title: string; authorId: string; likes: number }>;
comments: Record<string, { id: string; text: string; postId: string; authorId: string }>;
}
// Selectors encapsulate denormalization logic
const selectors = {
getUserWithPosts: (state: AppState, userId: string) => {
const user = state.users[userId];
const userPosts = Object.values(state.posts).filter(
(post) => post.authorId === userId
);
return { ...user, posts: userPosts };
},
getPostWithComments: (state: AppState, postId: string) => {
const post = state.posts[postId];
const author = state.users[post.authorId];
const postComments = Object.values(state.comments).filter(
(c) => c.postId === postId
);
return {
...post,
author,
comments: postComments.map((c) => ({
...c,
author: state.users[c.authorId],
})),
};
},
};
function Blog() {
const [state] = useState<AppState>({
users: {
'u1': { id: 'u1', name: 'Alice', email: 'alice@example.com' },
},
posts: {
'p1': { id: 'p1', title: 'Hello', authorId: 'u1', likes: 5 },
},
comments: {
'c1': { id: 'c1', text: 'Nice!', postId: 'p1', authorId: 'u1' },
},
});
const post = selectors.getPostWithComments(state, 'p1');
return (
<article>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<ul>
{post.comments.map((c) => (
<li key={c.id}>
{c.text} - {c.author.name}
</li>
))}
</ul>
</article>
);
}</details>
Complex data like graphs or trees with circular references requires special handling to avoid infinite loops and maintain consistency.
Store references by ID, not by nesting objects. Use selector functions to reconstruct relationships when needed.
import { useState, useCallback } from 'react';
// Graph-like data structure
interface Node {
id: string;
name: string;
parentId: string | null;
childIds: string[];
}
interface TreeState {
nodes: Record<string, Node>;
root: string | null;
}
function TreeManager() {
const [state, setState] = useState<TreeState>({
nodes: {
'node-1': {
id: 'node-1',
name: 'Root',
parentId: null,
childIds: ['node-2', 'node-3'],
},
'node-2': {
id: 'node-2',
name: 'Child 1',
parentId: 'node-1',
childIds: [],
},
'node-3': {
id: 'node-3',
name: 'Child 2',
parentId: 'node-1',
childIds: [],
},
},
root: 'node-1',
});
// Safe tree traversal
const getNodeWithChildren = useCallback(
(nodeId: string): Node & { children: ReturnType<typeof getNodeWithChildren>[] } => {
const node = state.nodes[nodeId];
return {
...node,
children: node.childIds.map((id) => getNodeWithChildren(id)),
};
},
[state.nodes]
);
// Add child safely
const addChild = useCallback(
(parentId: string, childName: string) => {
const newNodeId = `node-${Date.now()}`;
setState((prev) => {
const newNode: Node = {
id: newNodeId,
name: childName,
parentId,
childIds: [],
};
return {
...prev,
nodes: {
...prev.nodes,
[newNodeId]: newNode,
[parentId]: {
...prev.nodes[parentId],
childIds: [...prev.nodes[parentId].childIds, newNodeId],
},
},
};
});
},
[]
);
return (
<div>
{state.root && (
<TreeNode
node={getNodeWithChildren(state.root)}
onAddChild={addChild}
/>
)}
</div>
);
}
function TreeNode({
node,
onAddChild,
}: {
node: any;
onAddChild: (parentId: string, name: string) => void;
}) {
return (
<div style={{ marginLeft: 20 }}>
<p>{node.name}</p>
<button onClick={() => onAddChild(node.id, 'New Child')}>
Add Child
</button>
{node.children.map((child: any) => (
<TreeNode key={child.id} node={child} onAddChild={onAddChild} />
))}
</div>
);
}ββββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββ
β Structure β Best For β Trade-offs β
ββββββββββββββββββββΌββββββββββββββββββΌββββββββββββββββββββ€
β Compound β Related values β More boilerplate β
β Normalized β Complex data β Requires selectorsβ
β Denormalized β Fast access β Sync issues β
β Hybrid β Most cases β More complex β
β Graph/Tree β Hierarchical β ID references β
ββββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββReady to practice? Challenges | Next: State Architecture Patterns
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