Article
State Management Chaos: When Your App Can't Remember Anything
Data disappears on refresh, state updates don't propagate, and users see stale information—the state management disasters that plague AI-generated apps.
State Management Chaos: When Your App Can't Remember Anything
Your user fills out a complex form with 20 fields. They accidentally refresh the page. Everything is gone. They rage-quit your app and leave a 1-star review. Sound dramatic? This exact scenario happens thousands of times daily in AI-generated applications with broken state management.
State management is where AI coding tools truly struggle—because it requires understanding how data flows through an entire application over time, not just generating isolated functions.
The "Data Disappears on Refresh" Problem
This is the number one complaint about AI-generated apps. Users interact with your app, everything works, then they refresh and all their data vanishes.
Why This Happens
AI tools default to in-memory state that clears on every page load:
// ❌ State lost on refresh
function ShoppingCart() {
const [items, setItems] = useState([]);
return (
// Cart looks great until user refreshes
);
}
The Fix: Persistent State
Option 1: LocalStorage
// ✅ Persist to localStorage
function ShoppingCart() {
const [items, setItems] = useState<CartItem[]>(() => {
const saved = localStorage.getItem('cart');
return saved ? JSON.parse(saved) : [];
});
// Save whenever items change
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(items));
}, [items]);
return (
// Cart persists across refreshes
);
}
Option 2: SessionStorage (clears when tab closes)
// For sensitive data that should clear when browser closes
const [userData, setUserData] = useState(() => {
const saved = sessionStorage.getItem('userData');
return saved ? JSON.parse(saved) : null;
});
Option 3: IndexedDB (for large amounts of data)
import { openDB } from 'idb';
const db = await openDB('myApp', 1, {
upgrade(db) {
db.createObjectStore('cart');
},
});
// Store data
await db.put('cart', items, 'cartItems');
// Retrieve data
const items = await db.get('cart', 'cartItems');
State Not Syncing Across Components
User updates their profile in one component, but the header still shows the old name. Classic AI-generated state management failure.
The Problem: Prop Drilling Hell
// ❌ Passing state through 5 levels of components
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Header user={user} setUser={setUser} />;
}
function Header({ user, setUser }) {
return <Profile user={user} setUser={setUser} />;
}
// And so on...
Solution 1: Context API
// ✅ Create context for shared state
interface UserContextType {
user: User | null;
setUser: (user: User | null) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// Use anywhere in the tree
function Profile() {
const { user, setUser } = useContext(UserContext);
// Update here affects all components
}
function Header() {
const { user } = useContext(UserContext);
// Automatically sees updates
}
Solution 2: State Management Libraries
For complex apps, use dedicated libraries:
// Zustand (simple and powerful)
import create from 'zustand';
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
cart: [],
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
}));
// Use in any component
function Profile() {
const { user, setUser } = useStore();
}
function Cart() {
const { cart, addToCart } = useStore();
}
Race Conditions and Stale State
User clicks "Add to Cart" three times quickly. Only one item appears. Or worse—three API calls fire, but only the first one succeeds and the UI shows conflicting data.
The Problem
// ❌ Race condition
function addToCart(item) {
setIsLoading(true);
fetch('/api/cart', {
method: 'POST',
body: JSON.stringify(item),
})
.then(res => res.json())
.then(data => {
setCart(data.cart); // Which response arrives first?
setIsLoading(false);
});
}
The Fix: Proper Request Handling
// ✅ Cancel previous requests
function addToCart(item: CartItem) {
// Abort previous request
const controller = new AbortController();
setIsLoading(true);
fetch('/api/cart', {
method: 'POST',
body: JSON.stringify(item),
signal: controller.signal,
})
.then(res => res.json())
.then(data => {
setCart(data.cart);
setIsLoading(false);
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request cancelled');
}
});
// Cleanup
return () => controller.abort();
}
// Or use optimistic updates
function addToCart(item: CartItem) {
// Update UI immediately
setCart(prev => [...prev, item]);
// Sync with server in background
fetch('/api/cart', {
method: 'POST',
body: JSON.stringify(item),
}).catch(() => {
// Rollback on error
setCart(prev => prev.filter(i => i.id !== item.id));
showError('Failed to add item');
});
}
The Infinite Loop of Death
The app loads, makes an API call, updates state, which triggers another render, which makes another API call, which updates state... your API bill explodes and the app becomes unusable.
The Problem
// ❌ Infinite loop
function UserProfile() {
const [user, setUser] = useState(null);
// This runs on EVERY render
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data)); // Causes re-render, repeat forever
return <div>{user?.name}</div>;
}
The Fix: Proper Effect Dependencies
// ✅ Only fetch once on mount
function UserProfile() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []); // Empty array = only run once
return <div>{user?.name}</div>;
}
// ✅ Fetch when specific value changes
function UserPosts({ userId }: { userId: string }) {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(data => setPosts(data));
}, [userId]); // Re-fetch when userId changes
return <PostList posts={posts} />;
}
State Updates Not Reflecting in UI
User clicks a button, the state updates, but the UI stays the same. Or the UI updates, but the underlying data is wrong.
Common Causes
1. Mutating State Directly
// ❌ Direct mutation doesn't trigger re-render
function updateUser() {
user.name = 'New Name';
setUser(user); // React sees same object reference, doesn't re-render
}
// ✅ Create new object
function updateUser() {
setUser({ ...user, name: 'New Name' });
}
// ❌ Mutating arrays
function addItem(item: Item) {
items.push(item);
setItems(items); // Doesn't re-render
}
// ✅ Create new array
function addItem(item: Item) {
setItems([...items, item]);
}
2. Asynchronous State Updates
// ❌ Stale closure
function increment() {
setCount(count + 1);
setCount(count + 1); // Uses old 'count' value
// If count was 0, it becomes 1, not 2
}
// ✅ Use functional updates
function increment() {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Correctly increments by 2
}
Form State Management Nightmares
AI tools generate forms with terrible state management:
// ❌ AI-generated form mess
function ComplexForm() {
const [field1, setField1] = useState('');
const [field2, setField2] = useState('');
const [field3, setField3] = useState('');
// ... 20 more useState calls
const [error1, setError1] = useState('');
const [error2, setError2] = useState('');
// ... 20 more error states
}
Better Form State Management
// ✅ Single state object
interface FormData {
name: string;
email: string;
phone: string;
address: string;
}
function ComplexForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
phone: '',
address: '',
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const handleChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user types
setErrors(prev => ({ ...prev, [field]: '' }));
};
return (
<form>
<input
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
{errors.name && <span>{errors.name}</span>}
</form>
);
}
Or Use Form Libraries
// React Hook Form (excellent choice)
import { useForm } from 'react-hook-form';
function ComplexForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name', { required: true })} />
{errors.name && <span>Name is required</span>}
<input {...register('email', {
required: true,
pattern: /^\S+@\S+$/i
})} />
{errors.email && <span>Valid email required</span>}
</form>
);
}
Global State Pollution
AI generates code where everything is in global state, making the app unpredictable:
// ❌ Everything in global state
const globalStore = {
user: null,
cart: [],
products: [],
orders: [],
notifications: [],
theme: 'light',
isLoading: false,
error: null,
// 50 more properties...
};
Better State Organization
// ✅ Separate concerns
// User state
const useUserState = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
// Cart state (separate)
const useCartState = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
}));
// UI state (separate)
const useUIState = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
Server State vs. Client State Confusion
AI tools often treat server data like local state:
// ❌ Treating server data as local state
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
// Data might be stale, no refetching, no cache
}
Use Server State Libraries
// ✅ React Query (handles caching, refetching, etc.)
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
staleTime: 5000, // Consider fresh for 5 seconds
refetchOnWindowFocus: true, // Refetch when user returns to tab
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading users</div>;
return <UserList users={users} />;
}
State Synchronization Across Browser Tabs
User opens your app in two tabs, makes a change in one, but the other tab shows stale data.
The Solution: Broadcast Channel API
// ✅ Sync state across tabs
const channel = new BroadcastChannel('app-state');
function useSharedState<T>(key: string, initialValue: T) {
const [state, setState] = useState<T>(initialValue);
useEffect(() => {
// Listen for updates from other tabs
channel.onmessage = (event) => {
if (event.data.key === key) {
setState(event.data.value);
}
};
}, [key]);
const setSharedState = (value: T) => {
setState(value);
// Broadcast to other tabs
channel.postMessage({ key, value });
};
return [state, setSharedState] as const;
}
// Usage
const [user, setUser] = useSharedState('user', null);
Memory Leaks from Abandoned State
State updates continue after component unmounts, causing memory leaks and console warnings.
The Problem
// ❌ Updates state after unmount
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data)); // Might run after unmount
}, []);
}
The Fix
// ✅ Cancel on unmount
function UserProfile() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
fetch('/api/user')
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true;
};
}, []);
}
When State Management Gets Complex
Sometimes state issues are symptoms of deeper architectural problems:
- Complex state machines AI can't model
- Real-time synchronization requirements
- Offline-first data strategies
- Multi-user collaborative features
- Undo/redo functionality
These require professional state architecture design.
State management turning your app into chaos? Book a hardening sprint and we'll architect a robust state management solution.