Loading async data
Fetch a customer. Spinner while it loads. Error if it fails. Card when it arrives. Never let a slow response for an old id clobber a new one.
Trivial feature. In React it’s the #1 place people ship a race condition.
React — what you actually write
Section titled “React — what you actually write”import { useState, useEffect } from "react"import { fetchCustomer, type Customer } from "./api"
export function CustomerCard({ id }: { id: string }) { const [data, setData] = useState<Customer | null>(null) const [isPending, setIsPending] = useState(true) const [error, setError] = useState<unknown>(null)
useEffect(() => { const controller = new AbortController() setIsPending(true) setError(null) fetchCustomer(id, controller.signal) .then((c) => { if (controller.signal.aborted) return setData(c); setIsPending(false) }) .catch((err) => { if (controller.signal.aborted) return setError(err); setIsPending(false) }) return () => controller.abort() }, [id])
if (isPending) return <p><em>loading…</em></p> if (error) return <p className="error">error: {String(error)}</p> return ( <div className="card"> <p><strong>{data!.name}</strong> — {data!.city}</p> </div> )}~38 lines. Five are the feature. The rest is ceremony you repeat in every async component — and get subtly wrong under pressure.
Reactra — what you describe
Section titled “Reactra — what you describe”import { fetchCustomer } from "./api"
export component CustomerCard { param id: string
resource customer(id) signal => fetchCustomer(id, signal)
view { await(customer) { <div className="card"> <p><strong>{customer.data?.name}</strong> — {customer.data?.city}</p> </div> } pending { <p><em>loading…</em></p> } error(e) { <p className="error">error: {String(e)}</p> } }}import { fetchCustomer } from "./api"import { Suspense, use, useEffect } from "react"import { ErrorBoundary, useResource } from "@reactra/resource"import { useRoute } from "@reactra/router"
export const CustomerCard = () => { const __route = useRoute() const id = __route.params.id const customer = useResource(id, (id, { signal }) => (fetchCustomer(id, signal)), { resourceName: "customer" }) useEffect(() => { const h = globalThis.__REACTRA_TEST__; if (h) h.update("CustomerCard", { customer }) }) return (<> <ErrorBoundary fallback={(e) => <><p className="error">error: {String(e)}</p></>}><Suspense fallback={<><p><em>loading…</em></p></>}>{(() => { const __AwaitInner = () => { use(customer.promise); return <><div className="card"> <p><strong>{customer.data?.name}</strong> — {customer.data?.city}</p> </div></> }; __AwaitInner.displayName = "Await(customer)"; return <__AwaitInner /> })()}</Suspense></ErrorBoundary> </>)}export default CustomerCard~14 lines. Every one is the feature.
What disappeared
Section titled “What disappeared”- 3
useStates for one value → 1resource - The race (slow response for an old id overwriting the new) → handled by
signal - The dependency array you keep getting wrong → it’s the
(id)in the signature if pending … if error … return data!→ the structuralawait / pending / error- Cleanup, abort plumbing, the
data!assertion → gone
Same screen. None of the machinery.
It’s not a new runtime
Section titled “It’s not a new runtime”Reactra is a compiler. That component compiles at build time to ordinary
React 19 — React.use, <Suspense>, an error boundary — the exact code you’d
hand-write. Open the output; it reads like React, because it is.
You write what the screen is. The compiler writes the part where the bugs live.