Loading, Error, and Empty States in UI

In this module, we will discuss how to handle common scenarios when building user interfaces with React: loading states, error states, and empty states. These states are essential for providing users with a smooth and informative experience, especially when dealing with asynchronous data.

We will look at best practices and patterns for managing these states in React, and how to improve the user experience by effectively communicating the status of the application.


Table of Contents

  1. Why Handle Loading, Error, and Empty States?
  2. Handling Loading States
  3. Handling Error States
  4. Handling Empty States
  5. Combining Loading, Error, and Empty States
  6. Best Practices for Managing UI States
  7. Conclusion

1. Why Handle Loading, Error, and Empty States?

Handling loading, error, and empty states is crucial for providing a positive user experience in modern web applications. Users expect applications to be responsive, and if they don’t receive feedback when interacting with the app, they may assume that something went wrong.

Here’s why these states are important:

  • Loading states help users understand that data is being fetched and the app is not stuck.
  • Error states inform users of issues and guide them on how to resolve the problem or retry the action.
  • Empty states prevent users from getting confused when no data is available, offering clear next steps.

By providing appropriate feedback for each of these states, you improve the usability, reliability, and trustworthiness of your application.


2. Handling Loading States

The loading state is triggered when an operation, such as an API call or a background task, is in progress. It’s essential to inform the user that the application is fetching or processing data, so they don’t assume the app is frozen.

Example: Basic Loading State

You can use the isLoading or isFetching properties from hooks like useQuery in React Query, useState, or useEffect to manage loading states.

javascriptCopyEditimport React, { useState, useEffect } from 'react';

function DataLoader() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Fetched Data</h1>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default DataLoader;

Advanced Loading State: Skeleton Screens

For a better user experience, instead of displaying a simple “Loading…” message, use skeleton screens. Skeleton screens are placeholders that resemble the content being loaded, making the interface feel more responsive.

javascriptCopyEditimport React from 'react';
import Skeleton from 'react-loading-skeleton';

function SkeletonLoader() {
  return (
    <div>
      <Skeleton height={30} width={300} />
      <Skeleton count={5} height={20} />
    </div>
  );
}

3. Handling Error States

Error states occur when an API call fails, or some unexpected issue happens in your application. Proper error handling helps guide the user to the next step.

Example: Basic Error Handling

You can use a try-catch block in an async function or use the isError property from libraries like React Query to detect errors.

javascriptCopyEditimport React, { useState, useEffect } from 'react';

function ErrorHandlingComponent() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((response) => {
        if (!response.ok) {
          throw new Error('Data fetch failed');
        }
        return response.json();
      })
      .then((data) => setData(data))
      .catch((err) => setError(err.message));
  }, []);

  if (error) return <div>Error: {error}</div>;

  return <div>Data loaded successfully</div>;
}

Advanced Error State: Retry Logic

Sometimes, you might want to allow users to retry the operation if the data fetching fails. This can be done easily with React Query by using the refetch function.

javascriptCopyEditimport { useQuery } from '@tanstack/react-query';

function Posts() {
  const { data, error, isError, isLoading, refetch } = useQuery(
    ['posts'],
    fetchPosts
  );

  if (isLoading) return <div>Loading...</div>;

  if (isError) {
    return (
      <div>
        Error fetching data.
        <button onClick={refetch}>Retry</button>
      </div>
    );
  }

  return <div>{data}</div>;
}

4. Handling Empty States

An empty state occurs when no data is available or when an operation results in an empty set (e.g., search results with no matches). Instead of leaving users in the dark, show them a helpful message or alternative actions.

Example: Basic Empty State

For example, if an API response is empty, you can display a friendly message or provide options for the user.

javascriptCopyEditimport React, { useState, useEffect } from 'react';

function EmptyStateComponent() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((response) => response.json())
      .then((data) => setData(data))
      .catch((err) => console.error(err));
  }, []);

  if (data.length === 0) {
    return <div>No posts available. Try again later!</div>;
  }

  return (
    <div>
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default EmptyStateComponent;

Advanced Empty State: Suggestions or Actions

In some cases, you may want to provide users with additional information or suggestions. For instance, in a search page with no results, you can suggest different queries or offer related content.

javascriptCopyEditfunction EmptyStateWithSuggestions() {
  return (
    <div>
      <h2>No results found</h2>
      <p>Try searching for something else or check out these popular posts:</p>
      <ul>
        <li>Post 1</li>
        <li>Post 2</li>
      </ul>
    </div>
  );
}

5. Combining Loading, Error, and Empty States

To create a seamless user experience, you need to handle loading, error, and empty states together in your components. Here’s how you can combine all three in a single flow:

javascriptCopyEditimport { useQuery } from '@tanstack/react-query';

function CombinedStateComponent() {
  const { data, error, isLoading, isError } = useQuery(['posts'], fetchPosts);

  if (isLoading) return <div>Loading...</div>;

  if (isError) return <div>Error: {error.message}</div>;

  if (data.length === 0) return <div>No posts available.</div>;

  return (
    <div>
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

6. Best Practices for Managing UI States

  • Clear Feedback: Always give users clear and timely feedback regarding the state of the data (e.g., loading spinner, error message).
  • Fallback UI: For all three states, always provide an alternative UI (e.g., skeleton loaders, retry buttons, or empty state messages).
  • Avoid UI Blocking: Make sure the UI remains interactive during loading or error states. Use overlays, spinners, or other indicators instead of blocking the entire interface.
  • Consistent Patterns: Use consistent loading, error, and empty state patterns across your application to maintain a familiar and predictable user experience.

7. Conclusion

In this module, we learned how to handle loading, error, and empty states in React applications. These states are fundamental for building modern, user-friendly applications, especially when dealing with asynchronous data fetching.

By managing these states effectively, you can improve your app’s usability and ensure users always understand the status of the app. Incorporating skeleton loaders, retry logic, and helpful empty state messages can significantly enhance user experience, keeping the interface clean, intuitive, and responsive.