ReactA correct hand-rolled fetch, and the same thing with a tiny cache
import { useState, useEffect, useCallback } from "react";
const DB = {
1: { id: 1, name: "Ada Lovelace", role: "Mathematician" },
2: { id: 2, name: "Grace Hopper", role: "Rear Admiral" },
3: { id: 3, name: "Alan Turing", role: "Logician" },
};
let requestCount = 0;
function fakeFetch(id, signal) {
requestCount++;
return new Promise((resolve, reject) => {
const t = setTimeout(() => {
if (!DB[id]) reject(new Error("404 not found"));
else resolve(DB[id]);
}, 500);
signal?.addEventListener("abort", () => {
clearTimeout(t);
const e = new Error("aborted"); e.name = "AbortError"; reject(e);
});
});
}
function useUserByHand(id) {
const [state, setState] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
const controller = new AbortController();
setState({ status: "loading", data: null, error: null });
fakeFetch(id, controller.signal)
.then((data) => setState({ status: "success", data, error: null }))
.catch((e) => {
if (e.name === "AbortError") return;
setState({ status: "error", data: null, error: e.message });
});
return () => controller.abort();
}, [id]);
return state;
}
const cache = new Map();
function useUserCached(id) {
const [, force] = useState(0);
const key = String(id);
useEffect(() => {
if (cache.has(key)) return;
cache.set(key, { status: "loading" });
fakeFetch(id)
.then((data) => cache.set(key, { status: "success", data }))
.catch((e) => cache.set(key, { status: "error", error: e.message }))
.finally(() => force((n) => n + 1));
}, [key, id]);
return cache.get(key) ?? { status: "loading" };
}
function Card({ title, state }) {
return (
<div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, flex: 1 }}>
<div style={{ fontSize: 12, color: "#666" }}>{title}</div>
{state.status === "loading" && <p>loadingβ¦</p>}
{state.status === "error" && <p style={{ color: "crimson" }}>{state.error}</p>}
{state.status === "success" && (
<p><strong>{state.data.name}</strong><br /><span style={{ fontSize: 13 }}>{state.data.role}</span></p>
)}
</div>
);
}
export default function App() {
const [id, setId] = useState(1);
const byHand = useUserByHand(id);
const cached = useUserCached(id);
return (
<div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
<div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
<Card title="hand-rolled effect" state={byHand} />
<Card title="with a tiny cache" state={cached} />
</div>
<p>
{[1, 2, 3, 99].map((n) => (
<button key={n} onClick={() => setId(n)} style={{ marginRight: 6 }}>
user {n}{n === 99 ? " (404)" : ""}
</button>
))}
</p>
<p style={{ fontSize: 14 }}>network requests made: <strong>{requestCount}</strong></p>
<p style={{ color: "#666", fontSize: 13 }}>
Switch between users you have already loaded. The hand-rolled column
refetches every time; the cached one does not β that difference is most
of what a query library buys you, before staleness and refetching.
</p>
</div>
);
}