Handle loading, empty and error states
Drive a data block or a form block from your own fetching with the state prop, and wrap your own views in StateView.
On this page
Show a data block while it loads
Lists, boards, dashboards, calendars and chats take state, onRetry, emptyState and errorState. useResource turns a fetch into the state to pass:
import { Mail1 } from "@/components/ballmac/blocks/mail-1/mail-1"
import { useResource } from "@/lib/ballmac/use-resource"
export function Inbox() {
const mail = useResource((signal) => fetch("/api/mail", { signal }).then((r) => r.json()))
return (
<Mail1
state={mail.state} // "loading" | "ready" | "empty" | "error"
messages={mail.data}
onRetry={mail.retry}
emptyState={{ title: "Inbox zero", description: "Nothing new. Enjoy it." }}
errorState={{ description: "Mail is taking a break. Your messages are safe." }}
/>
)
}useResource aborts a request that is replaced or unmounted, never lets a slow old answer overwrite a newer one, and goes round again on retry().
Use TanStack Query or SWR instead
If you already use TanStack Query or SWR, map their result:
import { stateOf } from "@/lib/ballmac/use-resource"
const query = useQuery({ queryKey: ["mail"], queryFn: fetchMail })
<Mail1 state={stateOf(query)} messages={query.data} onRetry={query.refetch} />Drive a form block
Sign-in, sign-up, contact, newsletter, settings and the rest already run their own submit flow. state lets you drive it from your own async code, or show one stage on its own: submitting locks the fields, success shows the confirmation, error shows the failure. errorState.description sets the message.
const [phase, setPhase] = useState<"ready" | "submitting" | "success" | "error">("ready")
<Signup1
state={phase}
errorState={{ description: "That email is already registered." }}
onSubmit={async (values) => {
setPhase("submitting")
try { await createAccount(values); setPhase("success") } catch { setPhase("error") }
}}
/>Leave state out and the form decides by itself, as before. Several forms also take defaultValues or defaultEmail to prefill.
Wrap your own view
StateView is the part the blocks are built on. Wrap anything that loads:
<StateView
state={invoices.state}
loading={<InvoiceSkeleton />} // the same shape as the real list
empty={{ title: "No invoices", action: <Button>Create one</Button> }}
error={{ title: "Invoices are unavailable", details: String(invoices.error) }}
onRetry={invoices.retry}
>
<InvoiceList items={invoices.data} />
</StateView>The props of StateView and useResource are in the building blocks reference.