Efficient API Consumption in React TypeScript
APIs are playing an important role in software development today. In the world of cloud computing, no application can function without APIs. The way you architect and consume these APIs can significantly impact your app's performance.
Let’s explore building a robust HTTP Client in React TypeScript, integrating caching with Tanstack Query (formerly React Query), and creating custom hooks that streamline your API calls.
🌐 Understanding HTTP APIs
HTTP (HyperText Transfer Protocol) is the universal language systems use to communicate. Whether it’s fetching data, updating a resource, or deleting something from the server, HTTP has got us covered. Here’s a quick refresher on the HTTP methods:
GET: Grabs the data you need from the server.
POST: Sends data to create something new on the server.
PUT: Updates an existing resource entirely.
PATCH: Updates a part of an existing resource.
DELETE: Removes something from the server.
Each method is like a tool in your toolbox—pick the right one for the job, and your APIs will be a joy to work with.
💡 Why Axios?
So, why are we using Axios for our HTTP Client? Imagine having a personal assistant who handles all the boring tasks like adding headers, dealing with JSON, and managing timeouts—Axios does just that! It’s a library that simplifies HTTP requests, making our code cleaner and easier to maintain.
🛠️ Creating the HTTP Client
Step 1: Installing Axios
Before we dive into the fun stuff, let’s install Axios:
npm install axios
Step 2: Setting Up the HTTP Client
Now, let’s set up our HTTP Client to make API calls a breeze.
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
class HttpClient {
private instance: AxiosInstance;
constructor(baseURL: string) {
this.instance = axios.create({
baseURL,
withCredentials: true,
});
this.instance.interceptors.request.use(this.handleRequest);
this.instance.interceptors.response.use(this.handleResponse);
}
private handleRequest = (config: AxiosRequestConfig): AxiosRequestConfig => {
const token = localStorage.getItem("authToken"); // Replace with your token logic
if (token) {
if (!config.headers) {
config.headers = {
Authorization: `Bearer ${token}`,
};
return config;
}
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
};
private handleResponse = (response: AxiosResponse): AxiosResponse => {
return response;
};
public get<T>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.instance.get<T>(url, config);
}
public post<T>(
url: string,
data: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.instance.post<T>(url, data, config);
}
public put<T>(
url: string,
data: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.instance.put<T>(url, data, config);
}
public patch<T>(
url: string,
data: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.instance.patch<T>(url, data, config);
}
public delete<T>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.instance.delete<T>(url, config);
}
}
const httpClient = new HttpClient("https://jsonplaceholder.typicode.com");
export default httpClient;
Create a User Model
export interface Address {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
}
export interface Company {
name: string;
catchPhrase: string;
bs: string;
}
export interface User {
id: number;
name: string;
username: string;
email: string;
address: Address;
phone: string;
website: string;
company: Company;
}
🔄 Handling API Requests
Creating a User
const createUser = async (userData: UserData) => {
const response = await httpClient.post<User>('/users', userData);
return response.data;
};
Fetching Users
const fetchUsers = async () => {
const response = await httpClient.get<User[]>('/users');
return response.data;
};
Updating a User
const updateUser = async (userId: string, userData: UserData) => {
const response = await httpClient.put<User>(`/users/${userId}`, userData);
return response.data;
};
Deleting a User
const deleteUser = async (userId: string) => {
const response = await httpClient.delete(`/users/${userId}`);
return response.data;
};
🗄️ The Power of Caching with Tanstack Query
When consuming APIs, one of the biggest challenges is managing the state of the data—especially when it comes to caching. Caching allows us to store responses so that repeated requests for the same data can be served faster, improving the overall performance of our application. This is where Tanstack Query comes in. Tanstack Query is a powerful data-fetching library that makes it simple to manage server-state in your React apps, with built-in caching, synchronization, and more.
Installation
To get started, install Tanstack Query:
npm install @tanstack/react-query
⚙️ Creating Custom Hooks for API Requests
Creating custom hooks makes your API calls reusable and easier to manage. For each type of request (fetch, create, update, delete), we'll create a custom hook using Tanstack Query and the HttpClient we built earlier.
Setting Up Query Keys
First, let's define some constants for our query keys:
// queryKeys.ts
export const QUERY_KEYS = {
USERS: 'users',
USER: (userId: string) => ['user', userId],
};
Custom Hooks Examples
Fetching Users
import { useQuery } from '@tanstack/react-query';
import httpClient from '../api/HttpClient';
import { QUERY_KEYS } from '../api/queryKeys';
import { User } from '../api/interfaces';
const useFetchUsers = () => {
return useQuery<User[]>({
queryKey: [QUERY_KEYS.USERS],
queryFn: () => httpClient.get<User[]>("/users").then((res) => res.data),
});
};
Creating a User
import { useMutation, useQueryClient } from '@tanstack/react-query';
import httpClient from '../api/HttpClient';
import { QUERY_KEYS } from '../api/queryKeys';
import { User } from '../api/interfaces';
const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: Omit<User, "id">) =>
httpClient.post<User>("/users", newUser),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USERS] });
},
});
};
Updating a User
import { useMutation, useQueryClient } from '@tanstack/react-query';
import httpClient from '../api/HttpClient';
import { QUERY_KEYS } from '../api/queryKeys';
import { User } from '../api/interfaces';
const useUpdateUser = (userId: number) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (updatedUser: Partial<Omit<User, "id">>) =>
httpClient.put<User>(`/users/${userId}`, updatedUser),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USERS] });
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.USER(userId.toString())],
});
},
});
};
Deleting a User
import { useMutation, useQueryClient } from '@tanstack/react-query';
import httpClient from '../api/HttpClient';
import { QUERY_KEYS } from '../api/queryKeys';
const useDeleteUser = (userId: number) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => httpClient.delete(`/users/${userId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.USERS] });
},
});
};
Consumption of Hooks!
Here's a sample of how you can consume those hooks. We're using jsonplaceholder for mock APIs. You can see it in HttpClient file.
const App: React.FC = () => {
const { data: users } = useFetchUsers();
const createUser = useCreateUser();
const updateUser = useUpdateUser(1); // Example user ID
const deleteUser = useDeleteUser(1); // Example user ID
return (
<div>
<h1>Users</h1>
<button onClick={() => createUser.mutate(newUser)}>Create User</button>
<button
onClick={() =>
updateUser.mutate({
name: "Updated User",
})
}
>
Update User
</button>
<button onClick={() => deleteUser.mutate()}>Delete User</button>
<ul>
{users?.map((user: User) => (
<li key={user.id}>
{user.id} - {user.name}
</li>
))}
</ul>
</div>
);
};
📦 Wrapping It Up
Efficiently consuming APIs is crucial for building performant and scalable applications. By setting up a robust HTTP Client with Axios, handling state with Tanstack Query, and creating custom hooks for each API request, you’ll be well on your way to mastering API consumption in React TypeScript.
Remember, the key is to keep your code modular and reusable. As your application grows, these practices will help you maintain a clean and efficient codebase.
Subscribe to my newsletter
Read articles from Navayuvan Subramanian directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Navayuvan Subramanian
Navayuvan Subramanian
👋 Hey there! I’m a Full Stack Developer with 3+ years of experience building top-notch web and mobile apps. I’m here to help you craft the best app for your product using tech stacks like MERN, Flutter, React Native, Django, and more. 🚀 Currently on a mission to build an app that ensures you never forget a crucial task (believe me, it’s a total game changer!). 🚀💡