Skip to content
intermediatePhase 37 · Frontend Architecture

Loading States

Implement skeleton screens, spinners, and optimistic UI patterns.

30m
0 problems
Topic Progress0%

Skeleton Screens

Skeleton Screens

Skeleton screens show a placeholder that resembles the final content, improving perceived performance.

Basic Skeleton Component

// components/Skeleton.jsx
function Skeleton({ width, height, borderRadius = '4px', className = '' }) {
  return (
    <div
      className={`skeleton ${className}`}
      style={{
        width,
        height,
        borderRadius,
      }}
    />
  );
}

// Card skeleton
function CardSkeleton() {
  return (
    <div className="card-skeleton">
      <Skeleton width="100%" height="200px" borderRadius="8px 8px 0 0" />
      <div className="card-skeleton-body">
        <Skeleton width="70%" height="24px" />
        <Skeleton width="100%" height="16px" />
        <Skeleton width="80%" height="16px" />
      </div>
    </div>
  );
}

// List skeleton
function ListSkeleton({ count = 5 }) {
  return (
    <ul className="list-skeleton">
      {Array.from({ length: count }).map((_, i) => (
        <li key={i} className="list-skeleton-item">
          <Skeleton width="48px" height="48px" borderRadius="50%" />
          <div className="list-skeleton-content">
            <Skeleton width="60%" height="18px" />
            <Skeleton width="40%" height="14px" />
          </div>
        </li>
      ))}
    </ul>
  );
}

CSS Animation

.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.skeleton-text {
  height: 1em;
  border-radius: 4px;
}

.skeleton-circle {
  border-radius: 50%;
}

Usage Pattern

function ProductPage({ productId }) {
  const { data: product, isLoading } = useQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProduct(productId),
  });

  if (isLoading) {
    return <ProductSkeleton />;
  }

  return (
    <div className="product">
      <img src={product.image} alt={product.name} />
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

Benefits

  • Reduces perceived loading time
  • Provides visual feedback
  • Maintains layout stability
  • More informative than spinners

Spinners

Spinners

Spinners indicate ongoing activity when the content shape is unknown.

Spinner Component

// components/Spinner.jsx
function Spinner({ size = 'md', color = 'currentColor', className = '' }) {
  const sizeMap = {
    sm: '16px',
    md: '24px',
    lg: '32px',
    xl: '48px',
  };

  return (
    <svg
      className={`spinner ${className}`}
      width={sizeMap[size]}
      height={sizeMap[size]}
      viewBox="0 0 24 24"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <circle
        cx="12"
        cy="12"
        r="10"
        stroke={color}
        strokeWidth="3"
        strokeLinecap="round"
        strokeDasharray="31.42"
        strokeDashoffset="10"
      />
    </svg>
  );
}

// Button with loading state
function Button({ children, loading, disabled, ...props }) {
  return (
    <button
      disabled={disabled || loading}
      {...props}
    >
      {loading ? (
        <span className="btn-loading">
          <Spinner size="sm" />
          <span>Loading...</span>
        </span>
      ) : (
        children
      )}
    </button>
  );
}

Full-Page Loading

function FullPageSpinner() {
  return (
    <div className="full-page-spinner">
      <Spinner size="xl" />
      <p>Loading...</p>
    </div>
  );
}

// App-level loading
function App() {
  const { isLoading } = useAuth();

  if (isLoading) {
    return <FullPageSpinner />;
  }

  return <Router />;
}

Inline Loading

function InlineLoader({ isLoading, children, fallback = null }) {
  if (isLoading) {
    return fallback || <Spinner size="sm" />;
  }
  return children;
}

// Usage
<InlineLoader isLoading={saving}>
  <Input value={value} onChange={onChange} />
</InlineLoader>

When to Use Spinners

  • Button loading states
  • Form submissions
  • Unknown content shape
  • Short loading durations (< 3s)

Optimistic UI

Optimistic UI

Update the UI immediately before the server confirms the change.

Basic Pattern

function TodoList() {
  const queryClient = useQueryClient();

  const addTodo = useMutation({
    mutationFn: (newTodo) =>
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then(r => r.json()),

    // Optimistic update
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      // Snapshot previous value
      const previousTodos = queryClient.getQueryData(['todos']);

      // Optimistically update
      queryClient.setQueryData(['todos'], (old) => [
        ...old,
        { ...newTodo, id: 'temp-' + Date.now() },
      ]);

      return { previousTodos };
    },

    // Rollback on error
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context.previousTodos);
      showError('Failed to add todo');
    },

    // Refetch after success
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <div>
      <button onClick={() => addTodo.mutate({ text: 'New Todo' })}>
        Add Todo
      </button>
      {addTodo.isPending && <span>Saving...</span>}
    </div>
  );
}

Delete with Optimistic Update

const deleteTodo = useMutation({
  mutationFn: (id) =>
    fetch(`/api/todos/${id}`, { method: 'DELETE' }),

  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    const previousTodos = queryClient.getQueryData(['todos']);

    queryClient.setQueryData(['todos'], (old) =>
      old.filter((todo) => todo.id !== id)
    );

    return { previousTodos };
  },

  onError: (err, id, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos);
    showError('Failed to delete todo');
  },

  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Considerations

Pros:

  • Instant feedback
  • Feels faster
  • Better UX for predictable operations

Cons:

  • Requires rollback logic
  • Can cause confusion if server fails
  • Not suitable for all operations (payments, emails)

Best Practices:

  • Use for predictable, reversible actions
  • Show saving state
  • Handle errors gracefully with rollback
  • Never use for critical financial operations

Practice Problems

0/3solved
Build Loading States Component

Create a reusable React component implementing Loading States. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Loading States Testing

Write unit and integration tests for Loading States using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Loading States Performance

Optimize Loading States for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. When should you use skeleton screens over spinners?

Question 1 options

2. What is optimistic UI?

Question 2 options

3. Why should you cancel queries before optimistic updates?

Question 3 options

4. When should you NOT use optimistic updates?

Question 4 options

Flashcards

Question

What is a skeleton screen?

Answer

A placeholder UI that mimics the final content shape, shown while loading to improve perceived performance.

Question

When should you use a spinner vs skeleton?

Answer

Spinners for unknown content shape or buttons; skeletons when you know the final layout.

Question

What is rollback in optimistic updates?

Answer

Reverting the UI to the previous state when the server operation fails.

Question

What should you do after an optimistic update succeeds?

Answer

Invalidate the query to refetch the actual server data and ensure consistency.

Question

What is Loading States?

Answer

Loading States is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Skeleton screens improve perceived performance by showing content shape
  • 2.Use spinners for buttons and unknown content shapes
  • 3.Optimistic UI provides instant feedback for predictable operations
  • 4.Always implement rollback for failed optimistic updates
  • 5.Cancel background queries before applying optimistic updates

Interview Tips

  • Explain when to use skeleton screens vs spinners
  • Describe how optimistic updates work with error handling
  • Discuss the trade-offs of optimistic UI

Cheat Sheet

Loading States Cheat Sheet

Skeleton Screen

<div className="skeleton" style={{ width: '100%', height: '20px' }} />

Spinner

<Spinner size="md" />

Optimistic Update

onMutate: async (newData) => {
  await queryClient.cancelQueries({ queryKey });
  const previous = queryClient.getQueryData(queryKey);
  queryClient.setQueryData(queryKey, old => [...old, newData]);
  return { previous };
},
onError: (err, data, context) => {
  queryClient.setQueryData(queryKey, context.previous);
},