Introduction
Efficient API interaction is crucial for building responsive and data-driven applications in modern web development. This blog post explores a set of utility functions designed to simplify and streamline API calls in React TypeScript projects. We’ll cover functions for URL construction, GET and POST requests, and introduce error handling and request cancellation techniques.
The Core Utility Functions
Let’s examine the key functions that form the backbone of our API interaction toolkit:
1. buildURL: Constructing Dynamic URLs
import queryString from "query-string";
export function buildURL(
url: string,
params?: Record<string, any> | string
): string {
if (!params) return url;
const separator = url.includes("?") ? "&" : "?";
const queryParams =
typeof params === "object" ? queryString.stringify(params) : params;
return `${url}${separator}${queryParams}`;
}
This function dynamically constructs URLs by appending query parameters. It’s versatile, handling both object and string parameters, making it invaluable for creating complex API endpoints.
2. fetchData: Simple GET Requests
import axios, { AxiosInstance } from "axios";
const axiosInstance: AxiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL,
});
export async function fetchData<T>(
url: string,
params?: Record<string, any> | string
): Promise<T> {
try {
const response = await axiosInstance.get<T>(buildURL(url, params));
return response.data;
} catch (error) {
console.error("Error in fetchData:", error);
throw error;
}
}
fetchData
provides a straightforward way to make GET requests. It utilizes the buildURL
function and a configured Axios instance, simplifying basic data fetching operations.
3. fetchPaginatedData: Advanced GET Requests with Pagination
export async function fetchPaginatedData<T>({
url,
params = {},
}: {
url: string;
params?: { page?: number; pageSize?: number; [key: string]: any };
}): Promise<T> {
const { page = 0, pageSize = 10, ...rest } = params;
const fullUrl = buildURL(url, { page, pageSize, ...rest });
try {
const response = await axiosInstance.get<T>(fullUrl);
return response.data;
} catch (error) {
console.error("Error in fetchPaginatedData:", error);
throw error;
}
}
fetchPaginatedData
extends fetchData
’s functionality by adding built-in support for pagination. It’s ideal for scenarios where you’re dealing with large datasets that require page-by-page loading.
4. postData: Handling POST Requests
export async function postData<T>({
url,
data,
}: {
url: string;
data?: Record<string, any> | string;
}): Promise<T> {
try {
const response = await axiosInstance.post<T>(url, data);
return response.data;
} catch (error) {
console.error("Error in postData:", error);
throw error;
}
}
The postData
function simplifies making POST requests, handling data submission to your API endpoints.
Enhanced Error Handling and Request Cancellation
To make our utility functions more robust, let’s add error handling and request cancellation capabilities:
5. createCancelToken: Generate Cancellation Tokens
import axios, { CancelTokenSource } from "axios";
export function createCancelToken(): CancelTokenSource {
return axios.CancelToken.source();
}
This function creates a cancel token, which can be used to abort requests if needed.
6. withCancellation: Wrapper for Cancellable Requests
export async function withCancellation<T>(
promise: Promise<T>,
cancelToken: CancelTokenSource
): Promise<T> {
try {
return await promise;
} catch (error) {
if (axios.isCancel(error)) {
console.log("Request canceled:", error.message);
}
throw error;
}
}
withCancellation
wraps a promise with cancellation logic, allowing you to handle cancelled requests gracefully.
Putting It All Together
Here’s an example of how these functions can be used in a React TypeScript component:
import React, { useState, useEffect } from "react";
import {
fetchPaginatedData,
postData,
createCancelToken,
withCancellation,
} from "../utils/api";
interface User {
id: number;
name: string;
}
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [cancelToken, setCancelToken] = useState(createCancelToken());
useEffect(() => {
const fetchUsers = async () => {
try {
const data = await withCancellation(
fetchPaginatedData<{ users: User[] }>({
url: "/users",
params: { page: 1, pageSize: 10 },
}),
cancelToken
);
setUsers(data.users);
} catch (error) {
console.error("Failed to fetch users:", error);
}
};
fetchUsers();
return () => {
cancelToken.cancel("Component unmounted");
};
}, []);
const addUser = async (userData: Omit<User, "id">) => {
try {
const newUser = await postData<User>({ url: "/users", data: userData });
setUsers((prevUsers) => [...prevUsers, newUser]);
} catch (error) {
console.error("Failed to add user:", error);
}
};
const cancelRequest = () => {
cancelToken.cancel("Operation canceled by the user.");
setCancelToken(createCancelToken()); // Create a new token for future requests
};
return (
<div>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<button onClick={cancelRequest}>Cancel Request</button>
</div>
);
};
export default UserList;
Conclusion
These utility functions provide a solid foundation for managing API interactions in your React TypeScript applications. By encapsulating common patterns and adding robust error handling and cancellation capabilities, you can significantly streamline your data fetching logic and improve the overall reliability of your application.