Skip to content

Instantly share code, notes, and snippets.

@omerman
Last active November 3, 2022 15:18
Show Gist options
  • Save omerman/d650c0b9c654c2540dbdcd81349892a6 to your computer and use it in GitHub Desktop.
Save omerman/d650c0b9c654c2540dbdcd81349892a6 to your computer and use it in GitHub Desktop.
import { QueryFunctionContext } from "@tanstack/react-query";
import { Updater } from "@tanstack/query-core/build/types/packages/query-core/src/utils";
import { axiosClient } from "../axios/axiosClient";
import { queryClient } from "./queryClient";
export class ApiQuery<Data extends object & { __OBJECT?: any }, Arg = void> {
static skipToken = "$_$_SKIP_$_$" as const;
readonly buildApiPathKey: (arg: Arg) => string;
constructor(
readonly apiPath: void extends Arg ? string : (arg: Arg) => string,
readonly options?: {
customFetcher?: (url: string) => Promise<Data>;
}
) {
if (typeof apiPath === "function") {
this.buildApiPathKey = apiPath as any;
} else {
this.buildApiPathKey = () => apiPath as any;
}
}
// Use this in pages folder network requests.
invokeServerSideRequest = (...args: void extends Arg ? [] : [Arg]) => {
const apiPath = this.buildApiPathKey(args[0]!);
return axiosClient.get<Data>(apiPath, {
baseURL: process.env.SERVER_SIDE_BASE_URL,
});
};
updateCache(
...args: void extends Arg
? [Updater<Data, Data>]
: [Arg, Updater<Data, Data>]
) {
const key =
args.length === 1
? this.buildApiPathKey(undefined as any)
: this.buildApiPathKey(args[0]);
const updater = args.length === 1 ? args[0] : args[1];
if (typeof updater === "function") {
if (queryClient.getQueryData([key]) === undefined) return;
queryClient.setQueryData<Data>([key], (cachedValue) => {
return updater(cachedValue!);
});
} else {
queryClient.setQueryData([key], updater);
}
}
prefetch(...args: void extends Arg ? [] : [Arg]) {
const key =
args.length === 1
? this.buildApiPathKey(args[0])
: this.buildApiPathKey(undefined as any);
queryClient.prefetchQuery([key], (...args) => this.fetch(...args));
}
// Use this in pages folder network requests.
invalidate = (...args: void extends Arg ? [] : [Arg]) => {
const apiPath = this.buildApiPathKey(args[0]!);
return queryClient.invalidateQueries([apiPath]);
};
// Use this when query data doesn't consist of one type.
dataCast<CastData extends object>(): ApiQuery<CastData, Arg> {
return this as any;
}
async fetch({ queryKey }: QueryFunctionContext) {
const url = queryKey[0] as string;
const defaultFetcher = async () => {
const response = await axiosClient.get<Data>(url);
return response.data;
};
const { customFetcher } = this.options ?? {};
return customFetcher?.(url) ?? defaultFetcher();
}
}
export type SkipTokenType = typeof ApiQuery.skipToken;
import { ApiQuery } from "@project/core/react-query/ApiQuery";
import { SongListItem } from "./types";
export class SongsApi {
static listQuery = new ApiQuery<SongListItem[]>("/api/songs");
}
import { AxiosError } from "axios";
import {
UseQueryOptions,
UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { ApiQuery, SkipTokenType } from "./ApiQuery";
type ArgParamater<Arg> =
| Arg
| { [key in keyof Arg]: Arg[key] | SkipTokenType }
| SkipTokenType;
export interface UseApiQuery {
<Data extends object, Arg>(
apiQuery: ApiQuery<Data, Arg>,
...args: void extends Arg
? [UseQueryOptions<Data, AxiosError>?]
: [ArgParamater<Arg>, UseQueryOptions<Data, AxiosError>?]
): UseQueryResult<Data, AxiosError>;
}
export const useApiQuery: UseApiQuery = (apiQuery, ...args) => {
const arg =
args[0] === ApiQuery.skipToken
? ApiQuery.skipToken
: parseArg(args[0] as any);
const overrideOptions = (args.length === 1 ? args[0] : args[1]) as
| UseQueryOptions<any, any>
| undefined;
const queryKey =
arg === ApiQuery.skipToken
? [ApiQuery.skipToken]
: [apiQuery.buildApiPathKey(arg!)];
const { enabled = true } = overrideOptions ?? {};
return useQuery<any, any>({
...overrideOptions,
queryKey,
queryFn: (...args) => apiQuery.fetch(...args),
enabled: enabled && arg !== ApiQuery.skipToken,
});
};
/*
1. Will convert arg of object shape, for example {x: 1, y: <skipToken>}, to skipToken.
2. Will convert arg of array shape, for example [x, <skipToken>], to skipToken.
3. For All other shapes/values, parseArg will return the given arg AS IS.
*/
const parseArg = <Arg>(
arg: Arg | SkipTokenType | { [key in keyof Arg]: Arg[key] | SkipTokenType }
): Arg | SkipTokenType => {
if (arg === ApiQuery.skipToken) return ApiQuery.skipToken;
if (typeof arg !== "object") return arg;
if (Object.values(arg).some((x) => x === ApiQuery.skipToken))
return ApiQuery.skipToken;
return arg as Arg;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment