React UI Patterns

1. Never show stale UI - Loading spinners only when actually loading

What Is This

The React UI Patterns skill provides a set of best practices and patterns for managing UI states in modern React applications, focusing on how to handle loading states, errors, and data fetching in a user-centric way. These patterns help ensure that your React components always display the most accurate and helpful interface to users, especially in the presence of asynchronous data operations.

This skill is particularly relevant when building UI components that interact with remote data sources, handle asynchronous operations, or need to gracefully manage transitions between different states such as loading, error, and empty data sets. The patterns covered include rules for when to show loading spinners, how to surface errors, strategies for optimistic UI updates, progressive disclosure of data, and graceful degradation when only partial data is available.

Why Use It

React applications often fetch data asynchronously from APIs, which introduces a variety of states: loading, loaded, error, or empty. Poorly managed UI states lead to confusing user experiences. For example, showing loading spinners when data is already available can cause unnecessary flickers or hiding valuable cached information. Conversely, failing to surface errors leaves users in the dark, and abrupt UI changes can make an application feel unresponsive or unreliable.

The patterns provided by this skill ensure that:

  • Loading indicators only appear when absolutely necessary, avoiding stale or flickering UI.
  • Errors are always shown clearly, giving users a chance to understand and recover from failures.
  • Optimistic updates provide instant feedback, making the UI feel fast and responsive.
  • Data is progressively revealed, improving perceived performance.
  • Partial data is handled gracefully, ensuring some content is always better than none.

Following these principles results in a smoother, more reliable, and user-friendly experience.

How to Use It

1. Never Show Stale

UI

Only display loading indicators when there is no data to show. If cached or previously loaded data exists, display it immediately, even if a background refetch is happening.

Correct Approach:

const { data, loading, error, refetch } = 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} />;

Incorrect Approach:

if (loading) return <LoadingState />; // This causes spinner flashes even with cached data

2. Always Surface

Errors

Never hide failures. Always present error messages or states that inform the user what went wrong and provide a way to retry.

if (error) return <ErrorState error={error} onRetry={refetch} />;

3. Optimistic

Updates

Update the UI instantly based on user actions, even before the server confirms the change. Optimistic updates make the app feel snappy, but you must handle rollbacks if the server returns an error.

function handleAddItem(newItem) {
  // Optimistically update state
  setItems((items) => [...items, newItem]);
  // Send request
  api.addItem(newItem).catch(() => {
    // Rollback on error
    setItems((items) => items.filter(item => item.id !== newItem.id));
  });
}

4. Progressive

Disclosure

Show parts of the UI as soon as their data becomes available, rather than waiting for all data to load. This often involves breaking up large data fetches or using skeleton screens for sections.

return (
  <>
    <ProfileSection user={user} />
    {postsLoading ? <PostsSkeleton /> : <PostsList posts={posts} />}
  </>
);

5. Graceful

Degradation

If only partial data is available, show as much as possible rather than hiding everything. This helps users make progress even in degraded network conditions.

if (user && !posts) {
  return (
    <>
      <ProfileSection user={user} />
      <LoadingState message="Loading posts..." />
    </>
  );
}

Decision Tree for Loading States

A useful mental model for handling loading states is:

  • Is there an error?
    • Yes: Show error state with retry
    • No: Continue
  • Is it loading and we have no data?
    • Yes: Show loading indicator
    • No: Continue
  • Do we have data?
    • Yes, with items: Show the data
    • Yes, but empty: Show empty state
    • No: Show loading fallback

When to Use It

Use these React UI patterns whenever you build components that:

  • Fetch data asynchronously (REST, GraphQL, etc.)
  • Need to handle multiple states (loading, error, empty, success)
  • Display lists, dashboards, or detail views that should update in real time or upon user actions
  • Want to provide a polished, user-friendly experience in the face of network variability

These patterns are especially valuable in applications where perceived performance and reliability are critical, such as dashboards, data-heavy UIs, or any client application where users expect up-to-date information.

Important Notes

  • Always prefer showing cached or previously fetched data over a loading spinner.
  • Avoid hiding errors from users - always provide feedback and a retry option.
  • Use optimistic updates carefully, and make sure to handle rollbacks for failed requests.
  • Progressive disclosure and graceful degradation both improve the perceived speed and reliability of your app.
  • Adhering to these patterns leads to more maintainable and predictable UI components across your codebase.

By following these React UI patterns, you create user interfaces that are responsive, resilient, and clear, resulting in happier users and easier-to-maintain code.