---
name: loading-ux
description: >
  Loading UX without flash-of-loading in @suspensive/react: Suspense fallback,
  Delay (ms, fallback, isDelayed render prop) to hold back spinners and fade
  skeletons in, DefaultPropsProvider + new DefaultProps for app-wide default
  fallbacks. Load when writing Suspense fallbacks, skeletons, spinners, or
  global loading defaults.
metadata:
  type: sub-skill
  library: '@suspensive/react'
  library_version: '3.21.2'
  framework: react
requires:
  - 'react'
sources:
  - 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react/Suspense.mdx'
  - 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react/Delay.mdx'
  - 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react/DefaultPropsProvider.mdx'
  - 'toss/suspensive:packages/react/src/Delay.tsx'
---

# Loading UX with Suspense, Delay, and DefaultPropsProvider

This skill builds on react. Read ../SKILL.md first for the client-only constraint and the `.with()` HOC pattern.

## Setup

```tsx
'use client'
import { Delay, Suspense } from '@suspensive/react'
import { Content } from './Content'
import { Spinner } from './Spinner'

export const Example = () => (
  <Suspense
    fallback={
      <Delay ms={200}>
        <Spinner />
      </Delay>
    }
  >
    <Content />
  </Suspense>
)
```

`Delay` postpones exposing its children by `ms` — if `Content` resolves within 200ms, the user never sees the spinner. Recommended `ms`: 100–300 for most UI, 500+ for non-critical sections.

`DelayProps` is a union: either `{ ms?, fallback?: ReactNode, children?: ReactNode }` or `{ ms?, children?: ({ isDelayed }) => ReactNode }` (no `fallback` allowed with a render-prop child).

## Core Patterns

### Fade the skeleton in with the isDelayed render prop

```tsx
'use client'
import { Delay, Suspense } from '@suspensive/react'
import { Content } from './Content'
import { Skeleton } from './Skeleton'

export const Example = () => (
  <Suspense
    fallback={
      <Delay ms={200}>
        {({ isDelayed }) => <Skeleton style={{ opacity: isDelayed ? 1 : 0, transition: 'opacity 200ms' }} />}
      </Delay>
    }
  >
    <Content />
  </Suspense>
)
```

When `children` is a render prop, `Delay` always renders it and flips `isDelayed` to `true` after `ms`, enabling opacity transitions instead of an abrupt pop-in (available since v3.12).

### Show an interim fallback before the delayed children

```tsx
'use client'
import { Delay } from '@suspensive/react'

export const Example = () => (
  <Delay ms={200} fallback={<>Almost there...</>}>
    200ms delayed content
  </Delay>
)
```

### One default fallback for every Suspense and Delay in the app

```tsx
'use client'
import { DefaultProps, DefaultPropsProvider, Suspense } from '@suspensive/react'
import { Page } from './Page'
import { Spinner } from './Spinner'

const defaultProps = new DefaultProps({
  Delay: { ms: 1200 },
  Suspense: { fallback: <Spinner />, clientOnly: false },
})

export const App = () => (
  <DefaultPropsProvider defaultProps={defaultProps}>
    <Suspense>
      <Page />
    </Suspense>
  </DefaultPropsProvider>
)
```

`<Suspense>` and `<Delay>` without explicit props read defaults from the nearest provider; per-component props always win.

## Common Mistakes

### MEDIUM Hand-rolling spinner delay with useState/useEffect/setTimeout

Wrong:

```tsx
import { useEffect, useState } from 'react'

const DelayedSpinner = () => {
  const [show, setShow] = useState(false)
  useEffect(() => {
    const t = setTimeout(() => setShow(true), 200)
    return () => clearTimeout(t)
  }, [])
  return show ? <Spinner /> : null
}
```

Correct:

```tsx
import { Delay, Suspense } from '@suspensive/react'

const Example = ({ children }) => (
  <Suspense
    fallback={
      <Delay ms={200}>
        <Spinner />
      </Delay>
    }
  >
    {children}
  </Suspense>
)
```

`Delay` does the timeout declaratively, composes inside any Suspense fallback, and picks up default `ms` from `DefaultPropsProvider`.

Source: docs/suspensive.org/src/content/en/docs/react/Delay.mdx

### MEDIUM Passing a plain object to DefaultPropsProvider

Wrong:

```tsx
import { DefaultPropsProvider } from '@suspensive/react'

const App = ({ children }) => (
  <DefaultPropsProvider defaultProps={{ Suspense: { fallback: <Spinner /> } }}>{children}</DefaultPropsProvider>
)
```

Correct:

```tsx
import { DefaultProps, DefaultPropsProvider } from '@suspensive/react'

const defaultProps = new DefaultProps({ Suspense: { fallback: <Spinner /> } })

const App = ({ children }) => <DefaultPropsProvider defaultProps={defaultProps}>{children}</DefaultPropsProvider>
```

`defaultProps` must be an instance of the `DefaultProps` class; a plain object literal is not accepted.

Source: docs/suspensive.org/src/content/en/docs/react/DefaultPropsProvider.mdx

### MEDIUM Abrupt skeleton-to-content swap instead of isDelayed fade

Wrong:

```tsx
import { Delay } from '@suspensive/react'

// plus a separate hand-written fade-in component around Spinner
const Fallback = () => (
  <Delay ms={200}>
    <FadeIn>
      <Spinner />
    </FadeIn>
  </Delay>
)
```

Correct:

```tsx
import { Delay } from '@suspensive/react'

const Fallback = () => (
  <Delay ms={200}>
    {({ isDelayed }) => <Spinner style={{ opacity: isDelayed ? 1 : 0, transition: 'opacity 200ms' }} />}
  </Delay>
)
```

The render-prop child receives `isDelayed` (v3.12+) exactly for this; agents unaware of it wire CSS animation components by hand.

Source: docs/suspensive.org/src/content/en/docs/react/Delay.mdx (#1312)

### MEDIUM Reading Delay ms as extending fallback display time

Wrong:

```tsx
import { Delay, Suspense } from '@suspensive/react'

// expecting the spinner to stay visible for at least 3s
const Example = ({ children }) => (
  <Suspense
    fallback={
      <Delay ms={3000}>
        <Spinner />
      </Delay>
    }
  >
    {children}
  </Suspense>
)
```

Correct:

```tsx
import { Delay, Suspense } from '@suspensive/react'

// ms only postpones when the spinner APPEARS; content replaces it as soon as it resolves
const Example = ({ children }) => (
  <Suspense
    fallback={
      <Delay ms={200}>
        <Spinner />
      </Delay>
    }
  >
    {children}
  </Suspense>
)
```

`Delay` postpones exposing its children/fallback; it never prolongs how long a fallback stays visible — a 1s fetch with `ms={3000}` shows no spinner at all, not 4s of spinner.

Source: https://github.com/toss/suspensive/issues/182

See also: the `declarative-queries` skill in `@suspensive/react-query-4`/`@suspensive/react-query-5` — `Delay` composes inside Suspense fallbacks that wrap `SuspenseQuery`.
