Skip to content

Comments

Add loading skeleton for DataTable and localize empty state messages#118

Draft
dreglad wants to merge 2 commits intomarmelab:mainfrom
dreglad:datatable-skeletons
Draft

Add loading skeleton for DataTable and localize empty state messages#118
dreglad wants to merge 2 commits intomarmelab:mainfrom
dreglad:datatable-skeletons

Conversation

@dreglad
Copy link
Contributor

@dreglad dreglad commented Jan 28, 2026

Summary

This PR introduces a visible loading skeleton for the DataTable component and localizes internal empty state labels that were previously hardcoded.

Motivation

Currently, the DataTable displays a blank screen while fetching data from the API. While the library supports a loading state, it seems primarily intended for component lazy-loading, which doesn't account for the wait times typical of remote data fetching.
By adding a pulsing skeleton, we significantly improve the perceived performance and provide immediate visual feedback, ensuring the UI feels responsive during network requests.

Changes

  • Loading Skeleton: Integrated a DataTableSkeleton that renders automatically during data fetching.
  • Dynamic Dimensions: The skeleton honors the perPage setting from ListContext and matches the defined column count for a seamless UI transition.
  • i18n Localization: Updated DataTableEmpty and AutocompleteInput to use standard React Admin translation keys (ra.page.no_results and ra.page.no_matches), removing hardcoded labels.
  • Testing: Added unit tests data-table.spec.tsx to verify the skeleton behavior and correct row rendering.

Design Decision

I considered adding an opt-in prop for the skeleton to avoid changing existing UI expectations. However, I decided to prioritize API simplicity and integrated it as the default loading state. I believe this change is safe and aligns with modern design standards, though I'm open to making it configurable if the maintainers prefer.

image

Copy link
Member

@fzaninotto fzaninotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

}) => {
const { perPage } = useListContext();
const effectiveRowsCount = rowsCount ?? perPage ?? 5;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skeleton should only display if the data takes more than 1 second to load. Otherwise I find skeletons distracting (they add an unnecessary blink). Use the useTimeout hook for that, as in https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/list/datatable/DataTableLoading.tsx.

columnsCount = 5,
rowsCount,
}: {
columnsCount?: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

columnsCount should be required

rowsCount?: number;
}) => {
const { perPage } = useListContext();
const effectiveRowsCount = rowsCount ?? perPage ?? 5;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using perPage will reserve a too large space IMO. Use rowsCount and default it to 5.

return (
<div className="rounded-md border animate-pulse">
<Table>
<TableHeader>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to account for bulk actions, too

</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: effectiveRowsCount }).map((_, i) => (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you dim the row more and more as the number of rows increases?

@@ -286,9 +288,12 @@ const isPromise = (value: any): value is Promise<any> =>
value && typeof value.then === "function";

const DataTableEmpty = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add stories (in src/stories/data-table.stories.tsx) so that we can test this in isolation. Stories should should the various cases (fast loading, short loading, variable number of children, with and without bulk actions, etc).


describe("DataTable", () => {
it("should render a skeleton when isPending is true", async () => {
render(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render the component from the stories instead (see below)

it("should render a skeleton when isPending is true", async () => {
render(
<ListContextProvider value={{ perPage: 25 } as ListControllerSuccessResult}>
<DataTable isPending={true}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should also test in the context of a data provider that actually loads data after a while

<CommandList>
<CommandEmpty>No matching item found.</CommandEmpty>
<CommandEmpty>
{translate("ra.page.no_matches", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make the key ra.input.autocomplete.no_matches instead

<Alert>
<AlertDescription>No results found.</AlertDescription>
<AlertDescription>
{translate("ra.page.no_results", { _: "No results found." })}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fzaninotto
Copy link
Member

Hi, any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants