Data Fetching in React at Scale
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
- React Query (opens in a new tab): a data fetching library that provides a simple and concise API for fetching, caching, synchronizing, and updating server state in React.
- SWR (Stale-While-Revalidate) (opens in a new tab): a lightweight library that provides simple and efficient data fetching and caching, with automatic revalidation and stale data usage.
Example: useUsers
hook using React Query + Axios
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
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
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.
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.