DEV Community

Jalaj Bankar
Jalaj Bankar

Posted on

SWR, Webhooks, useState Deep Dive, and a Few Things Worth Cementing

A session that revisits some fundamentals with fresh eyes and adds a couple of genuinely useful concepts on top. The useState explanation here is the clearest it's been — worth reading carefully.


React SWR — Show Old Data, Fetch New Data, Nobody Waits
SWR stands for Stale-While-Revalidate — a caching strategy that shows you whatever data it already has while quietly fetching fresh data in the background.
Here's what makes it worth knowing:
You give it a key (usually the URL) and a fetcher function (your actual network call). It handles the rest — caching, revalidating, deduplication.
If ten components all need the same data, SWR doesn't make ten requests. It makes one and shares the result across all of them.
It also re-fetches automatically when you re-focus the window — so if a user switches tabs and comes back, they always see fresh data without you writing any logic for it.
const { data, error, isLoading } = useSWR('/api/user', fetcher);
Three things back — your data, any error, and a loading state. Clean, no boilerplate, handles edge cases you'd otherwise forget to write.


Webhooks — Stop Asking, Just Listen
Polling is when your app keeps asking the server every few seconds — "is it done? is it done? is it done?" — like a kid in the back seat. Works, but wasteful.
Webhooks flip that relationship. Instead of you asking, the server tells you when something happens. Once, immediately, no repeated requests.
Real example — user pays via GPay → GPay sends a notification (HTTP POST) directly to your server the moment the payment is confirmed. Your server wasn't sitting there asking. GPay knocked on the door when it had news.
Better for performance, better for real-time accuracy, better for your server's sanity.


useState — The Clearest Explanation Yet
Let's go through this properly because it trips people up more than almost anything else in React.
const [data, setData] = useState(0);
useState(0) — that 0 is the initial value. What data holds on the very first render before anything happens.
data — a read-only snapshot of the current state. You cannot change it directly. Ever. data = 5 does nothing useful in React.
setData — the only door into changing state. It receives new data, tells React "store this as the new value," triggers a re-render, and on that next render data becomes the new stored value.
So the question — "if data already has data, why do we need setData?" — here's the answer:
Because React needs to know something changed. If you mutate data directly, React has no idea and won't re-render. setData is the signal. It's not just storage — it's the trigger that kicks off the whole update cycle.
setData receives → React stores → data updates → component re-renders with new value. That's the full loop every single time.


Why Callbacks Sit Between Hooks and Their Logic
onChange(() => { setCount(count + 1) })
useEffect(() => { setAPI(data) }, [])
The callback function is there to say — "wait for me to run first, then you go." The hook (useEffect, onChange, setTimeout) needs to execute before the inner logic fires. The callback is what creates that waiting relationship. Without it, the setter functions would have nothing to wait for — they'd either run immediately or not at all.


setTimeout — Wait Before You Even Start
This one is worth being precise about. The delay in setTimeout is not "run this, but slowly." It's "don't even begin until this much time has passed."
setTimeout(() => { doSomething(); }, 3000);
Three seconds of silence. Then execution begins. Nothing happens in the meantime — no partial work, no progress. Just waiting. Then go.


Always Use try / catch / finally With Async Code
This is a best practice worth making a habit immediately:
async function fetchData() {
try {
const res = await fetch(url);
const data = await res.json();
setData(data);
} catch (err) {
console.error("Something went wrong:", err);
} finally {
setLoading(false);
}
}

try — the optimistic path. Things go well here.
*catch *— when they don't. Catches any error thrown inside try.
*finally *— runs no matter what. Success or failure, the loader disappears. This is non-negotiable with async functions — if you skip finally and an error gets thrown, setLoading(false) inside try never runs and your spinner spins forever.
Make it a reflex. Async function — try, catch, finally. Every time.

Top comments (0)