Handle Loading, Empty, and Error States in React

Flags React views that render only the success case, leaving blank screens during loading, silent failures on errors, and no message for empty data.

What it flags

Flaggedcomponents/InvoiceTable.tsx
import { useQuery } from '@tanstack/react-query';
import { fetchInvoices } from './api';
import { InvoiceRow } from './InvoiceRow';

export function InvoiceTable({ customerId }: { customerId: string }) {
  const { data } = useQuery({
    queryKey: ['invoices', customerId],
    queryFn: () => fetchInvoices(customerId),
  });

  // Loading, failed, and zero-invoice cases all render the same empty table
  return (
    <table>
      <tbody>
        {data?.map((invoice) => (
          <InvoiceRow key={invoice.id} invoice={invoice} />
        ))}
      </tbody>
    </table>
  );
}
Passescomponents/InvoiceTable.tsx
import { useQuery } from '@tanstack/react-query';
import { EmptyState, ErrorState, TableSkeleton } from '@/components/states';
import { fetchInvoices } from './api';
import { InvoiceRow } from './InvoiceRow';

export function InvoiceTable({ customerId }: { customerId: string }) {
  const { data, isPending, isError, refetch } = useQuery({
    queryKey: ['invoices', customerId],
    queryFn: () => fetchInvoices(customerId),
  });

  if (isPending) return <TableSkeleton rows={5} />;
  if (isError) return <ErrorState title="Could not load invoices" onRetry={() => refetch()} />;
  if (data.length === 0) return <EmptyState title="No invoices yet" />;

  return (
    <table>
      <tbody>
        {data.map((invoice) => (
          <InvoiceRow key={invoice.id} invoice={invoice} />
        ))}
      </tbody>
    </table>
  );
}

Why it matters

A component written only for the success case renders nothing while data loads, shows a bare table when a query returns no rows, and stays blank or shows stale data when the request fails. Users cannot tell a slow request from a broken page, so they reload, submit forms twice, or contact support. These gaps pass review because local development has fast responses and seeded data, so the missing states never appear on screen.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Handle Loading, Empty, and Error States in React

Applies to `**/*.tsx`, `**/*.jsx`.

Review React components that render fetched or async data for missing loading, empty, and error states.

Flag changes that:
- Render data from useQuery, useSWR, or a fetch in an effect without a loading state, leaving a blank area or a layout jump.
- Ignore the error from a data hook (error, isError) or a rejected request, so a failure renders as an empty or stale view with no message or retry.
- Map over a list with no empty-state branch when the list can be empty, such as search results, filtered tables, or a new account's records.
- Show the empty state while the first request is still pending, such as "No results" before any data has loaded.
- Suspend with use() or a suspense-enabled query without a Suspense fallback and an error boundary above it.
- Submit a form or run a mutation without a pending state (isPending from useTransition or useActionState, pending from useFormStatus) or a visible failure message, allowing double submits and silent failures.

Do not flag:
- Components that receive resolved data as props when a parent or route already handles loading and errors.
- Routes where framework files cover these states, such as Next.js loading.tsx and error.tsx.
- Background refetches that intentionally keep showing the previous data.

Checks files matching **/*.tsx, **/*.jsx.

More React agents

See all React code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code