# useDelayedLoading

> Shows a loading state only when work is actually slow, then keeps it up long enough that it never flickers.

Docs: https://hextaui.com/docs/use-delayed-loading
Markdown: https://hextaui.com/docs/use-delayed-loading.md

```tsx title="components/examples/use-delayed-loading/demo.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { useDelayedLoading } from "@/hooks/use-delayed-loading"

function Lane({ label, loading }: { label: string; loading: boolean }) {
  return (
    <div className="flex h-9 items-center justify-between gap-4 rounded-lg bg-muted px-3 text-sm">
      <span className="text-muted-foreground">{label}</span>
      <span className="flex size-4 items-center justify-center">
        {loading ? <Spinner /> : null}
      </span>
    </div>
  )
}

export function UseDelayedLoadingDemo() {
  const [loading, setLoading] = React.useState(false)
  const visible = useDelayedLoading(loading)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  const load = (ms: number) => {
    clearTimeout(timer.current)
    setLoading(true)
    timer.current = setTimeout(() => setLoading(false), ms)
  }

  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Lane label="loading" loading={loading} />
        <Lane label="useDelayedLoading(loading)" loading={visible} />
      </div>
      <div className="flex flex-wrap justify-center gap-2">
        <Button variant="outline" size="sm" onClick={() => load(80)}>
          80ms
        </Button>
        <Button variant="outline" size="sm" onClick={() => load(220)}>
          220ms
        </Button>
        <Button variant="outline" size="sm" onClick={() => load(1500)}>
          1.5s
        </Button>
      </div>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/use-delayed-loading.json
```

This adds the hook and anything it depends on.

### Manual

Copy and paste the following code into your project.

```ts title="hooks/use-delayed-loading.ts"
import * as React from "react"

type DelayedLoadingOptions = {
  delay?: number
  minDuration?: number
}

function useDelayedLoading(
  loading: boolean,
  { delay = 150, minDuration = 400 }: DelayedLoadingOptions = {}
) {
  const [visible, setVisible] = React.useState(false)
  const shownAt = React.useRef(0)

  React.useEffect(() => {
    if (loading === visible) {
      return
    }
    if (loading) {
      const timer = setTimeout(
        () => {
          shownAt.current = Date.now()
          setVisible(true)
        },
        Math.max(0, delay)
      )
      return () => clearTimeout(timer)
    }
    const remaining = Math.max(0, minDuration - (Date.now() - shownAt.current))
    const timer = setTimeout(() => setVisible(false), remaining)
    return () => clearTimeout(timer)
  }, [loading, visible, delay, minDuration])

  return visible
}

export { useDelayedLoading }
export type { DelayedLoadingOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { useDelayedLoading } from "@/hooks/use-delayed-loading"
```

```tsx
const { data, isFetching } = useQuery(query)
const showSpinner = useDelayedLoading(isFetching)

return showSpinner ? <Spinner /> : <Results data={data} />
```

Pass the raw loading flag and render from the boolean it returns. Most requests on a warm connection finish in under 150ms. Showing a spinner for those is worse than showing nothing: it flashes for a frame or two and reads as a glitch, not as progress.

## How it works

The hook applies two rules. It waits for `delay` before showing anything, so work that finishes sooner never shows a loading state. Once the indicator is visible, it stays for at least `minDuration`, so it can't appear and vanish within a few frames.

| Work takes | Description |
| --- | --- |
| `80ms` | Nothing is shown. |
| `250ms` | Shown at 150ms and held until 550ms, the 400ms minimum. |
| `900ms` | Shown at 150ms and hidden as soon as work ends. |

The 400ms minimum is long enough to register as a deliberate state and short enough not to slow anyone down.

- If `loading` turns back on while the indicator is still visible, it simply stays visible. There's no hide and show again.
- Timers are cleared when the inputs change or the component unmounts, so nothing updates state after it's gone.
- On the server and during the first render it returns `false`, so it never adds a hydration mismatch.

## Examples

### Skeletons

Skeletons replace content, so a flash is even more jarring than with a spinner. Here the first load is slow and shows the skeleton. Later loads come from a cache and never do.

```tsx title="components/examples/use-delayed-loading/skeleton.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { useDelayedLoading } from "@/hooks/use-delayed-loading"

const people = ["Ada Lovelace", "Grace Hopper", "Alan Turing"]

export function UseDelayedLoadingSkeleton() {
  const [loading, setLoading] = React.useState(false)
  const [cached, setCached] = React.useState(false)
  const showSkeleton = useDelayedLoading(loading, {
    delay: 200,
    minDuration: 500,
  })

  const refresh = () => {
    setLoading(true)
    setTimeout(
      () => {
        setLoading(false)
        setCached(true)
      },
      cached ? 60 : 1200
    )
  }

  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <ul className="flex flex-col gap-3">
        {people.map((name) => (
          <li key={name} className="flex h-5 items-center text-sm">
            {showSkeleton ? <Skeleton className="h-3 w-32" /> : name}
          </li>
        ))}
      </ul>
      <Button variant="outline" size="sm" onClick={refresh} disabled={loading}>
        {cached ? "Refresh (cached)" : "Refresh (slow)"}
      </Button>
    </div>
  )
}
```

### Tuning the timing

```tsx
const showSkeleton = useDelayedLoading(isLoading, {
  delay: 300,
  minDuration: 600,
})
```

Raise `delay` for indicators that cover a lot of the screen, like skeletons or overlays. Lower it toward 0 for actions where any wait needs acknowledging, like a payment. Keep `minDuration` above roughly 300ms.

## Good to know

- `<Spinner loading={...} />` and `<Button loading>` already use these timings. Reach for the hook when you render something else.
- Keep the space the indicator will take, as the examples do, so the layout doesn't shift when it appears.
- Pair it with an `aria-busy` or a status message. The hook only decides what to show visually.

## API reference

### useDelayedLoading(loading, options?)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | – | Whether the work is in progress right now. |
| `options.delay` | `number` | `150` | Milliseconds to wait before showing the loading state. |
| `options.minDuration` | `number` | `400` | Minimum milliseconds the loading state stays visible once shown. |

| Returns | Description |
| --- | --- |
| `boolean` | Whether to show the loading state. Always false on the server. |

### Used by

`Spinner` through its `loading` prop.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
