UNPKG

@suspensive/react

Version:

Suspensive interfaces for react

259 lines (199 loc) 6.65 kB
--- 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`: 100300 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`.