DEV Community

Cover image for Mastering React Data Fetching: A Comprehensive Guide for Developers 🚀
Maitrish Mukherjee
Maitrish Mukherjee

Posted on

Mastering React Data Fetching: A Comprehensive Guide for Developers 🚀

Hey, fellow React devs!

I am hoping this will be yet another all-encompassing article about React data fetching techniques. This will give you a SOLID understanding of data fetching in React.

Trust me, this is the extent of data fetching. Yes, there are a lot of other use-case-specific data fetching that could happen (like fetching paginated data with proper type-casting for a table component, or sequential promises, etc.), but this is the extent. What I mean is there are an infinite number of add-ons that could be used ON TOP of these.

In this comprehensive guide, we'll start from the basics, gradually progressing to advanced techniques, and finally, we'll architect React apps with a focus on separating concerns using React Query. Whether you're just starting or looking to level up your skills, this blog will equip you with the knowledge to architect React applications like a pro.

1. The Fundamentals: Fetch and Axios in useEffect

1.1 Basics with fetch:
Let's kick off with the fundamental approach using the fetch API in a functional component. We'll explore how to fetch data and update the component's state using the useState and useEffect hooks.

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

const BasicFetchExample = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []);

  return (
    <div>
      {data ? (
        // Render data
      ) : (
        // Render loading state or error message
      )}
    </div>
  );
};

export default BasicFetchExample;
Enter fullscreen mode Exit fullscreen mode

1.2 Axios for Enhanced Functionality:
Building upon the basics, let's explore Axios, a popular HTTP client, to handle data fetching. Axios simplifies error handling and provides additional features like request/response interceptors.

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

const AxiosExample = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('https://api.example.com/data');
        setData(response.data);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []);

  return (
    <div>
      {data ? (
        // Render data
      ) : (
        // Render loading state or error message
      )}
    </div>
  );
};

export default AxiosExample;
Enter fullscreen mode Exit fullscreen mode

2. Advanced Separation of Concerns: Custom Hooks and HOCs

2.1 Custom Hooks for Modularity:
Now, let's introduce the concept of custom hooks. We'll create a reusable data fetching hook, encapsulating the logic for fetching and handling data.

// useDataFetching.js

import { useState, useEffect } from 'react';

const useDataFetching = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};

export default useDataFetching;
Enter fullscreen mode Exit fullscreen mode

Now, we can easily use this custom hook in any component:

import React from 'react';
import useDataFetching from './useDataFetching';

const CustomHookExample = () => {
  const { data, loading, error } = useDataFetching('https://api.example.com/data');

  return (
    <div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data && (
        // Render data
      )}
    </div>
  );
};

export default CustomHookExample;
Enter fullscreen mode Exit fullscreen mode

2.2 Higher-Order Components (HOCs) for Reusability:
Another powerful technique for separating concerns is using Higher-Order Components. Let's create a data fetching HOC.

// withDataFetching.js

import React from 'react';

const withDataFetching = (url) => (WrappedComponent) => {
  return (props) => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      const fetchData = async () => {
        try {
          const response = await fetch(url);
          const result = await response.json();
          setData(result);
        } catch (error) {
          setError(error.message);
        } finally {
          setLoading(false);
        }
      };

      fetchData();
    }, [url]);

    return <WrappedComponent data={data} loading={loading} error={error} {...props} />;
  };
};

export default withDataFetching;
Enter fullscreen mode Exit fullscreen mode

Now, we can enhance any component with data fetching capabilities:

import React from 'react';
import withDataFetching from './withDataFetching';

const HOCExample = ({ data, loading, error }) => (
  <div>
    {loading && <p>Loading...</p>}
    {error && <p>Error: {error}</p>}
    {data && (
      // Render data
    )}
  </div>
);

export default withDataFetching('https://api.example.com/data')(HOCExample);
Enter fullscreen mode Exit fullscreen mode

3. Architecting React Apps with React Query

3.1 Introducing React Query:
React Query is a powerful library that simplifies data fetching in React apps. It provides a unified and consistent approach to handling API calls with features like caching, automatic refetching, and more.

3.2 Setting Up React Query:
Start by installing React Query:

npm install react-query
Enter fullscreen mode Exit fullscreen mode

Now, let's configure React Query in our app:

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

3.3 Fetching Data with React Query:
Now, let's see how React Query simplifies data fetching in components:

// ReactQueryExample.js

import React from 'react';
import { useQuery } from 'react-query';

const ReactQueryExample = () => {
  const { data, isLoading, isError } = useQuery('fetchData', async () => {
    const response = await fetch('https://api.example.com/data');
    return response.json();
  });

  return (
    <div>
      {isLoading && <p>Loading...</p>}
      {isError && <p>Error fetching data</p>}
      {data && (
        // Render data
      )}
    </div>
  );
};

export default ReactQueryExample;
Enter fullscreen mode Exit fullscreen mode

Architecting React Apps with TypeScript and React Query

1. Setting the Foundation: Installing Dependencies

Before we begin, make sure you have React Query and TypeScript installed in your project:

npm install react-query react-query/devtools typescript @types/react-query
Enter fullscreen mode Exit fullscreen mode

2. Configuration in index.tsx

In your index.tsx, set up React Query by creating a QueryClient and wrapping your App component:

// index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

3. Defining TypeScript Types

Create a types.ts file to define TypeScript types that will be used throughout the application:

// types.ts

export type ApiResponse<T> = {
  data: T;
  status: number;
};

export type User = {
  id: number;
  name: string;
  email: string;
};
Enter fullscreen mode Exit fullscreen mode

4. Crafting the Custom Hook for API Calls: useApi.ts

Now, let's create a custom hook, useApi, that handles API calls using React Query:

// useApi.ts

import { useQuery, UseQueryOptions } from 'react-query';
import { ApiResponse } from './types';

const BASE_URL = 'https://api.example.com';

const fetchApi = async <T>(path: string): Promise<ApiResponse<T>> => {
  const response = await fetch(`${BASE_URL}${path}`);
  const data = await response.json();

  return { data, status: response.status };
};

export const useApi = <T>(path: string, options?: UseQueryOptions<ApiResponse<T>>) => {
  return useQuery<ApiResponse<T>, Error>(path, () => fetchApi<T>(path), options);
};
Enter fullscreen mode Exit fullscreen mode

5. Using the Custom Hook in Components

Now, let's put our architecture to use by creating two components, UserList and PostList, each using the useApi hook:

// UserList.tsx

import React from 'react';
import { useApi } from './useApi';
import { User } from './types';

const UserList = () => {
  const { data, isLoading, isError } = useApi<User[]>('/users');

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error fetching users</p>;

  return (
    <ul>
      {data?.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

export default UserList;
Enter fullscreen mode Exit fullscreen mode
// PostList.tsx

import React from 'react';
import { useApi } from './useApi';
import { ApiResponse } from './types';

const PostList = () => {
  const { data, isLoading, isError } = useApi<ApiResponse<any>>('/posts');

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error fetching posts</p>;

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

export default PostList;
Enter fullscreen mode Exit fullscreen mode

6. Advantages of this Architecture

Let's briefly discuss the advantages of our architecture:

  • Reusability: The useApi hook promotes code reuse, allowing for consistent and efficient API calls across components.

  • Consistency: Our architecture ensures a uniform approach to handling API calls with React Query, enhancing code predictability.

  • Type Safety: TypeScript types provide robust type checking, reducing the likelihood of runtime errors and improving code reliability.

  • Maintainability: The separation of concerns and modular design enhance code maintainability and readability, crucial for long-term project success.

Conclusion:

You've embarked on a journey from basic data fetching with fetch and Axios to advanced separation of concerns using custom hooks, HOCs, and finally, architecting React apps with React Query. Embrace these techniques, experiment with real-world scenarios, and watch your React applications flourish with maintainability and scalability.

If you think this even gave you an idea about what to expect whether you're starting out in React or even have a few years in your pocket with regards to data-fetching in React, consider liking this article or following for more Frontend tips! Happy coding! 🚀💻

Reach out on Linkedin

Top comments (0)