I’m working on a movie discovery project where I need to fetch data from two APIs (Tmdb movie site) with different response structures (movies and genres endpoints). To avoid duplicating the same logic on every hook that I use to fetch data from the API, I know I can utilize generics but unfortunately when I try I get the error undefined.
Current genres hook
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
interface Genre {
id: number;
name: string;
}
interface FetchGenreResponse {
genres: Genre[];
}
const useGenres = () => {
const [genres, setGenres] = useState<Genre[]>([]);
const [error, setError] = useState();
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
const controller = new AbortController();
apiClient
.get<FetchGenreResponse>("/genre/movie/list", {
signal: controller.signal,
})
.then((res) => {
setGenres(res.data.genres);
setLoading(false);
})
.catch((err) => {
if (err instanceof CanceledError) return;
setLoading(false);
setError(err);
});
return () => controller.abort();
}, []);
return { genres, error, isLoading };
};
export default useGenres;
Current movies hook
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
interface FetchMoviesResponse {
total_results: number;
results: Movie[];
}
export interface Movie {
id: number;
title: string;
backdrop_path: string;
vote_average: number;
}
const useMovies = () => {
const [movies, setMovies] = useState<Movie[]>([]);
const [error, setError] = useState();
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
const controller = new AbortController();
apiClient
.get<FetchMoviesResponse>("/discover/movie", {
signal: controller.signal,
})
.then((res) => {
setMovies(res.data.results);
setLoading(false);
})
.catch((err) => {
if (err instanceof CanceledError) return;
setLoading(false);
setError(err);
});
return () => controller.abort();
}, []);
return { movies, error, isLoading };
};
export default useMovies;
But as you can see I’ve duplicated the same logic which is not a good practice.
I tried to extract a generic data-fetching hook to reduce redundancy, but it only works for the movies endpoint and returns undefined for genres. I believe this is because the generic hook expects a results array, which is present in the movies response but not in the genres response. The genres response uses a genres array instead.
My attempted solution
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
interface FetchResponse<T> {
results: T[];
}
const useData = <T>(endpoint: string) => {
const [data, setData] = useState<T[]>([]);
const [error, setError] = useState<any>();
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
const controller = new AbortController();
apiClient
.get<FetchResponse<T>>(endpoint, {
signal: controller.signal,
})
.then((res) => {
setData(res.data.results);
setLoading(false);
})
.catch((err) => {
if (err instanceof CanceledError) return;
setLoading(false);
setError(err);
});
return () => controller.abort();
}, []);
return { data, error, isLoading };
};
export default useData;
Here is the apiClient
import axios from "axios";
export default axios.create({
baseURL: "https://api.themoviedb.org/3",
params: {
key: "ABC",
},
headers: {
accept: "application/json",
Authorization:
"Bearer XYZ",
},
});
this is where I use the useData hook in movies
import useData from "./UseData";
export interface Movie {
id: number;
title: string;
backdrop_path: string;
vote_average: number;
}
const useMovies = () => useData<Movie>("/discover/movie");
export default useMovies;
and this is where i use the useData hook in genres
import useData from "./UseData";
export interface Genre {
id: number;
name: string;
}
const useGenres = () => useData<Genre>("/genre/movie/list");
export default useGenres;
How can I create a generic data hook that can fetch data from both the endpoints without being specific to the attributes?
2
Answers
It looks like the issue is that one of your API functions returns response object with the shape
{ results: Movie[]; total_results: number; }
while the other returns a response object with the shape{ genres: Genre[]; }
.Instead of trying to generically type a
FetchResponse
object "payload" you could pass in the entire expected response interface for the API you are hitting.Example:
Example Usages:
The request config object of the Axios API accepts a response data transformation function at the property
transformResponse
. I’ll inline the documentation here:You can accept this function as the second parameter of your generic hook
useData
and then use it to select/pick/transform the response data for each of the more specific hooks: genres and movies. Here’s an example of what that might look like based on the code you provided:(Complete example at the TypeScript Playground)