瀏覽代碼

refactor: useRequestCallback for all request hooks

Fela Maslen 5 年之前
父節點
當前提交
fb9d88700a
共有 3 個文件被更改,包括 68 次插入168 次删除
  1. 53 78
      gmus-web/src/hooks/fetch/artists.ts
  2. 1 68
      gmus-web/src/hooks/request.ts
  3. 14 22
      gmus-web/src/hooks/status.ts

+ 53 - 78
gmus-web/src/hooks/fetch/artists.ts

@@ -1,65 +1,65 @@
 import { AxiosInstance, AxiosResponse } from 'axios';
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 
 import { Song } from '../../types/songs';
 import { getApiUrl } from '../../utils/url';
-import { useCancellableRequest } from '../request';
+import { useRequestCallback } from '../request';
 
 type ArtistsResponse = {
   artists: string[];
 };
 
-const sendArtistsRequest = (axios: AxiosInstance): Promise<AxiosResponse<ArtistsResponse>> =>
-  axios.get(`${getApiUrl()}/artists`);
-
-type AlbumsResponse = {
-  artist: string;
-  albums: string[];
-};
-
-type AlbumsQuery = {
-  artist: string;
-};
-
-const sendAlbumsRequest = (
-  axios: AxiosInstance,
-  query: AlbumsQuery,
-): Promise<AxiosResponse<AlbumsResponse>> =>
-  axios.get(`${getApiUrl()}/albums?artist=${encodeURIComponent(query.artist)}`);
-
-type SongsResponse = {
-  artist: string;
-  songs: Song[];
-};
-
-type SongsQuery = AlbumsQuery;
-
-const sendSongsRequest = (
-  axios: AxiosInstance,
-  query: SongsQuery,
-): Promise<AxiosResponse<SongsResponse>> =>
-  axios.get(`${getApiUrl()}/songs?artist=${encodeURIComponent(query.artist)}`);
-
 export function useArtists(): ArtistsResponse & {
   fetching: boolean;
 } {
-  const [artists, setArtists] = useState<string[]>([]);
+  const sendRequest = useCallback(
+    (axios: AxiosInstance): Promise<AxiosResponse<ArtistsResponse>> =>
+      axios.get(`${getApiUrl()}/artists`),
+    [],
+  );
+
+  const [onFetch, response, fetching] = useRequestCallback<void, ArtistsResponse>({
+    sendRequest,
+  });
+
+  useEffect(onFetch, [onFetch]);
 
-  const [pause, setPause] = useState<boolean>(false);
+  return { artists: response?.artists ?? [], fetching };
+}
 
-  const handleResponse = useCallback((response: ArtistsResponse) => {
-    setArtists((last) => Array.from(new Set([...last, ...response.artists])));
-    setPause(true);
-  }, []);
+type ArtistDependencyResponse<K extends string, T> = { artist: string } & { [key in K]: T[] };
 
-  const fetching = useCancellableRequest<void, ArtistsResponse>({
-    query: undefined,
-    pause,
-    sendRequest: sendArtistsRequest,
-    handleResponse,
+function useArtistDependency<K extends string, T>(
+  key: K,
+  artist: string,
+  pause: boolean,
+): [ArtistDependencyResponse<K, T> | null, boolean] {
+  const sendRequest = useCallback(
+    (axios: AxiosInstance, query: string): Promise<AxiosResponse<ArtistDependencyResponse<K, T>>> =>
+      axios.get(`${getApiUrl()}/${key}?artist=${encodeURIComponent(query)}`),
+    [key],
+  );
+
+  const [onFetch, response, fetching] = useRequestCallback<string, ArtistDependencyResponse<K, T>>({
+    sendRequest,
   });
 
-  return { artists, fetching };
+  const [hasLoadedByArtist, setHasLoadedByArtist] = useState<Record<string, boolean>>({});
+  const hasLoadedThisArtist = !!hasLoadedByArtist[artist];
+
+  useEffect(() => {
+    if (!pause && !hasLoadedThisArtist) {
+      onFetch(artist);
+    }
+  }, [onFetch, pause, hasLoadedThisArtist, artist]);
+
+  useEffect(() => {
+    if (response) {
+      setHasLoadedByArtist((last) => ({ ...last, [response.artist]: true }));
+    }
+  }, [response]);
+
+  return [response, fetching];
 }
 
 export function useArtistsAlbumsAndSongs(
@@ -67,42 +67,17 @@ export function useArtistsAlbumsAndSongs(
   pauseAlbums: boolean,
   pauseSongs: boolean,
 ): {
-  albums: AlbumsResponse | undefined;
-  songs: SongsResponse | undefined;
+  albums: ArtistDependencyResponse<'albums', string> | null;
+  songs: ArtistDependencyResponse<'songs', Song> | null;
   fetchingAlbums: boolean;
   fetchingSongs: boolean;
 } {
-  const [hasLoadedAlbums, setHasLoadedAlbums] = useState<Record<string, boolean>>({});
-  const [hasLoadedSongs, setHasLoadedSongs] = useState<Record<string, boolean>>({});
-
-  const query = useMemo<AlbumsQuery>(() => ({ artist }), [artist]);
-
-  const [albums, setAlbums] = useState<AlbumsResponse | undefined>();
-  const [songs, setSongs] = useState<SongsResponse | undefined>();
-
-  const handleAlbumsResponse = useCallback((response: AlbumsResponse) => {
-    setAlbums(response);
-    setHasLoadedAlbums((last) => ({ ...last, [response.artist]: true }));
-  }, []);
-
-  const handleSongsResponse = useCallback((response: SongsResponse) => {
-    setSongs(response);
-    setHasLoadedSongs((last) => ({ ...last, [response.artist]: true }));
-  }, []);
-
-  const fetchingAlbums = useCancellableRequest<AlbumsQuery, AlbumsResponse>({
-    query,
-    pause: pauseAlbums || hasLoadedAlbums[artist],
-    sendRequest: sendAlbumsRequest,
-    handleResponse: handleAlbumsResponse,
-  });
-
-  const fetchingSongs = useCancellableRequest<SongsQuery, SongsResponse>({
-    query,
-    pause: pauseSongs || hasLoadedSongs[artist],
-    sendRequest: sendSongsRequest,
-    handleResponse: handleSongsResponse,
-  });
+  const [albums, fetchingAlbums] = useArtistDependency<'albums', string>(
+    'albums',
+    artist,
+    pauseAlbums,
+  );
+  const [songs, fetchingSongs] = useArtistDependency<'songs', Song>('songs', artist, pauseSongs);
 
   return {
     albums,

+ 1 - 68
gmus-web/src/hooks/request.ts

@@ -2,81 +2,14 @@ import axios, { Canceler, AxiosInstance, AxiosResponse } from 'axios';
 import { useCallback, useEffect, useRef, useState } from 'react';
 
 type Options<Query, Response> = {
-  query: Query;
-  pause?: boolean;
   sendRequest: (axiosInstance: AxiosInstance, query: Query) => Promise<AxiosResponse<Response>>;
-  handleResponse: (res: Response, query: Query) => void;
   onError?: (err: Error) => void;
-  onClear?: () => void;
-  debounceDelay?: number;
 };
 
-export function useCancellableRequest<Query, Response = void>({
-  query,
-  pause,
-  sendRequest,
-  handleResponse,
-  onError,
-  onClear,
-}: Options<Query, Response>): boolean {
-  const [loading, setLoading] = useState<boolean>(false);
-
-  const cancelRequest = useRef<Canceler>();
-
-  useEffect(() => {
-    setLoading(!!query);
-    if (!query) {
-      onClear?.();
-    }
-  }, [query, onClear]);
-
-  useEffect(() => {
-    let cancelled = false;
-    const request = async (): Promise<void> => {
-      try {
-        const axiosWithToken = axios.create({
-          cancelToken: new axios.CancelToken((token): void => {
-            cancelRequest.current = token;
-          }),
-        });
-        const res = await sendRequest(axiosWithToken, query);
-        if (cancelled) {
-          return;
-        }
-
-        handleResponse(res.data, query);
-      } catch (err) {
-        if (!axios.isCancel(err)) {
-          onError?.(err);
-        }
-      } finally {
-        if (!cancelled) {
-          setLoading(false);
-        }
-      }
-    };
-
-    if (!pause) {
-      request();
-    }
-
-    return (): void => {
-      cancelled = true;
-      if (cancelRequest.current) {
-        cancelRequest.current();
-      }
-    };
-  }, [sendRequest, handleResponse, onError, query, pause]);
-
-  return loading;
-}
-
-type CallbackOptions<Query, Response> = Pick<Options<Query, Response>, 'sendRequest' | 'onError'>;
-
 export function useRequestCallback<Query, Response = void>({
   onError,
   sendRequest,
-}: CallbackOptions<Query, Response>): [(query: Query) => void, Response | null, boolean] {
+}: Options<Query, Response>): [(query: Query) => void, Response | null, boolean] {
   const [response, setResponse] = useState<Response | null>(null);
   const [loading, setLoading] = useState<boolean>(false);
 

+ 14 - 22
gmus-web/src/hooks/status.ts

@@ -1,30 +1,22 @@
 import { AxiosInstance, AxiosResponse } from 'axios';
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useEffect } from 'react';
 
 import { Song } from '../types';
 import { getApiUrl } from '../utils/url';
-import { useCancellableRequest } from './request';
-
-type SongInfoQuery = { id: number };
-
-const sendSongInfoRequest = (
-  axios: AxiosInstance,
-  query: SongInfoQuery,
-): Promise<AxiosResponse<Song>> => axios.get(`${getApiUrl()}/song-info?id=${query.id}`);
+import { useRequestCallback } from './request';
 
 export function useCurrentlyPlayingSongInfo(songId: number | null): Song | null {
-  const [songInfo, setSongInfo] = useState<Song | null>(null);
-  const handleResponse = useCallback((res: Song) => {
-    setSongInfo(res);
-  }, []);
-
-  const query = useMemo<SongInfoQuery>(() => (songId ? { id: songId } : { id: 0 }), [songId]);
+  const sendRequest = useCallback(
+    (axios: AxiosInstance, id: number): Promise<AxiosResponse<Song>> =>
+      axios.get(`${getApiUrl()}/song-info?id=${id}`),
+    [],
+  );
 
-  useCancellableRequest<SongInfoQuery, Song>({
-    query,
-    pause: !songId,
-    sendRequest: sendSongInfoRequest,
-    handleResponse,
-  });
-  return songInfo;
+  const [onFetch, response] = useRequestCallback<number, Song>({ sendRequest });
+  useEffect(() => {
+    if (songId) {
+      onFetch(songId);
+    }
+  }, [onFetch, songId]);
+  return response;
 }