Loading and empty states
A grid is not always full of rows. Three props cover the rest: loading, error and emptyState.
The grid picks the right presentation for each from whether it has rows.
| Kwame Smith | Invited | Sales | $48.00 |
| Meera Nair | Paused | Growth | $1,073.00 |
| Aarav Rossi | Active | Growth | $36.00 |
| Meera Prabhakaran | Active | Research | $0.00 |
| Meera Garcia | Churned | Design | $0.00 |
| Meera Dubois | Active | Finance | $1,189.00 |
| Mateo Iyer | Churned | Design | $0.00 |
| Freya Kim | Active | Support | $609.00 |
| Kwame Bose | Active | Finance | $0.00 |
| Kwame Nguyen | Churned | Research | $0.00 |
0 of 30 row(s) selected.
Page 1 of 3
import * as React from "react";
import { Button } from "@/components/ui/button";import { ButtonGroup } from "@/components/ui/button-group";import { DataGrid, createDataGridColumns } from "@/components/ui/data-grid";
import { makePeople, STATUS_OPTIONS, type Person } from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.text("name", { header: "Name" }), col.enum("status", { header: "Status", options: STATUS_OPTIONS }), col.text("team", { header: "Team" }), col.currency("mrr", { header: "MRR", currency: "USD" }),]);
const rows = makePeople(30, 41);
type State = "ready" | "refetching" | "loading" | "error" | "empty";
const STATES: ReadonlyArray<[State, string]> = [ ["ready", "Loaded"], ["refetching", "Refetching"], ["loading", "First load"], ["error", "Error"], ["empty", "Empty"],];
/** One grid, five states. Refetching keeps the rows under a progress bar; a first load has none yet. */export default function DataGridStates() { const [state, setState] = React.useState<State>("ready"); const data = state === "loading" || state === "empty" ? [] : rows; return ( <div className="flex flex-col gap-3"> <ButtonGroup aria-label="Grid state"> {STATES.map(([value, label]) => ( <Button key={value} size="sm" variant={state === value ? "default" : "outline"} onClick={() => setState(value)} > {label} </Button> ))} </ButtonGroup> <DataGrid key="states" data={data} columns={columns} getRowId={(row) => row.id} loading={state === "loading" || state === "refetching"} error={state === "error" ? "The request timed out after 30 seconds." : null} onRetry={() => setState("ready")} height={360} /> </div> );}Use it
Section titled “Use it”Wire the three props to your data source. With TanStack Query:
<DataGrid data={query.data?.rows ?? []} columns={columns} loading={query.isFetching} error={query.error ? query.error.message : null} onRetry={() => query.refetch()} height={480}/>What shows, in order of precedence:
| Condition | What the grid shows |
|---|---|
error set |
The error state over everything, rows or not: a warning icon, Something went wrong., your error as the description, and a Retry button when onRetry is given. |
loading and no rows |
A spinner with Loading… below the header; the overlay does not take pointer events. |
loading with rows |
A thin animated bar along the top edge (role="progressbar"). The rows stay in place and usable, which is what you want on a refetch. |
| No rows, a filter or search active | No rows match the current filters. with a Clear button that resets every column filter and the search. |
| No rows | An inbox icon and No records found. |
While loading is true the grid also carries aria-busy.
Your own empty state
Section titled “Your own empty state”emptyState replaces the default, for both the empty and the filtered-empty case:
<DataGrid data={customers} columns={columns} emptyState={ <Empty className="border-0"> <EmptyHeader> <EmptyMedia variant="icon"><UsersIcon /></EmptyMedia> <EmptyTitle>No customers yet</EmptyTitle> <EmptyDescription>Import a CSV or add one by hand.</EmptyDescription> </EmptyHeader> <Button size="sm">Add customer</Button> </Empty> }/>It renders inside the grid, so a component you pass can call useDataGridContext() to know whether
filters are active and offer its own Clear (table.resetColumnFilters(); table.resetGlobalFilter(true)).
How it behaves
Section titled “How it behaves”- The overlay sits below the header. The header row stays visible and usable in every state, so a filter that emptied the grid can be cleared from the header that set it.
- Give it room. The states centre themselves in the scroll area. With no
heightan empty or loading grid grows to a minimum of 12rem so the message fits under the header; setheightorfillHeightfor a steadier frame, as the demo does. - Refetch, don’t flash. Keep the previous rows while the next page loads
(
placeholderData: keepPreviousDatain TanStack Query) andloading={isFetching}shows the bar rather than swapping the rows for a spinner. The server-side example does exactly this. - Error wins. An error is shown even when rows are present, over them. Clear
errorwhen the next request succeeds. - Strings.
loading,empty,emptyFiltered,error,retryandclearFiltersinlabelschange the wording; see Internationalisation.
Options
Section titled “Options”| Option | Type | Default | What it does |
|---|---|---|---|
loading |
boolean |
false |
The bar with rows, the spinner without. |
error |
React.ReactNode |
null |
Any node; a string is the common case. Truthy shows the error state. |
onRetry |
() => void |
Adds the Retry button to the error state. | |
emptyState |
React.ReactNode |
Replaces both empty states. | |
height / fillHeight |
Not state props, but the states need the room. |
The same four props exist on DataGridRoot for composed layouts.
See also
Section titled “See also”- Server-side data:
loading,errorandonRetryfrom a query. - Filtering: what Clear resets.
- Internationalisation.