react-ui-patterns

Patterns for building robust UI components that handle loading, error, empty, and success states.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-ui-patterns" with this command: npx skills add sivag-lab/roth_mcp/sivag-lab-roth-mcp-react-ui-patterns

React UI Patterns

Patterns for building robust UI components that handle loading, error, empty, and success states.

When to Use

  • Building components that fetch or mutate data

  • Handling async UI state transitions

  • Implementing form submissions

  • Reviewing UI for missing states

Core Principles

  • Never show stale UI — Loading indicators only when actually loading

  • Always surface errors — Users must know when something fails

  • Optimistic updates — Make the UI feel instant where safe

  • Progressive disclosure — Show content as it becomes available

  • Graceful degradation — Partial data is better than no data

Loading States

The Golden Rule

Show loading indicator ONLY when there's no data to display.

const { data, loading, error } = useQuery(GET_ITEMS);

if (error) return <ErrorState error={error} onRetry={refetch} />; if (loading && !data) return <LoadingSkeleton />; if (!data?.items.length) return <EmptyState />;

return <ItemList items={data.items} />;

// WRONG — flashes spinner on refetch when cached data exists if (loading) return <Spinner />;

// CORRECT — only show loading when no cached data if (loading && !data) return <Spinner />;

Decision Tree

Error? ──> Yes ──> Show error state with retry │ No │ Loading AND no data? ──> Yes ──> Show skeleton/spinner │ No │ Has data? ──> Yes, with items ──> Show data │ Yes, empty ──────> Show empty state No ──────────────────────────> Show loading (fallback)

Skeleton vs Spinner

Use Skeleton Use Spinner

Known content shape (lists, cards) Unknown content shape

Initial page load Modal/dialog actions

Content placeholders Button submissions

Dashboard layouts Inline operations

Error Handling

Error Hierarchy

Level When Example

Inline Field-level validation "Email is required" under input

Toast Recoverable, user can retry "Failed to save — try again"

Banner Page-level, partial data usable "Some data couldn't load"

Full screen Unrecoverable, needs action "Session expired — sign in"

Always Surface Errors

// CORRECT — error shown to user const [createItem] = useMutation(CREATE_ITEM, { onCompleted: () => toast.success('Item created'), onError: (error) => { console.error('createItem failed:', error); toast.error('Failed to create item'); }, });

// WRONG — error swallowed silently const [createItem] = useMutation(CREATE_ITEM, { onError: (error) => console.error(error), // User sees nothing! });

Error State Component

interface ErrorStateProps { error: Error; onRetry?: () => void; title?: string; }

function ErrorState({ error, onRetry, title }: ErrorStateProps) { return ( <div role="alert" className="error-state"> <AlertCircleIcon /> <h3>{title ?? 'Something went wrong'}</h3> <p>{error.message}</p> {onRetry && <Button onClick={onRetry}>Try Again</Button>} </div> ); }

Button States

Loading State

<Button onClick={handleSubmit} disabled={!isValid || isSubmitting} isLoading={isSubmitting}

Submit </Button>

Critical Rule

Always disable buttons during async operations.

// CORRECT — disabled and shows loading <Button disabled={isSubmitting} isLoading={isSubmitting} onClick={submit}> Submit </Button>

// WRONG — user can click multiple times <Button onClick={submit}> {isSubmitting ? 'Submitting...' : 'Submit'} </Button>

Empty States

Every list or collection MUST have an empty state.

// WRONG — blank screen when no items <FlatList data={items} renderItem={renderItem} />

// CORRECT — explicit empty state {items.length === 0 ? ( <EmptyState icon={<PlusCircleIcon />} title="No items yet" description="Create your first item to get started" action={{ label: 'Create Item', onClick: handleCreate }} /> ) : ( <ItemList items={items} /> )}

Contextual Empty States

Context Icon Title Action

Search no results Search "No results found" "Try different terms"

Empty collection PlusCircle "No items yet" "Create Item" button

Filtered empty Filter "No matches" "Clear filters" button

Error empty AlertTriangle "Couldn't load" "Retry" button

Form Submission

function CreateItemForm() { const [submit, { loading }] = useMutation(CREATE_ITEM, { onCompleted: () => { toast.success('Item created'); router.push('/items'); }, onError: (error) => { console.error('Create failed:', error); toast.error('Failed to create item'); }, });

const handleSubmit = async (values: FormValues) => { if (!isValid) { toast.error('Please fix errors before submitting'); return; } await submit({ variables: { input: values } }); };

return ( <form onSubmit={handleSubmit}> <Input name="name" value={values.name} onChange={handleChange} error={touched.name ? errors.name : undefined} /> <Button type="submit" disabled={!isValid || loading} isLoading={loading} > Create Item </Button> </form> ); }

Anti-Patterns

Loading

// WRONG — spinner when cached data exists (causes flash) if (loading) return <Spinner />;

// CORRECT — only show loading without data if (loading && !data) return <Spinner />;

Errors

// WRONG — error swallowed try { await mutation(); } catch (e) { console.log(e); }

// CORRECT — error surfaced onError: (error) => { console.error('operation failed:', error); toast.error('Operation failed'); }

Buttons

// WRONG — not disabled during submission <Button onClick={submit}>Submit</Button>

// CORRECT — disabled and loading <Button onClick={submit} disabled={loading} isLoading={loading}>Submit</Button>

UI State Checklist

Before shipping any UI component:

States:

  • Error state handled and shown to user

  • Loading state shown only when no data exists

  • Empty state provided for collections

  • Buttons disabled during async operations

  • Buttons show loading indicator

Data:

  • Mutations have onError handler

  • All user actions have feedback (toast / visual change)

  • Optimistic updates where appropriate

  • Stale data doesn't flash on refetch

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

react-modernization

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-flow-node-ts

No summary provided by upstream source.

Repository SourceNeeds Review
General

commit

No summary provided by upstream source.

Repository SourceNeeds Review
General

design-md

No summary provided by upstream source.

Repository SourceNeeds Review