AI models write code better and faster than devs, but what AI cannot do reliably for your specific product is make the fundamental system design decisions like:
"Where should this piece of data actually live, who owns it, and what happens to the user experience when the network fails, the user refreshes, or a link is shared?"
The modern frontend engineer’s primary value has shifted from writing code to evaluating trade-offs.
If you put every piece of data into a global Redux store or local useState, you aren't just writing messy code, you are making a system design error. To build fast, resilient interfaces, you need to treat state management as a distribution problem across three distinct domains: Server State, Client State, and URL State.
Take a look at this example
You must have come across this type of code before...
// Wrong: Blending 3 different types of state into one
const [products, setProducts] = useState([]); // Server Data
const [searchQuery, setSearchQuery] = useState(""); // URL/Navigation Intent
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); // Local UI
useEffect(() => {
// Syncing server state based on local client variables
fetchProducts(searchQuery).then(setProducts);
}, [searchQuery]);
When you mix these together, your application breaks down in subtle, frustrating ways:
- Stale Data: You manually fetch server data, cache it in React memory, and forget to revalidate it when the database changes.
- State Blackout: The user customizes their search view, hits refresh, and gets reset to page one because the view state only lived in ephemeral browser memory
- Bloated Memory: Temporary UI toggles (like "is dropdown open") leak into global stores, making the app harder to debug and test.
To fix this, we need to categorize data by ownership and lifecycle, not just syntax.
The 3 Pillars of Frontend State Architecture
┌─────────────────┬───────────────────────────────┬──────────────────────────────────┐
│ State Category │ What It Really Is │ Best Tooling / Pattern │
├─────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 1. Server State │ External data owned by the DB │ Next.js Cache, TanStack Query, │
│ │ (Async, cached, shared) │ RTK Query, SWR │
├─────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 2. URL State │ Navigation intent │ `searchParams`, React Router, │
│ │ (Shareable, persistent) │ `window.location` │
├─────────────────┼───────────────────────────────┼──────────────────────────────────┤
│ 3. Client State │ Ephemeral UI memory │ `useState`, `useReducer`, │
│ │ (Local, temporary, interactive)│ Zustand, React Context │
└─────────────────┴───────────────────────────────┴──────────────────────────────────┘
1. Server State: You Don't Own It
The Mental Model: Server state is data that lives on a remote database (user profiles, product lists, checkout carts). You do not own this data on the client - you're merely borrowing a temporary snapshot of it.
Because server state is asynchronous and shared across multiple users, its main challenges are staleness, caching, deduplication, and revalidation.
- When to use it: Any data that originates from a database or external API.
-
How to manage it: Stop putting API response data into local
useStateor traditional Redux stores. Use dedicated server-state tools like TanStack Query, SWR, or Next.js Server Components with Data Caching.
// Right: Server state managed by a tool built for caching & revalidation
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // 5 minutes before re-fetching
});
2. URL State: The Forgotten Source of Truth
The Mental Model: URL state is the input parameter for your entire screen. It represents navigation intent, what specific view, page, or filtered slice of data the user wants to see right now.
If a user configures a complex search filter on an e-commerce site, that view should exist as a shareable link. If you trap those filters inside React’s useState, you strip the web of its best native feature: the link.
- When to use it: Search queries, category filters, pagination numbers, active tabs, sorting directions, and selected view modes (grid vs. list).
-
How to manage it: Sync UI controls directly to URL search parameters (
?search=shoes&page=2).
// URL State as the single source of truth for view parameters
export default async function CatalogPage({ searchParams }) {
const { category, page } = await searchParams;
// The server/API reads directly from the URL context
const products = await getProducts({ category, page: Number(page) || 1 });
return <ProductGrid products="{products}"/>;
}
3. Client State: Ephemeral UI Memory
The Mental Model: Client state is purely local, temporary UI memory. It does not come from a database, and it doesn't need to survive a page share or a browser refresh.
If the user closes the tab, this data can safely disappear without ruining their experience.
- When to use it: Modal open/close toggles, hover states, active tab animations, form input drafts before submission, dark mode toggles, and multi-step wizard progress.
-
How to manage it: Keep it close to where it's used with
useStateoruseReducer. If it needs to be accessed globally across unrelated components (like a sidebar collapse state), use a lightweight client store like Zustand.
// Simple, isolated local client state
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
Evaluating the Trade-offs
When designing a feature, ask yourself these three sequential questions to determine where state belongs:
[New Piece of State]
│
Does it come from an API/Database?
┌────────────────┴────────────────┐
YES NO
│ │
(1. SERVER STATE) Should this view be shareable
Use: Next.js / TanStack or survive a page refresh?
┌───────────┴───────────┐
YES NO
│ │
(2. URL STATE) (3. CLIENT STATE)
Use: searchParams Use: useState / Zustand
Practical Example - an E-Commerce Product Page
- The Product List Data: This is server state. It should be fetched and cached via server components or a query hook so it can revalidate in the background.
-
The Active Category & Search Term: This is URL state (
/products?category=shoes&q=nike). If the user emails this link to a friend, the friend sees the exact same Nike shoes. - The "Filter Drawer Open" Toggle: This is client state The friend doesn't need the filter drawer to automatically pop open just because the sender had it open.
Conclusion
Writing syntax is rapidly becoming an automated commodity. You can prompt an AI tool to write a complex reducer or a fetch handler in seconds.
However, choosing where state lives, how it expires, and how it travels across the network requires engineering judgment.
By cleanly separating your architecture into Server State (remote truth), URL State (navigation truth), and Client State (interactive truth), you build applications that are inherently faster, far more resilient, and built to scale.
Top comments (0)