# State view

> One layout for a region's loading, empty and error states, and an error boundary that reports errors, recovers and never shows error details to users.

Source: https://platform.azmara.io/docs/components/state-view

Group: Feedback · Status: stable · Added in @azmr/ui 0.12.0

## Usage

```tsx
import { StateView } from "@azmr/ui/state-view"; // Server Component
import { ErrorBoundary } from "@azmr/ui/error-boundary"; // "use client"
```

```tsx
import { Button } from "@azmr/ui/button";
import { StateView } from "@azmr/ui/state-view";

export default function StateViewStates() {
  return (
    <div className="grid gap-4 sm:grid-cols-3">
      <div className="rounded-md border">
        <StateView state="loading" title="Loading invoices…" delay={0} titleAs="h3" />
      </div>
      <div className="rounded-md border">
        <StateView
          state="empty"
          title="No invoices yet"
          description="Invoices you create appear here."
          action={<Button size="sm">Create invoice</Button>}
          titleAs="h3"
        />
      </div>
      <div className="rounded-md border">
        <StateView
          state="error"
          title="Invoices couldn't load"
          description="Check your connection and try again."
          action={
            <Button size="sm" variant="outline">
              Try again
            </Button>
          }
          titleAs="h3"
        />
      </div>
    </div>
  );
}
```

Every list, table and page has the same three extra states: still loading, nothing to show, and failed. `StateView` gives them one layout (icon, title, a sentence, an action), so they look and read the same everywhere. It is a **Server Component**, so it works in `loading.tsx`, `error.tsx`, Suspense fallbacks and empty tables.

| `state`   | Announced as                                                                                             | Default title        | Default icon                     |
| --------- | -------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------- |
| `loading` | a polite status (it is [`Loading`](https://platform.azmara.io/docs/components/loading.md)), after 200 ms | Loading…             | the spinner, or your `logo`      |
| `empty`   | plain content                                                                                            | Nothing here yet     | inbox                            |
| `error`   | an alert                                                                                                 | Something went wrong | alert, in the destructive colour |

`variant="page"` fills the content area and `section` (default) fills its container. `titleAs` picks the heading level (`h2` by default) so it fits the page's outline; use `h1` when the state is the whole page, such as a 404 page. `icon={null}` drops the icon, and `action` takes any buttons or links.

## Page loader

A full-page loader with your logo is `<StateView state="loading" variant="page" logo={…} />`, the same as `<Loading variant="page" logo={…} />`. The ring spins around the logo while the logo stays still.

## Error boundary

```tsx
"use client";

import { Button } from "@azmr/ui/button";
import { ErrorBoundary } from "@azmr/ui/error-boundary";
import { useState } from "react";

function Report({ broken }: { broken: boolean }) {
  if (broken) throw new Error("Report query failed for customer #4123");
  return <p className="p-6 text-sm">Sales this month: $12,480.00</p>;
}

export default function ErrorBoundaryDemo() {
  const [broken, setBroken] = useState(false);
  const [reported, setReported] = useState(0);

  return (
    <div className="flex max-w-md flex-col gap-4">
      <Button variant="destructive" className="self-start" onClick={() => setBroken(true)}>
        Break the report
      </Button>
      <div className="rounded-md border">
        <ErrorBoundary
          // Your reporting hook: Sentry.captureException, posthog.captureException...
          onError={() => setReported((n) => n + 1)}
          // Try again re-renders the children, so fix the cause first.
          onReset={() => setBroken(false)}
          fallbackProps={{ titleAs: "h3", title: "The report couldn't load" }}
        >
          <Report broken={broken} />
        </ErrorBoundary>
      </div>
      <p className="text-sm text-muted-foreground">Errors reported: {reported}</p>
    </div>
  );
}
```

`ErrorBoundary` catches errors thrown while its children render and shows a fallback with **Try again**, instead of a blank page.

- **Reporting:** `onError(error, info)` is called once per caught error, with the component stack. Send it to Sentry, PostHog or your own logger. The boundary has no reporting service built in.
- **Recovery:** Try again calls `reset`, which renders the children again (so fix the cause first, in `onReset`). `resetKeys={[id]}` resets by itself when a route or record changes.
- **No error details by default:** the default fallback says "Something went wrong" and never shows `error.message`, which can carry an email address, a record id or a server path. Customise the text with `fallbackProps`, or pass your own `fallback`, which receives `{ error, reset }`.
- **What it does not catch:** errors in event handlers, timers and promises are not render errors. Catch them where they happen and show a toast or set state.

## In Next.js

Next.js has its own error boundary, the `error.tsx` file. Use `StateView` as its UI. Since Next.js 16.2 it receives `retry`, which fetches the segment again; `reset` only re-renders it.

```tsx
"use client";

import { Button } from "@azmr/ui/button";
import { StateView } from "@azmr/ui/state-view";
import { useEffect } from "react";

export default function InvoicesError({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
  useEffect(() => {
    reportError(error); // your reporting hook; error.digest matches the server logs
  }, [error]);

  return (
    <StateView
      state="error"
      variant="page"
      title="Invoices couldn't load"
      description="Try again, or come back later."
      action={<Button onClick={() => retry()}>Try again</Button>}
    />
  );
}
```

```tsx
import { StateView } from "@azmr/ui/state-view";

export default function InvoicesLoading() {
  return <StateView state="loading" variant="page" title="Loading invoices…" />;
}
```

Use `ErrorBoundary` for a part of a page (a chart, a widget) that should fail on its own. In Next.js pass `rethrow={unstable_rethrow}` from `next/navigation`: `redirect()` and `notFound()` work by throwing, and `rethrow` lets them through to Next.js instead of showing them as errors. They are not passed to `onError` either.

```tsx
"use client";

import { ErrorBoundary } from "@azmr/ui/error-boundary";
import { unstable_rethrow } from "next/navigation";

<ErrorBoundary rethrow={unstable_rethrow} onError={reportError} fallbackProps={{ title: "This chart couldn't load" }}>
  <RevenueChart />
</ErrorBoundary>
```

## Props

### StateView

| Prop          | Type                                               | Default | Required | Description                                                                                                                                                                          |
| ------------- | -------------------------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `className`   | `string`                                           |         | No       |                                                                                                                                                                                      |
| `logo`        | `ReactNode`                                        |         | No       | A logo or image shown in the middle of the spinner (`<img>`, `<svg>`, an `Avatar`). It fills a circle inside the ring. The logo stays upright and readable; only the ring moves.     |
| `state`       | `StateViewState`                                   |         | Yes      | Which state to show.                                                                                                                                                                 |
| `title`       | `string`                                           |         | No       | The headline. Defaults: "Loading…", "Nothing here yet", "Something went wrong". Generic on purpose: never put a raw error message here, it can carry personal or internal details.   |
| `description` | `ReactNode`                                        |         | No       | A sentence under the title: what happened, or what to do next.                                                                                                                       |
| `icon`        | `ReactNode`                                        |         | No       | Replaces the default icon (inbox for empty, alert for error). `null` shows none. Ignored while loading.                                                                              |
| `action`      | `ReactNode`                                        |         | No       | A button or link: "Try again", "Create your first invoice". Not shown while loading.                                                                                                 |
| `variant`     | `"section" \| "page" \| undefined`                 |         | No       | `section` (default) fills its container; `page` fills the content area.                                                                                                              |
| `titleAs`     | `"h1" \| "h2" \| "h3" \| "h4" \| "p" \| undefined` |         | No       | The title's element, to fit the page's heading outline. Default `h2`. Use `h1` when the state is the whole page (a 404 page, a root error page) and nothing else on it is a heading. |
| `delay`       | `number`                                           |         | No       | Loading only: milliseconds before it fades in. Default 200.                                                                                                                          |

### ErrorBoundary

| Prop            | Type                                                                          | Default | Required | Description                                                                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `children`      | `ReactNode`                                                                   |         | Yes      |                                                                                                                                                                                     |
| `fallback`      | `((props: ErrorBoundaryFallbackProps) => React.ReactNode) \| React.ReactNode` |         | No       | What to show instead of the children. Default: a `StateView` error with a generic message and a "Try again" button (no error details, which can carry personal or internal data).   |
| `onError`       | `((error: unknown, info: React.ErrorInfo) => void) \| undefined`              |         | No       | Called once per caught error, for reporting (Sentry, PostHog, your own logger). Not called for errors `rethrow` lets through.                                                       |
| `onReset`       | `(() => void) \| undefined`                                                   |         | No       | Called when the boundary resets, by `reset()` or a change in `resetKeys`.                                                                                                           |
| `resetKeys`     | `readonly unknown[] \| undefined`                                             |         | No       | Reset automatically when any of these change (a route, a record id, a filter).                                                                                                      |
| `rethrow`       | `((error: unknown) => void) \| undefined`                                     |         | No       | Throws errors this boundary must not handle, so they reach the framework. In Next.js pass `unstable_rethrow` from `next/navigation`, so `redirect()` and `notFound()` keep working. |
| `fallbackProps` | `Omit<object, "state">`                                                       |         | No       | Props for the default fallback (title, description, variant...). Ignored with `fallback`.                                                                                           |

## Accessibility

- An error is a `role="alert"`, so it is announced when it appears; loading is a polite status; empty is plain content, found by reading the page.
- The title is a real heading (`titleAs`), so screen reader users can jump to it.
- Icons are decorative (`aria-hidden`); the title says what happened.
- After an error, **Try again** is an ordinary button: keyboard users reach it with Tab.
