Ivan Kaminskyi

Aug 03, 20247 min

Mastering Data Fetching in React: A Journey from useEffect to Server Components

In modern web development, efficient data fetching is paramount to building responsive and user-friendly applications. This journey explores the evolution of data fetching techniques in React, starting with the foundational useEffect hook and progressing through to the advanced capabilities of React Query and React Server Components (RSC). Each method addresses specific needs, from simple API calls to complex data management and server-side rendering, enhancing performance and user experience. This guide provides a comprehensive understanding and practical examples of mastering data fetching in React, ensuring your applications remain robust and efficient.

useEffect Hook

The useEffect hook is one of the most commonly used hooks in React for handling side effects, like data fetching. When you must fetch data from an API and display it in your component, useEffect is a go-to solution. It allows you to run your code after the component renders, making it perfect for fetching data when it loads.

With useEffect, you define your data-fetching logic inside the hook, which runs after the component has rendered. This approach is straightforward and works well for simple use cases. However, managing multiple API calls, handling caching, and dealing with complex loading states can become cumbersome as your application grows. The useEffect hook provides a lot of flexibility. Still, it requires you to handle most state management and side-effect control.

In the example below, we're building a simple AdminDashboard component that fetches a list of users, some analytics data, and the latest notifications. To achieve this, we use useEffect to trigger our data fetching logic as soon as the component mounts.

Here's how it works:

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const AdminDashboard = () => {
    const [users, setUsers] = useState([]);
    const [analytics, setAnalytics] = useState({});
    const [notifications, setNotifications] = useState([]);

    useEffect(() => {
// Fetch User List
        const fetchUsers = async () => {
            try {
                const response = await axios.get('/api/users');
                setUsers(response.data);
            } catch (error) {
                console.error('Error fetching users:', error);
            }
        };

        // Fetch Analytics Data
        const fetchAnalytics = async () => {
            try {
                const response = await axios.get('/api/analytics');
                setAnalytics(response.data);
            } catch (error) {
                console.error('Error fetching analytics:', error);
            }
        };

        // Fetch Last Notifications
        const fetchNotifications = async () => {
            try {
                const response = await axios.get('/api/notifications');
                setNotifications(response.data);
            } catch (error) {
                console.error('Error fetching notifications:', error);
            }
        };

        fetchUsers();
        fetchAnalytics();
        fetchNotifications();
    }, []);

    return (
        <div className="admin-dashboard">
            <h1>Admin Dashboard</h1>

            <section>
                <h2>User List</h2>
                <ul>
                    {users.map(user => (
                        <li key={user.id}>{user.name}</li>
                    ))}
                </ul>
            </section>

            <section>
                <h2>Analytics</h2>
                <div>
                    <p>Total Users: {analytics.totalUsers}</p>
                    <p>Total Sales: {analytics.totalSales}</p>
                    {/* Add more analytics data as needed */}
                </div>
            </section>

            <section>
                <h2>Last Notifications</h2>
                <ul>
                    {notifications.map(notification => (
                        <li key={notification.id}>{notification.message}</li>
                    ))}
                </ul>
            </section>
        </div>
    );
};

export default AdminDashboard;

In this example, we use three separate functions within useEffect to fetch different sets of data: users, analytics, and notifications. We make these API calls using Axios and store the results in state variables like users, analytics, and notifications. The data is then rendered in corresponding sections of our dashboard.


React Query

While useEffect gets the job done for basic data fetching, when your app starts growing, and you need more advanced features like caching, automatic refetching, and background updates, React Query is a game-changer. React Query is like a supercharged data-fetching tool that simplifies the process and boosts performance and user experience.

With React Query, you can effortlessly manage data fetching, caching, background updates, and synchronization. It handles loading and error states out of the box. Its caching mechanism ensures that your application is performant and responsive. React Query is particularly useful in scenarios where you have complex data dependencies, need real-time updates, or want to reduce the load on your server by reusing cached data.

Instead of relying on useEffect, React Query introduces hooks like useQuery and useQueries that simplify data fetching and allow you to focus more on building your UI than managing API calls.

Let's take our previous AdminDashboard component, where we fetched users, analytics, and notifications using useEffect and refactor it using React Query. Here's how it looks after the transformation:

import React from 'react';
import { useQueries } from 'react-query';
import axios from 'axios';

const fetchUsers = async () => {
    const response = await axios.get('/api/users');
    return response.data;
};

const fetchAnalytics = async () => {
    const response = await axios.get('/api/analytics');
    return response.data;
};

const fetchNotifications = async () => {
    const response = await axios.get('/api/notifications');
    return response.data;
};

const AdminDashboard = () => {
    const queryResults = useQueries([
        {
            queryKey: 'users',
            queryFn: fetchUsers,
        },
        {
            queryKey: 'analytics',
            queryFn: fetchAnalytics,
        },
        {
            queryKey: 'notifications',
            queryFn: fetchNotifications,
        }
    ]);

    const [usersQuery, analyticsQuery, notificationsQuery] = queryResults;

    if (usersQuery.isLoading || analyticsQuery.isLoading || notificationsQuery.isLoading) {
        return <div>Loading...</div>;
    }

    if (usersQuery.isError || analyticsQuery.isError || notificationsQuery.isError) {
        return <div>Error loading data</div>;
    }

    const users = usersQuery.data;
    const analytics = analyticsQuery.data;
    const notifications = notificationsQuery.data;

    return (
        <div className="admin-dashboard">
            <h1>Admin Dashboard</h1>

            <section>
                <h2>User List</h2>
                <ul>
                    {users.map(user => (
                        <li key={user.id}>{user.name}</li>
                    ))}
                </ul>
            </section>

            <section>
                <h2>Analytics</h2>
                <div>
                    <p>Total Users: {analytics.totalUsers}</p>
                    <p>Total Sales: {analytics.totalSales}</p>
                    {/* Add more analytics data as needed */}
                </div>
            </section>

            <section>
                <h2>Last Notifications</h2>
                <ul>
                    {notifications.map(notification => (
                        <li key={notification.id}>{notification.message}</li>
                    ))}
                </ul>
            </section>
        </div>
    );
};

export default AdminDashboard;

We swapped out useEffect for React Query's useQueries hook in this refactor. This change lets us fetch all three data sets (users, analytics, and notifications) simultaneously and handle the results much cleaner. Each query is defined with a queryKey (used by React Query for caching and tracking) and a queryFn, just our async function making the API call.


React Server Components (RSC)

React Server Components (RSC) are a newer addition to the React ecosystem. They bring a whole new approach to data fetching by moving much of the heavy lifting to the server. Unlike traditional React components that run entirely on the client, Server Components allow you to fetch data, render the component, and send the fully rendered HTML to the client—all from the server. This means less JavaScript on the client, faster load times, and a smoother user experience.

RSC shifts the burden of data fetching to the server, reducing the amount of JavaScript sent to the client and improving performance, especially on slower devices or networks. This approach is particularly beneficial for SEO and initial load times, as the content is server-rendered and immediately available to users and search engines.

Let's refactor our AdminDashboard component, previously using React Query, into a Server Component. Here's what the code looks like after the transformation:

import React from 'react';
import axios from 'axios';

// Fetch data on the server
const fetchUsers = async () => {
    const response = await axios.get('/api/users');
    return response.data;
};

const fetchAnalytics = async () => {
    const response = await axios.get('/api/analytics');
    return response.data;
};

const fetchNotifications = async () => {
    const response = await axios.get('/api/notifications');
    return response.data;
};

const AdminDashboard = async () => {
    const [users, analytics, notifications] = await Promise.all([
        fetchUsers(),
        fetchAnalytics(),
        fetchNotifications()
    ]);

    return (
        <div className="admin-dashboard">
            <h1>Admin Dashboard</h1>

            <section>
                <h2>User List</h2>
                <ul>
                    {users.map(user => (
                        <li key={user.id}>{user.name}</li>
                    ))}
                </ul>
            </section>

            <section>
                <h2>Analytics</h2>
                <div>
                    <p>Total Users: {analytics.totalUsers}</p>
                    <p>Total Sales: {analytics.totalSales}</p>
                    {/* Add more analytics data as needed */}
                </div>
            </section>

            <section>
                <h2>Last Notifications</h2>
                <ul>
                    {notifications.map(notification => (
                        <li key={notification.id}>{notification.message}</li>
                    ))}
                </ul>
            </section>
        </div>
    );
};

export default AdminDashboard;

We've moved all our data fetching to the server in this refactor. When AdminDashboard is requested, it runs the data fetching logic for users, analytics, and notifications on the server using axios. The component waits for all the data to be fetched with Promise.all, ensuring everything is ready before it starts rendering. Once the data is fetched, the server renders the HTML and sends it directly to the client.

Tags:
React
Share:

Related Posts

Get The Latest Insights straight To Your Inbox For Free!

Subscribe To The NewsletterSubscribe To The NewsletterSubscribe To The Newsletter