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.
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:
To fix this, we need to categorize data by ownership and lifecycle, not just syntax.
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β 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 β
βββββββββββββββββββ΄ββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ
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.
useState
or traditional Redux stores. Use dedicated server-state tools like
// Right: Server state managed by a tool built for caching & revalidation
const { data: user, is } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // 5 minutes before re-fetching
});
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.
?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}"/>;
}
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.
useState
or useReducer
. If it needs to be accessed globally across unrelated components (like a sidebar collapse state), use a lightweight client store like
// Simple, isolated local client state
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
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
/products?category=shoes&q=nike
). If the user emails this link to a friend, the friend sees the exact same Nike shoes.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.