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!