The Power of Generic Hooks in React Data Fetching

Hello! Welcome back! In my previous post, we created a custom hook, like 'useUsers', to fetch data from a specific endpoint, say '/users'. But what if we need data from different endpoints like '/posts'? We'd end up writing similar custom hooks, introducing redundancy.

Generic Hooks to the Rescue: Enter the concept of generic hooks, offering a more streamlined solution. Let's transition from our specific 'useUsers' custom hook to a more versatile useData generic hook.

Before: Custom Hook for 'useUsers'

(from the previous post)

//src\hooks\useUsers.ts
import { useEffect, useState } from 'react';
import apiClient from '../services/api-client';
import { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}

const useUsers = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState('');
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    apiClient
      .get<User[]>('/users', { signal: controller.signal })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message)
        setLoading(false);
      });
  return () => controller.abort();
  }, []);

  return  { users, error, isLoading };
};

export default useUsers;

After: Generic Hook 'useData'

This versatile hook fetches data from any endpoint with minimal code duplication. It's like having a universal tool for fetching data, making your codebase clean and consistent.

//src\hooks\useData.ts
// Example usage:
// const { data, error, isLoading } = useData<MyDataType>('/api/dataEndpoint');

import { useEffect, useState } from 'react';
import apiClient from '../services/api-client';
import { CanceledError } from 'axios';

const useData = <T>(endpoint: string) => {
  const [data, setData] = useState<T[]>([]);
  const [error, setError] = useState('');
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    apiClient
      .get<T[]>(endpoint, { signal: controller.signal })
      .then((res) => {
        setData(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => controller.abort();
  }, []);

  return { data, error, isLoading };
};

export default useData;

Understanding the Transition:

  • Type Parameter<T>: The 'useData' hook employs a type parameter <T>, allowing it to adapt to various data structures.

  • Endpoint Flexibility: 'useData' is designed for any endpoint specified during its usage, offering adaptability.

  • Reduced Redundancy: 'useData' eliminates redundancy by serving as a universal tool for fetching data, reducing the need for separate hooks.

Using Generic Hooks

We replace our specific hooks like useUsers with the generic one, useData. The magic happens when we specify the data type we expect, like useData<User>('/users'). This adapts the generic hook to fetch user-specific data, eliminating redundant code.

//src\hooks\useUsers.ts

import useData from './useData';

interface User {
  id: number;
  name: string;
}

const useUsers = () => useData<User>('/users');

export default useUsers;

Simplified Component Usage: Implementing these hooks in components becomes a breeze. For example, in the Users component, we use useUsers, and the code stays clean and concise.

//src\components\Users.tsx

import useUsers from '../hooks/useUsers';

const Users = () => {
  const { data, error, isLoading } = useUsers();
  return (
   // ... JSX structure
  );
};
export default Users;

Customization for Your Needs

Every project is unique. We can customize the generic hook based on specific requirements.

Adapting to Specific Structures: Sometimes, APIs have unique response structures. No worries! We can tweak our generic hook to accommodate specific needs, like the useDataFetchResponse hook. It handles structures like { count: number, results: T[] }, making it adaptable to diverse API scenarios.

//src\hooks\useData.ts

import { CanceledError } from 'axios';
import { useEffect, useState } from 'react';
import apiClient from '../services/api-client';

// Response structure for useData
interface FetchResponse<T> {
  count: number;
  results: T[];
}

const useData = <T>(endpoint: string) => {
  const [data, setData] = useState<T[]>([]);
  const [error, setError] = useState('');
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    apiClient
      .get<FetchResponse<T>>(endpoint, { signal: controller.signal })
      .then((res) => {
        setData(res.data.results); // Extracting results property
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });

    return () => controller.abort();
  }, []);

  return { data, error, isLoading };
};

export default useData;

Conclusion

To sum it up, using generic hooks is a big win. It makes code reusable and easy to maintain, helping developers build scalable applications effortlessly. By keeping data-fetching logic in one place, errors are reduced, and a strong foundation for a solid codebase is laid. In the fast-paced world of web development, mastering concepts like generic hooks empowers us to create efficient and easy-to-manage React applications. Happy coding!