Я работаю над проектом поиска фильмов, где мне нужно получить данные из двух API (сайт фильма Tmdb) с разными структурами ответов (конечными точками фильмов и жанров). Чтобы избежать дублирования одной и той же логики для каждого перехватчика, который я использую для получения данных из API, я знаю, что могу использовать дженерики, но, к сожалению, когда я пытаюсь, я получаю неопределенную ошибку.
Текущие жанры
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;
Актуальные фильмы
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;
Но, как вы можете видеть, я продублировал ту же логику, что не является хорошей практикой.
Я попытался извлечь общий крючок для извлечения данных, чтобы уменьшить избыточность, но он работает только для конечной точки фильмов и возвращает неопределенное значение для жанров. Я считаю, что это связано с тем, что общий хук ожидает массив результатов, который присутствует в ответе фильмов, но не в ответе жанров. Вместо этого в ответе жанров используется массив жанров.
Моя попытка решения
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;
Вот APIКлиент
import axios from "axios";
export default axios.create({
baseURL: "https://api.themoviedb.org/3",
params: {
key: "ABC",
},
headers: {
accept: "application/json",
Authorization:
"Bearer XYZ",
},
});
здесь я использую хук useData в фильмах
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;
и здесь я использую хук useData в жанрах
import useData from "./UseData";
export interface Genre {
id: number;
name: string;
}
const useGenres = () => useData<Genre>("/genre/movie/list");
export default useGenres;
Как я могу создать универсальный перехватчик данных, который может получать данные из обеих конечных точек, не привязываясь к атрибутам?
О, ок, думаю, я вижу проблему. Не возражаете ли вы также добавить свой код apiClient
, который обрабатывает эти запросы и возвращает объекты ответа?
Огромное спасибо за внесенные вами исправления, я здесь еще новичок. Я включил запрошенный код.
Похоже, проблема в том, что одна из ваших функций API возвращает объект ответа с формой { results: Movie[]; total_results: number; }
, а другая возвращает объект ответа с формой { genres: Genre[]; }
.
Вместо того, чтобы пытаться в общих чертах ввести «полезную нагрузку» объекта FetchResponse
, вы можете передать весь интерфейс ожидаемого ответа для API, к которому вы обращаетесь.
Пример:
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
const useData = <FetchResponse>(endpoint: string) => {
const [data, setData] = useState<FetchResponse | undefined>();
const [error, setError] = useState<any>();
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
const controller = new AbortController();
apiClient
.get<FetchResponse>(endpoint, {
signal: controller.signal,
})
.then((res) => {
setData(res.data);
setLoading(false);
})
.catch((err) => {
setLoading(false); // clear loading before any early returns
if (err instanceof CanceledError) return;
setError(err);
});
return () => controller.abort();
}, []);
return { data, error, isLoading };
};
Примеры использования:
import useData from "./UseData";
export interface Movie {
id: number;
title: string;
backdrop_path: string;
vote_average: number;
}
interface FetchMoviesResponse {
total_results: number;
results: Movie[];
}
const useMovies = () => useData<FetchMoviesResponse>("/discover/movie");
export default useMovies;
import useData from "./UseData";
export interface Genre {
id: number;
name: string;
}
interface FetchGenreResponse {
genres: Genre[];
}
const useGenres = () => useData<FetchGenreResponse>("/genre/movie/list");
export default useGenres;
Огромное тебе спасибо, чувак, это сработало, и самое главное, я понял, где я ошибся. Я очень благодарен. Я застрял на два дня с этой проблемой.
@OsteenOmega Не беспокойтесь, оказалось, что мне действительно не нужно было видеть ваш код Axios (кроме того, чтобы убедиться, что вы там не делаете ничего сумасшедшего), и на самом деле вы просто вводили ожидаемые объекты ответа. Здоровья и удачи!
Объект конфигурации запроса API Axios принимает функцию преобразования данных ответа в свойстве transformResponse
. Я вставлю документацию сюда:
// `transformResponse` allows changes to the response data to be made before // it is passed to then/catch transformResponse: function (data) { // Do whatever you want to transform the data return data; },
Вы можете принять эту функцию в качестве второго параметра вашего общего перехватчика useData
, а затем использовать ее для выбора/выбора/преобразования данных ответа для каждого из более конкретных перехватчиков: жанров и фильмов. Вот пример того, как это может выглядеть на основе предоставленного вами кода:
(Полный пример на TypeScript Playground)
type UseDataResult<T> =
| { // still loading
data: undefined;
error: undefined;
isLoading: true;
}
| { // exception occurred
data: undefined;
error: unknown;
isLoading: false;
}
| { // success: data received
data: T;
error: undefined;
isLoading: false;
};
function useData<Initial, Final>(
endpoint: string,
transformResponse: (value: Initial) => Final,
): UseDataResult<Final> {
const [data, setData] = useState<Final | undefined>();
const [error, setError] = useState<unknown>();
const [isLoading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
setLoading(true);
const axiosResponse = await apiClient.get(endpoint, {
signal: controller.signal,
transformResponse,
});
setData(axiosResponse.data);
setError(undefined);
} catch (cause) {
setData(undefined);
setError(cause);
} finally {
setLoading(false);
}
})();
return () => controller.abort();
}, []);
return { data, error, isLoading } as UseDataResult<Final>;
}
const useMovies = () =>
useData("/discover/movie", (data: { results: Movie[] }) => data.results);
const useGenres = () =>
useData("/genre/movie/list", (data: { genres: Genre[] }) => data.genres);
Можете ли вы отредактировать , чтобы показать, где и как вы используете свой собственный
useData
крючок? Также было бы полезно рассказать, как выглядят эти ответы API, чтобы мы могли увидеть, что будет/должно вернуть использование. См. минимально воспроизводимый пример.