How to Use the Loading Component and Manage Loading States in refine-shadcn

Use the LoadingIcon or Loader components from the theme package and wire them to the Boolean loading flags (e.g., isLoading, formLoading) exposed by refine hooks like useTable, useOne, and useForm.

The ferdiunal/refine-shadcn repository provides lightweight, decoupled loading primitives designed to integrate seamlessly with refine data hooks. These components allow you to manage loading states in refine-shadcn applications without coupling your UI logic to the underlying data-fetching implementation.

Loading Primitives Available in refine-shadcn

The theme package exposes two distinct loading components that serve different visual purposes. Both accept a className prop for Tailwind CSS styling.

LoadingIcon for Inline Spinners

Located in packages/theme/src/ui/loading.tsx, the LoadingIcon component renders a spinning Lucide RefreshCwIcon as an inline SVG. This primitive is ideal for button states, inline indicators, or compact UI elements requiring immediate feedback. Pass Tailwind classes like h-4 w-4 animate-spin to control dimensions and animation speed.

Loader for Block-Level Placeholders

Found in packages/theme/src/components/loader.tsx, the Loader component displays a three-dot animated SVG suitable for larger placeholders. Use this inside table bodies, empty states, or full-screen overlays where a prominent loading indicator improves perceived performance.

Wiring Loading States to Data Hooks

In refine-shadcn, loading UI is driven by Boolean flags exposed through refine core hooks. These flags turn true when a query initiates and revert to false upon completion, regardless of success or error states.

Table Data Loading

When using useTable, access the loading state via table.refineCore.tableQuery.isLoading. In packages/theme/src/table/index.tsx, the implementation checks this flag to render a full-width table row containing the Loader component while data fetches.

Form Submission States

Form components receive loading status through props.refineCore.formLoading. As shown in packages/theme/src/components/form.tsx, this flag disables the cancel button and triggers the spinner inside the SaveButton component automatically when passed the loading prop.

For fetching associated data, useOne exposes an isLoading flag. The example in templates/vite-react/src/pages/posts/Show.tsx demonstrates conditionally rendering "Loading…" text while fetching category details for a post.

Implementation Examples

Inline Button Loading State

The following pattern from packages/theme/src/components/confirm.tsx shows how to swap an action icon for a LoadingIcon during async operations:

import { LoadingIcon } from "@/ui/loading";
import { CheckIcon } from "lucide-react";
import {
  AlertDialog,
  AlertDialogAction,
} from "@/components/alert-dialog";

export const ConfirmDialog = ({
  loading = false,
  onConfirm,
}) => {
  const OkIcon = loading ? (
    <LoadingIcon className="mr-2" />
  ) : (
    <CheckIcon className="mr-2 h-4 w-4" />
  );

  return (
    <AlertDialogAction onClick={onConfirm} disabled={loading}>
      {OkIcon}
      Confirm
    </AlertDialogAction>
  );
};

Table Body Loading Placeholder

This implementation from packages/theme/src/table/index.tsx demonstrates handling empty table states:

import Loader from "@/components/loader";
import { useTable } from "@refinedev/react-table";
import {
  TableBody,
  TableRow,
  TableCell,
} from "@/components/table";

export const Table = (props) => {
  const { table } = useTable({ ...props });

  return (
    <TableBody>
      {table.refineCore.tableQuery.isLoading ? (
        <TableRow>
          <TableCell colSpan={columns.length} className="h-24 text-center">
            <Loader className="h-4 text-primary" />
          </TableCell>
        </TableRow>
      ) : (
        table.getRowModel().rows.map((row) => (
          // render row
        ))
      )}
    </TableBody>
  );
};

Form Submit Button Spinner

As implemented in packages/theme/src/components/form.tsx, wire the formLoading flag to your submit button:

import { SaveButton } from "@/ui/buttons";
import { Button } from "@/components/button";
import { CardFooter } from "@/components/card";
import { Form as FormUI } from "@/components/form";

export const Form = (props) => {
  return (
    <FormUI {...props}>
      {/* form fields */}
      <CardFooter className="flex justify-end gap-x-4">
        <Button
          type="button"
          onClick={onBack}
          disabled={props.refineCore.formLoading}
          variant="outline"
        >
          Cancel
        </Button>
        <SaveButton
          type="submit"
          loading={props.refineCore.formLoading}
        />
      </CardFooter>
    </FormUI>
  );
};

Dependent Data Fetching

From templates/vite-react/src/pages/posts/Show.tsx, handle dependent data loading with useOne:

import { useOne, useShow } from "@refinedev/core";

export const PostShow = () => {
  const { query: { data } } = useShow<IPost>();
  const record = data?.data;

  const { data: categoryData, isLoading: categoryIsLoading } = useOne<ICategory>({
    resource: "categories",
    id: record?.category.id ?? "",
    queryOptions: { enabled: !!record },
  });

  return (
    <ShowPage>
      <ShowPage.Row
        title="Category"
        children={categoryIsLoading ? "Loading…" : categoryData?.data.title ?? ""}
      />
    </ShowPage>
  );
};

Global Suspense Boundaries

For application-wide loading states in Next.js, wrap your routes with React Suspense as shown in templates/nextjs/app/layout.tsx:

import { Suspense } from "react";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Suspense fallback={<div>Loading…</div>}>
          {children}
        </Suspense>
      </body>
    </html>
  );
}

Summary

  • Import primitives from packages/theme/src/ui/loading.tsx (LoadingIcon) or packages/theme/src/components/loader.tsx (Loader) depending on your visual needs.
  • Consume loading flags from refine hooks: useTable provides table.refineCore.tableQuery.isLoading, useOne provides isLoading, and forms receive props.refineCore.formLoading.
  • Apply Tailwind classes via the className prop to size and color your spinners consistently with your design system.
  • Maintain decoupled architecture by keeping loading UI in the theme package and data logic in your hooks, enabling easy spinner swaps without touching data-fetching code.

Frequently Asked Questions

How do I change the spinner color in refine-shadcn loading components?

Both LoadingIcon and Loader accept a className prop. Pass standard Tailwind color utilities like text-primary or text-blue-500 to control the SVG stroke color. For example: <Loader className="h-4 text-primary" /> applies the primary color to the three-dot animation.

Can I use these loading components outside of refine data hooks?

Yes. While designed to integrate with refine's isLoading flags, these are pure presentational components. You can use them with any Boolean state variable, React Query's isPending, or manual useState toggles for custom async operations.

Where should I place a full-page loading fallback in a Next.js app with refine-shadcn?

Wrap your application in a React Suspense boundary within your root layout, as shown in templates/nextjs/app/layout.tsx. Provide a static fallback like <div>Loading…</div> or the Loader component for server-side rendered routes and lazy-loaded chunks.

Why does my SaveButton not show a spinner even when I set loading={true}?

Ensure you are importing the SaveButton from the theme package (@/ui/buttons) as implemented in packages/theme/src/components/form.tsx. The button must internally support the loading prop and render a LoadingIcon when true. If using a standard shadcn button, you will need to manually compose it with LoadingIcon following the pattern in packages/theme/src/components/confirm.tsx.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →