Framework docs
React
Reference docs for @waitency/react. Start here if you are shipping Waitency in a React 18+ application.
Installation
Works with Next.js, Vite, Create React App, Remix, and any React 18+ setup.
Basic usage
Import useWaitency and defineTimeline, define your phases, then call start() and resolve(). The hook returns a phase object whose name you switch on to render the right UI.
import { useState } from 'react'
import { useWaitency, defineTimeline } from '@waitency/react'
type Profile = { id: string; name: string }
const phases = defineTimeline([
{ name: 'silent', duration: 50 },
{ name: 'spinner', duration: 950 },
{ name: 'message' },
])
function MyComponent() {
const [profile, setProfile] = useState<Profile | null>(null)
const { phase, isPending, start, resolve } = useWaitency<Profile>({ phases })
async function handleLoad() {
start()
const nextProfile = await fetchData()
setProfile(nextProfile)
resolve(nextProfile)
}
if (!profile && phase?.name === 'spinner') return <Spinner />
if (!profile && phase?.name === 'message') return <Spinner message="Taking a moment…" />
return <YourContent data={profile} isLoading={isPending} onLoad={handleLoad} />
}Custom timelines
Each component can have its own timing. Use shorter timelines for fast interactions and longer timelines for data-heavy screens.
import { useState } from 'react'
import { useWaitency, defineTimeline } from '@waitency/react'
type Row = { id: string; name: string }
const phases = defineTimeline([
{ name: 'silent', duration: 80 },
{ name: 'spinner', duration: 1920 },
{ name: 'message' },
])
function DataTable({ userId }: { userId: string }) {
const [rows, setRows] = useState<Row[]>([])
const { phase, start, resolve } = useWaitency<Row[]>({ phases })
async function load() {
start()
const nextRows = await fetch(`/api/users/${userId}`).then((response) => response.json())
setRows(nextRows)
resolve(nextRows)
}
if (!rows.length && phase?.name === 'spinner') return <TableSkeleton rows={6} />
if (!rows.length && phase?.name === 'message') return <TableSkeleton rows={6} message="Fetching records…" />
return <Table data={rows} onRefresh={load} />
}Minimum display
minDisplay ensures a phase stays visible for at least the specified duration, even if resolve() fires early. Use this to prevent spinner flash when a request finishes just after a loading state appears.
import { defineTimeline } from '@waitency/react'
const phases = defineTimeline([
{ name: 'silent', duration: 50 },
{ name: 'spinner', duration: 950, minDisplay: 300 },
{ name: 'message', minDisplay: 500 },
])
// If resolve() fires at 200ms, the spinner still
// stays visible for at least 300ms to avoid flash.Error handling
Call reject(error) instead of resolve() when an operation fails. The hook moves to the rejected status and exposes the error via error and isRejected. Call reset() before retrying to return to idle.
import { useState } from 'react'
import { useWaitency, defineTimeline } from '@waitency/react'
const phases = defineTimeline([
{ name: 'silent', duration: 50 },
{ name: 'spinner', duration: 950 },
{ name: 'message' },
])
function UserProfile({ userId }: { userId: string }) {
const [profile, setProfile] = useState(null)
const { phase, isRejected, error, start, resolve, reject, reset } = useWaitency({ phases })
async function load() {
start()
try {
const data = await fetchUser(userId)
setProfile(data)
resolve(data)
} catch (err) {
reject(err)
}
}
if (isRejected) {
return (
<div>
<p>Something went wrong: {error?.message}</p>
<button onClick={() => { reset(); load() }}>Retry</button>
</div>
)
}
if (phase?.name === 'spinner') return <Spinner />
if (phase?.name === 'message') return <Message text="Hang tight…" />
return <ProfileCard data={profile} onLoad={load} />
}TypeScript
Waitency ships its own type definitions. phase, status, and the phase config are all typed.
import type { WaitencyPhase, WaitencyStatus } from '@waitency/react'
import { defineTimeline, useWaitency } from '@waitency/react'
const phases: WaitencyPhase[] = defineTimeline([
{ name: 'silent', duration: 50 },
{ name: 'spinner', duration: 950 },
{ name: 'message' },
])
const {
status,
phase,
elapsed,
start,
resolve,
reject,
reset,
} = useWaitency({ phases })
// status -> WaitencyStatus
// phase -> WaitencyPhase | null
// elapsed -> numberAPI reference
defineTimeline(phases)
Creates a validated phase array with absolute timing boundaries. Each phase object accepts:
| Property | Type | Default | Description |
|---|---|---|---|
| name | string | required | Phase identifier returned via phase.name. |
| duration | number | Infinity | Time in ms before advancing to the next phase. |
| minDisplay | number | 0 | Minimum time in ms this phase remains visible after resolve. |
useWaitency(options)
Options:
| Option | Type | Description |
|---|---|---|
| phases | WaitencyPhase[] | Timeline produced by defineTimeline(). |
| componentId | string? | Registers the hook with the global registry for Pro tooling. |
| onPhaseChange | (phase, elapsed) => void | Called each time the active phase changes. |
| onResolve | (data, elapsed) => void | Called when resolve() settles the operation. |
| onReject | (error, elapsed) => void | Called when reject() settles the operation with an error. |
| onCancel | (elapsed) => void | Called when cancel() aborts the timeline. |
Return value:
| Property | Type | Description |
|---|---|---|
| state | WaitencyState | Full state snapshot returned by the underlying machine. |
| status | WaitencyStatus | 'idle' | 'pending' | 'resolved' | 'rejected'. |
| phase | WaitencyPhase | null | Current phase object while the timeline is active. |
| settledPhase | WaitencyPhase | null | Phase that was active when the request settled. |
| elapsed | number | Milliseconds since start() was called. |
| data | TData | undefined | Data passed to resolve(data). |
| error | TError | undefined | Error passed to reject(error). |
| isIdle | boolean | True when the hook has not started. |
| isPending | boolean | True while the operation is in progress. |
| isResolved | boolean | True after resolve() completes. |
| isRejected | boolean | True after reject() completes. |
| start | () => void | Begin the timeline. |
| resolve | (data?) => void | Mark the operation as successful. |
| reject | (error?) => void | Mark the operation as failed. |
| cancel | () => void | Abort the operation and return to idle. |
| reset | () => void | Reset state back to idle manually. |