Handbook
Data Fetching

Data Fetching in React at Scale

Last updated by Noel Varanda (opens in a new tab),
React
Data fetching
Scalability
REST
tRPC
GraphQL
React Query
SWR
Apollo Client
Relay

As React apps scale in complexity, the need to fetch data from the server becomes increasingly important. This guide aims to provide best practices for implementing data fetching in React.

Preview and sample code

For a minimal react app setup with all data fetching implementations mentioned here then have a look at all the code here (opens in a new tab).

Business challenges

Implementing an effective data fetching strategy involves considering various business factors. These factors help in making informed decisions about the technologies to use. Key factors to consider include:

  • Ownership of the API: Does the same team own both the frontend and the API?
  • Number of Datastores and Microservices: Evaluate the complexity of the API, including the number of datastores and microservices it consumes.
  • Development Team Size: Consider the number of developers working on the project.
  • Project Longevity: Determine whether the project is short-term or long-term in nature.

Requirements

Server state presents unique challenges and requirements. Effective management of server state involves addressing specific needs, such as:

  • Caching: Implementing mechanisms to optimize server state retrieval and minimize network requests.

  • Optimistic updates: Allowing clients to provide immediate feedback, even before server confirmation.

  • Deduping requests: Avoiding redundant requests to the server by eliminating duplicate requests.

  • Background updates: Updating server state in the background to keep it synchronized with latest changes.

  • Mutations: Handling mutations or modifications to server state in a consistent and controlled manner.

  • Pagination: Implementing pagination strategies to efficiently fetch and display large datasets from the server.

Abstract logic in hooks

Firstly, when you abstract the logic that deals with fetching and managing server state into custom hooks, you can encapsulate the complexity and give components a clean interface to interact with the server state, no matter which client you're using (like React Query or Apollo Client). For example:

export const Products = () => {
  const { products, isLoading, error } = useProducts();
 
  if (isLoading) {
    return <div>Loading...</div>;
  }
 
  if (error) {
    return <div>Error: {error.message}</div>;
  }
 
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.title}</li>
      ))}
    </ul>
  );
};

REST

REST (Representational State Transfer) is a widely-used architectural style for building APIs. It follows a stateless, client-server communication model where data is fetched using HTTP methods (GET, POST, PUT, DELETE) and represented in formats such as JSON or XML.

Pros and Cons

Pros

  • Widely adopted in the industry.
  • Simple to understand and implement.
  • Uses HTTP caching mechanisms.
  • Good fit for CRUD operations.

Cons

  • May lead to over- or under-fetching of data.

  • Requires multiple API requests for fetching related data.

  • Lack of strong typing and validation.

When to use REST

  • Simple requirements.
  • Familiar with REST principles.
  • Frontend/backend separation.
  • Existing RESTful APIs.

Recommended libraries

For fetching data

  • Axios (opens in a new tab): a promise-based HTTP client for the browser and Node.js. It provides an easy-to-use API for making HTTP requests.

For linking to state

Example: useUsers hook using React Query + Axios
./domains/users/hooks/rest/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import axios, { AxiosResponse } from 'axios';
 
export const useUsers = () => {
  const { data, isLoading, error } = useQuery<
    AxiosResponse<{ id: string; name: string; editableField: string }[]>,
    { message: string }
  >({
    queryKey: ['users'],
    queryFn: () => axios.get('/api/users'),
  });
 
  return { data: data?.data, isLoading, error };
};

GraphQL

GraphQL is a query language for APIs and a runtime for executing those queries with your existing data. It provides a flexible and efficient approach to data fetching, allowing clients to request specific data requirements.

Pros and cons

Pros

  • Efficient data fetching with selective fields.

  • Strong typing and validation with a well-defined GraphQL schema.

  • Real-time updates with subscriptions for reactive data fetching.

  • Rich ecosystem and community support.

Cons

  • Requires a learning curve for setup and usage.

  • Increased complexity in backend implementation compared to REST.

  • May result in over-fetching of data if not properly managed.

  • A lot of steps required to set up TypeScript generation support.

When to use GraphQL

  • Complex data requirements.
  • Need for flexibility in data fetching.
  • Advanced data manipulation and composition.
  • API connects to multiple services.
  • Need for real-time updates or subscriptions.

Recommended libraries

  • Apollo Client (opens in a new tab): a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.
  • Relay (opens in a new tab): a GraphQL-specific state management library with a focus on performance and efficiency, providing automatic batching and caching optimizations.
Example: useUsers hook using Apollo Client
./domains/users/hooks/graphql/useUsers.ts
import { useQuery, gql } from '@apollo/client';
 
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      editableField
    }
  }
`;
 
export const useUsers = () => {
  /**
   * For demonstration puposes, we're manually typing this
   * In a real life Apollo client app, these types are generated.
   *
   * Read the article below:
   * https://www.apollographql.com/docs/react/development-testing/static-typing/
   */
  const {
    data,
    loading: isLoading,
    error,
  } = useQuery<{
    users: { name: string; id: string; editableField: string }[];
  }>(GET_USERS);
 
  return { data: data?.users, isLoading, error };
};

tRPC

tRPC is a framework-agnostic library that simplifies communication between the client and server. It provides a type-safe and efficient way to fetch data in React.

Pros and cons

Type-safe API definitions: tRPC leverages TypeScript to ensure type safety throughout the communication between client and server. Efficient data fetching: tRPC supports batching and caching to minimize unnecessary network requests and improve performance. Lightweight and framework-agnostic: You can use tRPC with any backend framework of your choice, providing flexibility in your tech stack.

Pros

  • Automatic code generation.

  • Provides type-safe API definitions.

  • Efficient data fetching with both batching and caching mechanisms.

  • Lightweight and easy to set up in React projects.

Cons

  • Smaller ecosystem compared to GraphQL.

  • Requires learning curve for setup and usage.

When to Use tRPC

  • Full stack TypeScript projects
  • Need for type-safe APIs
  • Need for efficient data fetching
  • Simplicity and ease of use

Recommended stack

tRPC uses React Query under the hood, meaning we can leverage the same benefits of React Query in our tRPC projects.

  • tRPC (opens in a new tab): tRPC is a framework-agnostic library that simplifies communication between the client and server. It provides a type-safe and efficient way to fetch data in React.
Example: useUsers hook using tRPC
./domains/users/hooks/trpc/useUsers.ts
import { trpc } from '@/domains/trpc';
 
export const useUsers = () => {
  return trpc.user.list.useQuery();
};

Integrating with state management

Read the state management guide to understand how to solve state management.

As mentioned in the state management guide, there are two types of state to manage:

  • Client state: State that is only relevant to the client and does not need to be persisted.
  • Server state: State that is relevant to the server and needs to be persisted.

In this case, we focus on the server state.

We have also learned the importance of abstracting logic in hooks. By following this practice, we can take the example hooks mentioned above and seamlessly integrate them into our React code.

In the following example, we use the tRPC hook, but you can easily switch it out for any of the other hooks mentioned.

./domains/users/pages/UsersPage.tsx
import { useUsers } from '../hooks/trpc/useUsers';
// import { useUsers } from '../hooks/rest/useUsers';
// import { useUsers } from '../hooks/graphql/useUsers';
 
export const UsersPage = () => {
  const { data: users, isLoading, error } = useUsers();
 
  if (isLoading) return <div>Loading...</div>;
 
  if (error) return <div>{error.message}</div>;
 
  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users?.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

Keep up to date with any latest changes or announcements by subscribing to the newsletter below.