State management is one of the most debated topics in frontend development. With countless libraries and patterns available, choosing the right approach requires understanding your application's specific needs. AI can help you navigate these decisions.
Understanding State Categories#
Not all state is equal. Categorizing state helps choose the right management approach:
Local UI State#
- Form input values
- Modal open/closed
- Dropdown selections
- Animation states
Pattern: Component state (useState, useReducer)
Shared UI State#
- Theme preferences
- Sidebar collapsed
- Active filters
- Sort orders
Pattern: Context API or lightweight stores
Server Cache State#
- API response data
- User profiles
- Product listings
- Search results
Pattern: React Query, SWR, Apollo
Global Application State#
- Authentication
- User permissions
- Shopping cart
- Notification queue
Pattern: Redux, Zustand, Jotai
Pattern 1: Local State with Hooks#
For truly local state, hooks are sufficient:
1function SearchForm() {
2 const [query, setQuery] = useState('');
3 const [isExpanded, setIsExpanded] = useState(false);
4
5 return (
6 <form onSubmit={() => onSearch(query)}>
7 <input
8 value={query}
9 onChange={(e) => setQuery(e.target.value)}
10 onFocus={() => setIsExpanded(true)}
11 />
12 {isExpanded && <FilterOptions />}
13 </form>
14 );
15}When to use: State doesn't need to be shared, component lifecycle matches state lifecycle.
Pattern 2: Lifting State Up#
When siblings need to share state:
1function ProductPage() {
2 const [selectedVariant, setSelectedVariant] = useState(null);
3
4 return (
5 <div>
6 <ProductImages variant={selectedVariant} />
7 <ProductDetails
8 selectedVariant={selectedVariant}
9 onSelectVariant={setSelectedVariant}
10 />
11 <AddToCart variant={selectedVariant} />
12 </div>
13 );
14}When to use: Small component trees, clear data flow.
Pattern 3: Context for Dependency Injection#
Context works well for data that many components need:
1const ThemeContext = createContext<Theme>('light');
2const UserContext = createContext<User | null>(null);
3
4function App() {
5 const [theme, setTheme] = useState<Theme>('light');
6 const [user, setUser] = useState<User | null>(null);
7
8 return (
9 <ThemeContext.Provider value={{ theme, setTheme }}>
10 <UserContext.Provider value={user}>
11 <MainContent />
12 </UserContext.Provider>
13 </ThemeContext.Provider>
14 );
15}
16
17// Deeply nested component
18function UserAvatar() {
19 const user = useContext(UserContext);
20 const { theme } = useContext(ThemeContext);
21
22 return <Avatar user={user} theme={theme} />;
23}When to use: Data accessed by many components at different levels.
Warning: Context causes re-renders of all consumers. Split contexts by update frequency.
Pattern 4: Server State with React Query#
For data that comes from APIs:
1function ProductList({ categoryId }) {
2 const {
3 data: products,
4 isLoading,
5 error,
6 refetch
7 } = useQuery({
8 queryKey: ['products', categoryId],
9 queryFn: () => fetchProducts(categoryId),
10 staleTime: 5 * 60 * 1000, // 5 minutes
11 });
12
13 if (isLoading) return <Skeleton />;
14 if (error) return <ErrorMessage error={error} onRetry={refetch} />;
15
16 return <ProductGrid products={products} />;
17}Benefits:
- Automatic caching
- Background refetching
- Optimistic updates
- Request deduplication
When to use: Any data from external sources.
Pattern 5: Global Store with Zustand#
For application-wide state:
1interface CartStore {
2 items: CartItem[];
3 addItem: (item: CartItem) => void;
4 removeItem: (itemId: string) => void;
5 clear: () => void;
6 total: () => number;
7}
8
9const useCartStore = create<CartStore>((set, get) => ({
10 items: [],
11
12 addItem: (item) => set((state) => ({
13 items: [...state.items, item]
14 })),
15
16 removeItem: (itemId) => set((state) => ({
17 items: state.items.filter((i) => i.id !== itemId)
18 })),
19
20 clear: () => set({ items: [] }),
21
22 total: () => get().items.reduce((sum, item) => sum + item.price, 0),
23}));
24
25// Usage anywhere
26function CartIcon() {
27 const itemCount = useCartStore((state) => state.items.length);
28 return <Badge count={itemCount}><ShoppingCartIcon /></Badge>;
29}
30
31function CartTotal() {
32 const total = useCartStore((state) => state.total());
33 return <span>${total.toFixed(2)}</span>;
34}When to use: State that needs to persist across navigation, accessible from anywhere.
Pattern 6: Atomic State with Jotai#
For fine-grained reactivity:
1// Define atoms
2const filterAtom = atom<Filter>({ category: 'all', priceRange: [0, 100] });
3const sortAtom = atom<Sort>('newest');
4const searchAtom = atom<string>('');
5
6// Derived atom
7const filteredProductsAtom = atom((get) => {
8 const filter = get(filterAtom);
9 const sort = get(sortAtom);
10 const search = get(searchAtom);
11
12 return products
13 .filter(matchesFilter(filter))
14 .filter(matchesSearch(search))
15 .sort(sortBy(sort));
16});
17
18// Components only re-render when their atoms change
19function FilterPanel() {
20 const [filter, setFilter] = useAtom(filterAtom);
21 // Only re-renders when filter changes
22}
23
24function SearchBar() {
25 const [search, setSearch] = useAtom(searchAtom);
26 // Only re-renders when search changes
27}When to use: Need fine-grained updates without complex selectors.
Pattern 7: URL State#
State that should be shareable:
1function ProductSearch() {
2 const [searchParams, setSearchParams] = useSearchParams();
3
4 const category = searchParams.get('category') || 'all';
5 const sort = searchParams.get('sort') || 'popular';
6 const page = parseInt(searchParams.get('page') || '1');
7
8 const updateFilters = (updates: Partial<Filters>) => {
9 const newParams = new URLSearchParams(searchParams);
10 Object.entries(updates).forEach(([key, value]) => {
11 newParams.set(key, String(value));
12 });
13 setSearchParams(newParams);
14 };
15
16 return (
17 <div>
18 <FilterBar
19 category={category}
20 sort={sort}
21 onFilterChange={updateFilters}
22 />
23 <ProductGrid category={category} sort={sort} page={page} />
24 </div>
25 );
26}When to use: Filters, pagination, view modes—anything users might want to bookmark or share.
Choosing the Right Pattern#
AI can help evaluate your specific situation:
Help me choose a state management approach:
Application: E-commerce site
State needs:
- Shopping cart (persist across pages)
- User authentication
- Product filters (shareable via URL)
- Product data (from API)
- UI preferences (theme, currency)
- Form state (checkout flow)
Team: 3 developers, familiar with React hooks
Existing stack: React 18, TypeScript
Recommend state management strategy.
Anti-Patterns to Avoid#
Global State for Local Concerns#
1// ❌ Don't put form state in global store
2const useFormStore = create((set) => ({
3 email: '',
4 password: '',
5 setEmail: (email) => set({ email }),
6 setPassword: (password) => set({ password }),
7}));
8
9// ✅ Keep form state local
10function LoginForm() {
11 const [email, setEmail] = useState('');
12 const [password, setPassword] = useState('');
13 // ...
14}Duplicating Server State#
1// ❌ Don't duplicate API data in store
2const useProductStore = create((set) => ({
3 products: [],
4 fetchProducts: async () => {
5 const products = await api.getProducts();
6 set({ products });
7 },
8}));
9
10// ✅ Use a server state library
11const { data: products } = useQuery({
12 queryKey: ['products'],
13 queryFn: api.getProducts,
14});Over-Engineering Simple Apps#
// ❌ Don't add Redux for a simple app
// Store, slices, actions, selectors, middleware...
// ✅ Start simple, add complexity when needed
const [items, setItems] = useState([]);Migration Strategies#
From Props Drilling to Context#
Convert this props-drilling pattern to Context:
Currently passing 'user' through 5 component levels.
Only leaf components actually use 'user'.
Provide refactored code with appropriate context structure.
From Context to External Store#
Migrate this Context-based state to Zustand:
Current issues:
- Too many re-renders
- Complex value object
- Need for selectors
Provide migration path that doesn't break existing consumers.
Conclusion#
State management isn't one-size-fits-all. The best approach depends on:
- What kind of state you're managing
- How widely it's used
- How frequently it updates
- Whether it needs to survive navigation
- Your team's familiarity
Start with the simplest approach that works. Add complexity only when you hit real problems. Use AI to evaluate tradeoffs and identify the right pattern for your specific needs.