Nov 13, 2026
Stop fetching your dashboard data in useEffect
Every dashboard I built in my first two years started the same way. The page mounts, the table is empty, a spinner spins, and a second later the data pops in. I wrote that pattern so many times I stopped seeing it as a pattern at all — it was just how you loaded data in React:
"use client";
export default function OrdersPage() {
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/orders")
.then((r) => r.json())
.then(setOrders)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
return <OrdersTable orders={orders} />;
}
It works. It also wastes a whole round trip before it even starts. The browser downloads the JavaScript, React hydrates, the component mounts, and only then does the effect fire and the fetch begin. The data you needed to render the page is the last thing to arrive, because you asked for it last. On a slow connection or a cheap phone — the kind of device a shop owner in Dhaka actually checks their dashboard on — that gap is the whole experience.
The deeper problem is that useEffect runs after render by design. It is a hook for synchronising with something outside React, not a place to declare what a component needs. Fetching there means the loading state, the error state, and the empty state are all yours to hand-roll, and the race conditions are yours to find: a slow response arriving after the user navigated away, two requests in flight and the older one winning, the double fetch in development under StrictMode. I have debugged all three, and none of them taught me anything worth knowing.
In Next.js, the fix is to stop fetching on the client for the first paint entirely. A Server Component can be async, so it just awaits the data and renders:
export default async function OrdersPage() {
const orders = await getOrders(); // runs on the server
return <OrdersTable orders={orders} />;
}
No useEffect, no loading state, no client-side fetch at all. The data is in the HTML before it reaches the browser. And because this runs on the server, getOrders can talk to the database directly — there is no API route in between, no extra hop, and no secret leaking into client code. The rule I use now: Server Components fetch data, Client Components handle interaction. If a component does not need state or browser APIs, it stays a Server Component.
The objection I hear is the slow widget. One chart takes two seconds, so the whole page waits two seconds. That is what <Suspense> is for — split the page into sections and let each one stream in when it is ready:
export default function DashboardPage() {
return (
<>
<RevenueSummary /> {/* fast, renders immediately */}
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</>
);
}
async function RecentOrders() {
const orders = await getRecentOrders();
return <OrdersTable orders={orders} />;
}
The shell and the fast sections render straight away; the slow table streams in behind its skeleton. The boundary has to wrap the async component itself — put the fetch outside it and nothing suspends, which is a mistake I made the first time and stared at for longer than I will admit.
The interactive parts still live on the client, but they become leaves. The filter dropdown, the row actions, the modal — they take the data as props and only own the interaction:
export default async function OrdersPage() {
const orders = await getOrders();
return <OrdersTable orders={orders} />; // Client Component, receives data
}
A few honest limits. useEffect is not banned — it is the right tool when the data genuinely depends on something only the browser knows, like a value the user just typed or a position from navigator.geolocation. Live values that keep changing after the page loads — a stock count ticking down, a delivery status — still need client-side fetching, and that is where a library like TanStack Query earns its keep: caching, deduping, refetching, all the things a hand-rolled effect does badly. And if you are not on Next.js, none of this applies in the same way — the server component pattern needs a framework that supports it.
The question to ask about any fetch in your codebase is simple: did you know you needed this data before the component rendered? If the answer is yes, declare it before render instead of asking for it after. Your dashboard will load faster, your loading states will mostly disappear, and the race conditions you used to debug on a Friday evening will stop existing.