Implement Modern React UI Patterns for State Management
A React UI-states skill for loading, error, and empty states - no stale spinners, no swallowed errors, no double-submit buttons.
Why it matters
Build robust React components that handle loading, error, and empty states correctly, ensuring users always see appropriate feedback and never encounter stale UI or silent failures.
Outcomes
What it gets done
Show loading indicators only when no data exists to prevent UI flashing
Surface all errors to users with retry options and appropriate feedback levels
Implement empty states for collections with contextual messaging and actions
Disable buttons during async operations with loading indicators to prevent duplicate submissions
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-code-showcase-react-ui-patterns | bash Overview
React UI Patterns
This skill covers React UI patterns for loading, error, and empty states: showing loading only when no data exists, always surfacing mutation errors, disabling buttons during async operations, and providing contextual empty states. Use it when building UI components, handling async data, or managing loading/error/empty/success states in React or React Native.
What it does
This skill covers modern React UI patterns for loading states, error handling, and data fetching, built on five core principles: never show stale UI, meaning loading spinners appear only when actually loading rather than on a cached-data refetch; always surface errors to the user; use optimistic updates to make the UI feel instant; use progressive disclosure to show content as it becomes available; and favor graceful degradation, since partial data beats no data at all. The golden rule for loading states is to show a loading indicator only when there's no data to display yet, checked via a decision tree that handles errors first with a retry option, then loading-with-no-data, then data-with-items versus data-but-empty - distinguishing skeleton loaders for known content shapes like list or card layouts and initial page loads from spinners for unknown shapes like modal actions or button submissions. Error handling follows a severity hierarchy from inline field-level validation errors, to toast notifications for recoverable errors the user can retry, to page-level error banners when data is still partially usable, up to a full error screen for unrecoverable failures - with a hard rule against silently swallowing errors, since every mutation needs an error handler that both logs the failure and surfaces a user-facing toast, alongside a documented error-state component pattern with an icon, title, message, and optional retry button. Button state patterns require disabling the trigger during async operations, not just changing its label, to prevent double-submission, paired with a loading indicator prop. Every list or collection must have an explicit empty state, made contextual to the situation - a "no results found" message for a failed search versus a "no items yet" message with a create action for a genuinely empty list. A full form-submission example ties these together: validation before submit, a mutation with completion and error handlers, and a submit button that stays disabled and shows loading during the async call.
When to use - and when NOT to
Use it when building UI components, handling async data, or managing UI states such as loading, error, empty, or success - specifically React or React Native components backed by GraphQL-style mutations and queries.
Inputs and outputs
Given a data-fetching or form-submission UI requirement, it produces components following the loading, error, and empty-state decision tree, with disabled-and-loading button states and mutation error handlers that always surface feedback to the user.
Integrations
Works with GraphQL-style query and mutation hooks, a toast notification system, and sibling skills covering GraphQL schema mutation error-handling patterns, testing all UI states, and form submission patterns.
Who it's for
Frontend developers building data-driven UI who need a consistent, checklist-enforced approach to loading indicators, error surfacing, empty states, and button disable and loading behavior - avoiding stale spinners, swallowed errors, and double-submittable buttons.
Source README
React UI Patterns
When to Use
Use this skill when you need modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states.
Core Principles
- Never show stale UI - Loading spinners only when actually loading
- Always surface errors - Users must know when something fails
- Optimistic updates - Make the UI feel instant
- Progressive disclosure - Show content as it becomes available
- Graceful degradation - Partial data is better than no data
Loading State Patterns
The Golden Rule
Show loading indicator ONLY when there's no data to display.
// CORRECT - Only show loading when no data exists
const { data, loading, error } = useGetItemsQuery();
if (error) return <ErrorState error={error} onRetry={refetch} />;
if (loading && !data) return <LoadingState />;
if (!data?.items.length) return <EmptyState />;
return <ItemList items={data.items} />;
// 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
2. Toast notification → Recoverable errors, user can retry
3. Error banner → Page-level errors, data still partially usable
4. Full error screen → Unrecoverable, needs user action
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 && (
<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 && !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>
Checklist
Before completing any UI component:
UI 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 when appropriate
Data & Mutations:
- Mutations have onError handler
- All user actions have feedback (toast/visual)
Integration with Other Skills
- graphql-schema: Use mutation patterns with proper error handling
- testing-patterns: Test all UI states (loading, error, empty, success)
- formik-patterns: Apply form submission patterns
Limitations
- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.