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.

0
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!). 🚀💡