# DataState

> One wrapper for the states of a data view, in a fixed order of loading, signed out, service unavailable, error with retry, empty with an action, then the content.

Source: https://docs.nasaqui.com/components/data-state

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/data-state.json
```

Wraps a list, table or chart that loads data and picks the right state for you. Pass what your fetch knows
(`loading`, `error`, `empty`…) and the content as children; only one thing renders.

## When to use

- Any view that fetches: a table page, a dashboard card, a side panel list.
- When the same view can be signed out (401), down (503) or failing for another reason.

## When not to use

- A whole failed page or route: use [`ErrorPages`](https://docs.nasaqui.com/components/error-pages).
- A single state on its own: use `LoadingState`, `EmptyState` or `ErrorState` from [`States`](https://docs.nasaqui.com/components/states).

## Import

```tsx
import { DataState, ServiceUnavailable } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Button, DataState } from "@fadymondy/nasaq/web";

declare const query: { isLoading: boolean; error: Error | null; data: { id: string; name: string }[]; refetch(): void };

export function Projects() {
  return (
    <DataState
      loading={query.isLoading}
      error={query.error}
      onRetry={query.refetch}
      empty={query.data.length === 0}
      labels={{ emptyTitle: "No projects yet" }}
      emptyAction={<Button variant="primary">New project</Button>}
    >
      <ul>{query.data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
    </DataState>
  );
}
```

## Anatomy

```
DataState                       data-slot="data-state" on the state shown
├─ loading       → loadingFallback, or LoadingState with skeleton rows
├─ unauthorized  → EmptyState with a sign-in button
├─ unavailable   → ServiceUnavailable with retry
├─ error         → ErrorState with the message and retry
├─ empty         → EmptyState with emptyAction
└─ children      when none of the above
```

The first true state wins, in that order.

## API

**DataState**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | | Show the loading state. |
| `loadingFallback` | `ReactNode` | | Your own skeleton instead of the default. |
| `loadingRows` | `number` | `LoadingState`'s default | Rows of the default skeleton. |
| `unauthorized` | `boolean` | | Signed out or session expired. |
| `onSignIn` / `signInHref` | `() => void` / `string` | | The sign-in button's action or link. |
| `unavailable` | `boolean` | | The service is down (503, offline). |
| `error` | `unknown` | | An `Error`, a string or any truthy value. Its message is shown. |
| `onRetry` | `() => void` | | Adds a retry button to the error and unavailable states. |
| `empty` | `boolean` | | No rows. |
| `emptyIcon` | `LucideIcon` | | Icon of the empty state. |
| `emptyAction` | `ReactNode` | | A call to action, usually "Create…". |
| `labels` | `Partial<DataStateLabels>` | | Override any string, including the empty state's `emptyTitle` and `emptyBody`. |
| `className` | `string` | | On the state shown. |
| `children` | `ReactNode` | | The content. |

**ServiceUnavailable**: the `EmptyState` props plus `onRetry`, an optional `title` and `labels`. The "can't reach the service" state on its own.

## Accessibility

- Loading is announced through `LoadingState` (`role="status"`). Errors use `role="alert"`.
- Retry and sign-in are real buttons or links with visible text.

## RTL & i18n

- English and Arabic strings follow the Nasaq locale. The states are centred, so they read the same in both directions.

## Styling & tokens

- Built on `States` and `Button`. Target `[data-slot="data-state"]`.

## Do / Don't

- Do pass `onRetry` whenever the request can be repeated.
- Do keep `empty` for "no rows" and `error` for "request failed"; don't show an empty table after a failure.
- Don't nest DataState inside DataState for the same request.

## Related

- [`States`](https://docs.nasaqui.com/components/states)
- [`ErrorPages`](https://docs.nasaqui.com/components/error-pages)

## Lab

https://docs.nasaqui.com/?path=/docs/components-loading-states-data-state--docs

## Code

### React

```tsx
import { Button, DataState } from "@fadymondy/nasaq/web";

declare const query: { isLoading: boolean; error: Error | null; data: { id: string; name: string }[]; refetch(): void };

export function Projects() {
  return (
    <DataState
      loading={query.isLoading}
      error={query.error}
      onRetry={query.refetch}
      empty={query.data.length === 0}
      labels={{ emptyTitle: "No projects yet" }}
      emptyAction={<Button variant="primary">New project</Button>}
    >
      <ul>{query.data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
    </DataState>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { DataState } from "@/components/ui/data-state";

declare const query: { isLoading: boolean; error: Error | null; data: { id: string; name: string }[]; refetch(): void };

export function Projects() {
  return (
    <DataState
      loading={query.isLoading}
      error={query.error}
      onRetry={query.refetch}
      empty={query.data.length === 0}
      labels={{ emptyTitle: "No projects yet" }}
      emptyAction={<Button variant="primary">New project</Button>}
    >
      <ul>{query.data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
    </DataState>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqDataState } from "@fadymondy/nasaq/vue";

const props = withDefaults(defineProps<{ loading?: boolean; error?: Error | null; items?: string[] }>(), {
  items: () => [],
});
</script>

<template>
  <NqDataState :loading="loading" :error="error" :empty="props.items.length === 0" @retry="() => {}">
    <template #empty-action><NqButton variant="primary" size="sm">New order</NqButton></template>
    <ul>
      <li v-for="item in props.items" :key="item">{{ item }}</li>
    </ul>
  </NqDataState>
</template>
```

### Blade

```blade
<div class="flex flex-col gap-6">
    <x-nq::data-state :empty="true">
        <x-slot:emptyAction><x-nq::button variant="primary" size="sm">New order</x-nq::button></x-slot:emptyAction>
    </x-nq::data-state>
    <x-nq::data-state :unavailable="true" :retry="['wire:click' => 'load']" />
    <x-nq::data-state error="The orders request timed out." />
    <x-nq::data-state :unauthorized="true" sign-in-href="/login" />
    <x-nq::data-state><ul><li>Order 1042</li></ul></x-nq::data-state>
</div>
```

### HTML + Alpine

```html
<div class="flex flex-col gap-6">
    <div data-slot="empty-state" data-state="empty" class="flex flex-col items-center justify-center gap-3 border border-dashed border-border px-6 py-12 text-center rounded-card">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
  <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">Nothing here yet</p>
            </div>
            <div class="mt-1 flex flex-wrap items-center justify-center gap-2"><button data-slot="button"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control-sm px-2.5">
        New order</button></div>
    </div>
    <div data-slot="service-unavailable" role="status" data-state="unavailable" class="flex flex-col items-center justify-center gap-3 border border-dashed border-border px-6 py-12 text-center rounded-card">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10.94 5.274A7 7 0 0 1 15.71 10h1.79a4.5 4.5 0 0 1 4.222 6.057"/>
  <path d="M18.796 18.81A4.5 4.5 0 0 1 17.5 19H9A7 7 0 0 1 5.79 5.78"/>
  <path d="m2 2 20 20"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">This service is not available right now</p>
                    <p class="text-body-sm text-muted-foreground">It may be starting up or under maintenance. Try again in a moment.</p>
            </div>
            <div class="mt-1 flex flex-wrap items-center justify-center gap-2"><button data-slot="button"
     type="button"                         wire:click="load" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        Try again</button></div>
    </div>
    <div data-slot="error-state" role="alert" data-state="error" class="flex flex-col items-center justify-center gap-3 border border-dashed border-border px-6 py-12 text-center rounded-card">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-nq-danger-text">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <line x1="12" x2="12" y1="8" y2="12"/>
  <line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">Something went wrong</p>
                    <p class="text-body-sm text-muted-foreground">The orders request timed out.</p>
            </div>
    </div>
    <div data-slot="data-state" data-state="unauthorized" class="flex flex-col items-center justify-center gap-3 border border-dashed border-border px-6 py-12 text-center rounded-card">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m10 17 5-5-5-5"/>
  <path d="M15 12H3"/>
  <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">Your session has ended</p>
                    <p class="text-body-sm text-muted-foreground">Sign in again to see this page.</p>
            </div>
            <div class="mt-1 flex flex-wrap items-center justify-center gap-2"><a data-slot="button"
     href="/login"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control-sm px-2.5">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m10 17 5-5-5-5"/>
  <path d="M15 12H3"/>
  <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
</svg> Sign in</a></div>
    </div>
    <ul><li>Order 1042</li></ul>
</div>
```
