How To Display Loading States In React.js

Loading states are a crucial aspect of user experience in React.js applications, especially when dealing with asynchronous operations such as data fetching.

Displaying a loading indicator informs users that something is happening behind the scenes and enhances the overall usability of your application.

In this guide, we'll explore different techniques for implementing loading states in React.js components.

Table of Contents #

  1. Introduction
  2. Basic Loading State with State Variable
  3. Loading Spinner Component
  4. Higher-Order Component (HOC) for Loading States
  5. Using React Suspense
  6. Conclusion

Basic Loading State with State Variable

A common approach is to use a state variable to track whether data is currently being loaded. Let's look at a simple example:

import { useState, useEffect } from 'react';

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

    useEffect(() => {
        const fetchData = async () => {
            try {
                // Simulating an asynchronous operation, like fetching data from an API
                await new Promise((resolve) => setTimeout(resolve, 2000));
                setData('Fetched data');
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                // Set loading to false when the operation is complete
                setLoading(false);
            }
        };

        fetchData();
    }, []); // Empty dependency array means the effect runs once after the initial render

    return <div>{loading ? <p>Loading...</p> : <p>Data: {data}</p>}</div>;
};

export default MyComponent;

In this example:

Loading Spinner Component

To enhance the user experience, you can replace the simple text-based loading indicator with a loading spinner component.

Here's an example using a popular library called react-spinners:

npm install react-spinners
import { useState, useEffect } from 'react';
import { RingLoader } from 'react-spinners';

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

    useEffect(() => {
        const fetchData = async () => {
            try {
                // Simulating an asynchronous operation
                await new Promise((resolve) => setTimeout(resolve, 2000));
                setData('Fetched data');
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                setLoading(false);
            }
        };

        fetchData();
    }, []);

    return (
        <div>
            {loading ? (
                <div
                    style={{
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        height: '100vh',
                    }}
                >
                    <RingLoader loading={loading} color="#36D7B7" size={150} />
                </div>
            ) : (
                <p>Data: {data}</p>
            )}
        </div>
    );
};

export default MyComponent;

In this example:

Higher-Order Component (HOC) for Loading States

To abstract the loading state logic and make it reusable across components, you can create a higher-order component (HOC). Here's a basic example:

import { useState, useEffect } from 'react';
import { RingLoader } from 'react-spinners';

const withLoading = (WrappedComponent) => {
    return (props) => {
        const [loading, setLoading] = useState(true);

        useEffect(() => {
            const fetchData = async () => {
                try {
                    // Simulating an asynchronous operation
                    await new Promise((resolve) => setTimeout(resolve, 2000));
                } catch (error) {
                    console.error('Error fetching data:', error);
                } finally {
                    setLoading(false);
                }
            };

            fetchData();
        }, []);

        return (
            <div>
                {loading ? (
                    <div
                        style={{
                            display: 'flex',
                            justifyContent: 'center',
                            alignItems: 'center',
                            height: '100vh',
                        }}
                    >
                        <RingLoader
                            loading={loading}
                            color="#36D7B7"
                            size={150}
                        />
                    </div>
                ) : (
                    <WrappedComponent {...props} />
                )}
            </div>
        );
    };
};

const MyComponent = () => {
    return (
        <div>
            <p>Data: Fetched data</p>
        </div>
    );
};

export default withLoading(MyComponent);

In this example:

Using React Suspense

React Suspense is a feature that allows components to suspend rendering while waiting for something, such as data to be loaded.

While it's typically associated with the new React concurrent mode and server-rendered applications, you can use it in simpler scenarios as well.

import { Suspense } from 'react';

const MyComponent = () => {
    // Simulating data fetching using a promise
    const fetchData = () =>
        new Promise((resolve) =>
            setTimeout(() => resolve('Fetched data'), 2000)
        );

    // Wrap the component with Suspense and specify a fallback UI
    return (
        <Suspense fallback={<p>Loading...</p>}>
            <DataComponent fetchData={fetchData} />
        </Suspense>
    );
};

const DataComponent = ({ fetchData }) => {
    const data = fetchData();
    return <p>Data: {data}</p>;
};

export default MyComponent;

In this example:

Note: React Suspense is most effective when used in combination with asynchronous rendering features, such as those provided by concurrent mode.

Conclusion

Implementing loading states in React.js is essential for creating a smooth and responsive user experience.

Whether using a basic state variable, incorporating a loading spinner, or abstracting the logic into a higher-order component, the goal is to keep users informed about ongoing operations.

Choose the approach that best fits your application's structure and requirements, and consider using libraries like react-spinners or features like React Suspense for more advanced scenarios.

With proper loading state management, you can enhance the perceived performance of your React applications and provide users with a more engaging and user-friendly interface.