react-ui-patterns

Modern React UI patterns for loading states, error handling, and data fetching. Covers five core principles: never show stale UI, always surface errors, use optimistic updates, progressive disclosure, and graceful degradation Provides decision trees and component patterns for loading indicators, skeleton screens, error states, empty states, and button submission handling Includes critical anti-patterns to avoid, such as showing spinners when cached data exists, silently swallowing errors, and failing to disable buttons during async operations Offers a pre-submission checklist covering UI states, data mutations, and user feedback requirements

INSTALLATION
npx skills add https://github.com/sickn33/antigravity-awesome-skills --skill react-ui-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

if (error) return ;

if (loading && !data) return ;

if (!data?.items.length) return ;

return ;

// WRONG - Shows spinner even when we have cached data

if (loading) return <LoadingState />; // Flashes on refetch!


### Loading State Decision Tree

Is there an error?

→ Yes: Show error state with retry option

→ No: Continue

Is it loading AND we have no data?

→ Yes: Show loading indicator (spinner/skeleton)

→ No: Continue

Do we have data?

→ Yes, with items: Show the data

→ Yes, but empty: Show empty state

→ No: Show loading (fallback)


### Skeleton vs Spinner

Use Skeleton When
Use Spinner When

Known content shape
Unknown content shape

List/card layouts
Modal actions

Initial page load
Button submissions

Content placeholders
Inline operations

## Error Handling Patterns

### The Error Handling Hierarchy
  1. Inline error (field-level) → Form validation errors
  1. Toast notification → Recoverable errors, user can retry
  1. Error banner → Page-level errors, data still partially usable
  1. Full error screen → Unrecoverable, needs user action
  2. 
    ### Always Show Errors
    
    **CRITICAL: Never swallow errors silently.**
    

// CORRECT - Error always surfaced to user

const [createItem, { loading }] = useCreateItemMutation({

onCompleted: () => {

toast.success({ title: 'Item created' });

},

onError: (error) => {

console.error('createItem failed:', error);

toast.error({ title: 'Failed to create item' });

},

});

// WRONG - Error silently caught, user has no idea

const [createItem] = useCreateItemMutation({

onError: (error) => {

console.error(error); // User sees nothing!

},

});


### Error State Component Pattern

interface ErrorStateProps {

error: Error;

onRetry?: () => void;

title?: string;

}

const ErrorState = ({ error, onRetry, title }: ErrorStateProps) => (

<div className="error-state">

<Icon name="exclamation-circle" />

<h3>{title ?? 'Something went wrong'}</h3>

<p>{error.message}</p>

{onRetry &#x26;&#x26; (

<Button onClick={onRetry}>Try Again</Button>

)}

</div>

);


## Button State Patterns

### Button Loading State

<Button

onClick={handleSubmit}

isLoading={isSubmitting}

disabled={!isValid || isSubmitting}

>

Submit

</Button>


### Disable During Operations

**CRITICAL: Always disable triggers during async operations.**

// CORRECT - Button disabled while loading

<Button

disabled={isSubmitting}

isLoading={isSubmitting}

onClick={handleSubmit}

>

Submit

</Button>

// WRONG - User can tap multiple times

<Button onClick={handleSubmit}>

{isSubmitting ? 'Submitting...' : 'Submit'}

</Button>


## Empty States

### Empty State Requirements

Every list/collection MUST have an empty state:

// WRONG - No empty state

return <FlatList data={items} />;

// CORRECT - Explicit empty state

return (

<FlatList

data={items}

ListEmptyComponent={<EmptyState />}

/>

);


### Contextual Empty States

// Search with no results

<EmptyState

icon="search"

title="No results found"

description="Try different search terms"

/>

// List with no items yet

<EmptyState

icon="plus-circle"

title="No items yet"

description="Create your first item"

action={{ label: 'Create Item', onClick: handleCreate }}

/>


## Form Submission Pattern

const MyForm = () => {

const [submit, { loading }] = useSubmitMutation({

onCompleted: handleSuccess,

onError: handleError,

});

const handleSubmit = async () => {

if (!isValid) {

toast.error({ title: 'Please fix errors' });

return;

}

await submit({ variables: { input: values } });

};

return (

<form>

<Input

value={values.name}

onChange={handleChange('name')}

error={touched.name ? errors.name : undefined}

/>

<Button

type="submit"

onClick={handleSubmit}

disabled={!isValid || loading}

isLoading={loading}

>

Submit

</Button>

</form>

);

};


## Anti-Patterns

### Loading States

// WRONG - Spinner when data exists (causes flash)

if (loading) return <Spinner />;

// CORRECT - Only show loading without data

if (loading &#x26;&#x26; !data) return <Spinner />;


### Error Handling

// WRONG - Error swallowed

try {

await mutation();

} catch (e) {

console.log(e); // User has no idea!

}

// CORRECT - Error surfaced

onError: (error) => {

console.error('operation failed:', error);

toast.error({ title: 'Operation failed' });

}


### Button States

// WRONG - Button not disabled during submission

<Button onClick={submit}>Submit</Button>

// CORRECT - Disabled and shows loading

<Button onClick={submit} disabled={loading} isLoading={loading}>

Submit

</Button>

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card