Mastering React Data Fetching: A Comprehensive Guide for Developers 🚀
Tags: javascript, react, webdev, programming
Published: 2023-11-28 08:26:43

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.

```jsx
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;
```

**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.

```jsx
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;
```

### **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.

```jsx
// 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;
```

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

```jsx
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;
```

**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.

```jsx
// 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;
```

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

```jsx
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);
```

### **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:

```bash
npm install react-query
```

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

```jsx
// 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')
);
```

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

```jsx
// 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;
```

**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:

```bash
npm install react-query react-query/devtools typescript @types/react-query
```

### **2. Configuration in `index.tsx`**

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

```tsx
// 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')
);
```

### **3. Defining TypeScript Types**

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

```tsx
// types.ts

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

export type User = {
  id: number;
  name: string;
  email: string;
};
```

### **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:

```tsx
// 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);
};
```

### **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:

```tsx
// 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;
```

```tsx
// 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;
```

### **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](https://www.linkedin.com/in/maitrishmukherjee/)