Intro

There’s no standard way of handling API requests, some do it in the component and some place the functions in utility modules. I am a fan of the latter because it scales better.

My Process

Define types - TS specific

In a folder like src/types/typeOne.ts create a Interface for the returned data type. Example:

// types/User.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

API Utility Modules

Similarly, bundle your API utils in a single folder, something along the lines of src/utils/api/userReq.ts

import { User } from '../types/User';
 
export const fetchUser = async (id: number): Promise<User> => {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('Failed to fetch user');
  }
  return response.json();
};

or this real JS example from a project of mine:

export async function getUserProfile(){
    const response = await fetch(`${SERVER_BASE_URL}/users/profile/`, {
        credentials: "include",
        method: "GET",
        headers: {
            "Content-Type": "application/json",
        },
    });
 
    if (!response.ok) {
        throw new Error("Failed to fetch user profile");
    }
    const info = await response.json();
    return info.data;
}

Using the API Util

In your React component, you can define a function in your component that invokes the util using a try-catch and that function is then used wherever it’s needed. E.g. in your useEffect on initial mount and rendering.

Example:

const checkAdmin = async () => {
    setLoading(true);
    setError(null);
 
    try {
      const response = await fetch("https://api.clickclack.aabuharrus.dev/api/v1/auth/me"
        , {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
        credentials: 'include'
      });
 
      if (!response.ok) {
        throw new Error("Failed to authenticate.");
      }
 
      const authRes = await response.json();
      setIsAdmin(authRes.data.isAdmin);
      setLoggedin(authRes.data.loggedin)
    } catch (error) {
      console.error("Error:", error);
      setError(error.message);
    } finally {
      console.log("You're at home, Harry! 🏠")
      setLoading(false)
    }
  }
  
  useEffect(() => {
    checkAdmin();
  }, []);

Another Process

Some prefer to write their own hooks. So after you’ve defined the utilities, create a folder for your hooks. Example:

// hooks/useUser.ts
import { useEffect, useState } from 'react';
import { fetchUser } from '../api/userApi';
import { User } from '../types/User';
 
export const useUser = (id: number) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    setLoading(true);
    fetchUser(id)
      .then(setUser)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [id]);
 
  return { user, loading, error };
};

// Full disclosure, I've not tried this process before and haven't made custom hooks.