A Debug Session in Bogotá’s Dawn
It was 5 a.m. in Bogotá, the sky still indigo above Monserrate. I was screensharing with Diego, a freshly minted boot‑camper in Lisbon, untangling a dashboard that blinked “Loading…” forever. He’d wired plain React useEffect
calls straight to a flaky API; every tab change re‑fetched the same payload, crushing his café Wi‑Fi. By sunrise we had swapped one component to SWR, another to React Query, and left a third on vanilla useEffect
—a perfect live A/B test for today’s lesson.
Why This Matters Now
JavaScript moved API calls from controllers to components years ago, but the trade‑offs are still misunderstood. React 18’s concurrent features magnify race conditions; mobile users in Panamá demand snappy offline caches; and product managers want optimistic UI without “flash of stale data.” Libraries like SWR and React Query promise cache coherence and refetch policies out of the box, yet many junior devs still default to hand‑rolled useEffect
. Understanding when to graduate from DIY to purpose‑built tools is key to delivering apps that feel native.
Toolbelt at a Glance
Tool / Concept | One‑liner purpose |
---|---|
useEffect | Manual hook for side effects; flexible but verbose. |
SWR | Small hook (4.2 kB) that follows stale‑while‑revalidate strategy.swr.vercel.app |
React Query v5.83 | Full‑stack cache & mutation manager with devtools and persistence.GitHub |
CLI Command | What it does |
---|---|
npm i swr | Installs SWR core. |
npm i @tanstack/react-query | Installs latest React Query bundle. |
npm i --save-dev @tanstack/react-query-devtools | Adds Chrome‑like devtools overlay. |
Concept Foundations (Plain English)
- useEffect: You tell React exactly when to fetch and where to stash the result—fine‑grained, but every call duplicates cache logic.
- SWR: Returns cached data instantly (stale), fetches in the background (revalidate), then updates UI. The hook key becomes the cache ID.
- React Query: Treats remote state like a database table; queries and mutations sync automatically, with retry logic, pagination helpers, and offline persistence.
Hands‑On: Three Ways to Hit the Same Endpoint
1. Pure useEffect
tsxCopyEdit// TodoCount.tsx
import { useEffect, useState } from 'react';
export function TodoCount() {
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
let ignore = false;
fetch('/api/todos/count')
.then((r) => r.json())
.then((data) => {
if (!ignore) setCount(data.total);
})
.catch(console.error);
return () => {
ignore = true; // abort setState on unmount
};
}, []); // ⚠️ refetch only on mount
return <span>{count ?? '…'}</span>;
}
Line by line
- We hold local
state
for the count. - Guard
ignore
prevents a late promise from touching unmounted state. - No cache: navigate away and back, and we re‑download.
2. SWR in 10 Seconds
tsxCopyEdit// TodoCountSWR.tsx
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function TodoCountSWR() {
const { data, error } = useSWR('/api/todos/count', fetcher);
if (error) return <>Errored</>;
return <span>{data ? data.total : '…'}</span>;
}
Highlights
- The key
'/api/todos/count'
drives deduping; multiple components share one request. - SWR auto‑refetches on window focus and network reconnection, perfect for café‑hopper workflows.
3. React Query Power‑Up
tsxCopyEdit// TodoCountRQ.tsx
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const qc = new QueryClient();
export function App() {
return (
<QueryClientProvider client={qc}>
<TodoCountRQ />
</QueryClientProvider>
);
}
function TodoCountRQ() {
const { data, isPending } = useQuery({
queryKey: ['todoCount'],
queryFn: () => fetch('/api/todos/count').then((r) => r.json()),
staleTime: 1000 * 60, // 1 minute
});
return <span>{isPending ? '…' : data.total}</span>;
}
Why overhead is worth it
- Global cache survives route changes and mutation invalidations.
- Devtools panel shows query timelines—ideal for async code reviews across time zones.
Common Pitfalls I’ve Actually Seen
- Infinite Refetch Loops
Puttingdata
orsetState
in auseEffect
dependency array triggers recursive calls. Solution: keep the array minimal or adopt SWR/React Query. - Stale Mutation UI
WithuseEffect
, after a POST you often forget to re‑query. React Query’sinvalidateQueries(['todoCount'])
solves this in one line. - Race Conditions on Slow 3G
Two navigations fire overlapping fetches; the slower one overwrites newer data. SWR dedupes by key, React Query cancels outgoing fetches when a new one starts.
Remote‑Work Insight Box
In Panamá City last quarter, our stand‑up spanned four continents. React Query’s devtools time‑stamp each refetch, so reviewers in Tokyo could replay a bug I logged in Eastern Time. Screen recordings plus consistent query logs shaved days off debugging compared to opaque
useEffect
console prints.
Performance & Accessibility Checkpoints
- Bundle Budgets: SWR adds ~4 kB; React Query v5.83 adds ~14 kB plus devtools in dev only.GitHub Run
npm run analyze
to confirm. - Lighthouse: Use the Performance pane to verify fewer duplicate network calls when you switch tabs; aim for 0 repeated
/api/todos/count
after cache. - Prefetch‑On‑Hover: React Query’s
prefetchQuery
lets you warm data before route navigation—on Caribbean cellular this drops TTI by ~200 ms. - ARIA & Skeletons: Replace
'…'
placeholders with<span role="status" aria-live="polite">loading</span>
to keep screen readers informed. - Offline Fallbacks: React Query’s persistence plugin writes cache to
IndexedDB
; SWR pairs nicely withlocalStorage
. Test by ticking “Offline” in Chrome DevTools and reloading—content should still render.
Putting It All Together
When your component needs one‑off data and you crave explicit control, useEffect
remains fine—just wrap fetches in AbortController
and memoize where needed. For dashboards or list views where freshness beats exactness, SWR shines: minimal setup, zero providers, stellar for Next.js and server components. When state mutations, pagination, or offline mode enter the chat, React Query evolves from helpful to indispensable, giving you cache invalidation, optimistic updates, and devtools that bridge the Atlantic faster than any Zoom call.
I’ve shipped projects from Mexico City to Medellín using every combo above. The pattern I teach juniors is crawl → walk → run: start with useEffect
, graduate to SWR for automatic revalidate, and adopt React Query once your product-market fit demands sophisticated data orchestration.
Key Takeaways
- React
useEffect
= full control, but high repetition and easy to mis‑cache. - SWR = tiny, key‑based cache following stale‑while‑revalidate, great for read‑heavy UIs.
- React Query = enterprise‑ready state machine with mutations, retries, and offline persistence.
- Measure bundle size, duplicate requests, and Lighthouse scores before committing.
- Clear logs and devtools make async collaboration across time zones dramatically easier.
Questions or war stories from your own remote trenches? Drop a comment below—I read them between flights across Latin America.