Replies: 1 comment
-
Extending on this to generate querykey using import {
type SkipToken,
type UseMutationOptions,
type UseMutationResult,
type UseQueryOptions,
type UseQueryResult,
type UseSuspenseQueryOptions,
type UseSuspenseQueryResult,
useMutation as useRQMutation,
useQuery as useRQQuery,
useSuspenseQuery as useRQSuspenseQuery,
} from "@tanstack/react-query";
import type { InferResponseType } from "hono/client";
import type {
ClientErrorStatusCode,
ServerErrorStatusCode,
StatusCode,
SuccessStatusCode,
} from "hono/utils/http-status";
type ErrorStatusCode = ClientErrorStatusCode | ServerErrorStatusCode;
type HttpMethodKey = "$get" | "$post" | "$put" | "$delete" | "$patch" | "$options" | "$head";
type AvailableMethodKeys<T> = Extract<keyof T, HttpMethodKey>;
type EndpointMethodParams<T extends object, M extends AvailableMethodKeys<T>> = T[M] extends (
params: infer P,
...args: any[]
) => any
? P
: never;
type EndpointResponseType<
T extends object,
M extends AvailableMethodKeys<T>,
U extends StatusCode = StatusCode,
> = T[M] extends (...args: any[]) => Promise<Response> ? InferResponseType<T[M], U> : never;
function getPathFromUrl(url: string): string {
try {
if (url.startsWith("http")) {
const urlObj = new URL(url);
return urlObj.pathname;
}
return url;
} catch {
return url;
}
}
type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
export type QueryKey<
T extends object & { $url: () => URL | { toString: () => string } },
M extends AvailableMethodKeys<T>,
Params extends EndpointMethodParams<T, M>,
> = [M, string, Params];
export const getQueryKey = <
T extends object & { $url: () => URL | { toString: () => string } },
M extends AvailableMethodKeys<T>,
Params extends EndpointMethodParams<T, M>,
>(
endpoint: T,
method: M,
params: Params,
): QueryKey<T, M, Params> => {
const urlString = endpoint.$url().toString();
const path = getPathFromUrl(urlString);
const filteredParams = {} as any;
if (params && typeof params === "object") {
if ("param" in params) {
filteredParams.param = params.param;
}
if ("query" in params) {
filteredParams.query = params.query;
}
}
return [method, path, filteredParams] as unknown as QueryKey<T, M, Params>;
};
export const queryOptions = <
T extends object & { $url: () => URL | { toString: () => string } },
M extends AvailableMethodKeys<T>,
Params extends EndpointMethodParams<T, M>,
Options extends Omit<
UseQueryOptions<
TResponse,
TError,
InferSelectReturnType<TResponse, TError>,
QueryKey<T, M, Params>
>,
"queryKey" | "queryFn"
>,
TResponse = EndpointResponseType<T, M, SuccessStatusCode>,
TError = EndpointResponseType<T, M, ErrorStatusCode>,
>(
endpoint: T,
method: M,
params: Params,
options?: Options,
): NoInfer<
Omit<
UseQueryOptions<
TResponse,
TError,
InferSelectReturnType<TResponse, Options["select"]>,
QueryKey<T, M, Params>
>,
"queryFn"
> & {
queryFn: Exclude<
UseQueryOptions<
TResponse,
TError,
InferSelectReturnType<TResponse, Options["select"]>,
QueryKey<T, M, Params>
>["queryFn"],
SkipToken | undefined
>;
}
> => {
const endpointFn = endpoint[method] as unknown as (params: any) => Promise<Response>;
const result = {
queryKey: getQueryKey(endpoint, method, params),
queryFn: async () => {
const res = await endpointFn(params);
if (res.status >= 200 && res.status < 300) {
return (await res.json()) as TResponse;
}
const errorData = (await res.json()) as TError;
const error = new Error(`Request failed with status ${res.status}`) as Error & {
status: number;
data: TError;
};
error.status = res.status;
error.data = errorData;
throw error;
},
...options,
};
return result as any;
};
export const mutationOptions = <
T extends object,
M extends AvailableMethodKeys<T>,
TResponse = EndpointResponseType<T, M, SuccessStatusCode>,
TError = EndpointResponseType<T, M, ErrorStatusCode>,
TVariables = EndpointMethodParams<T, M>,
TContext = unknown,
>(
endpoint: T & { $url: () => URL | { toString: () => string } },
method: M,
options?: Omit<
UseMutationOptions<TResponse, TError, TVariables, TContext>,
"mutationFn" | "mutationKey"
>,
): UseMutationOptions<TResponse, TError, TVariables, TContext> => {
const endpointFn = endpoint[method] as unknown as (params: TVariables) => Promise<Response>;
return {
mutationKey: getQueryKey(endpoint, method, {} as any),
mutationFn: async (variables) => {
const res = await endpointFn(variables);
if (res.status >= 200 && res.status < 300) {
return (await res.json()) as TResponse;
}
const errorData = (await res.json()) as TError;
const error = new Error(`Request failed with status ${res.status}`) as Error & {
status: number;
data: TError;
};
error.status = res.status;
error.data = errorData;
throw error;
},
...options,
};
};
export const useQuery = <
T extends object & { $url: () => URL | { toString: () => string } },
M extends AvailableMethodKeys<T>,
Params extends EndpointMethodParams<T, M>,
Options extends Omit<
UseQueryOptions<
TResponse,
TError,
InferSelectReturnType<TResponse, TError>,
QueryKey<T, M, Params>
>,
"queryKey" | "queryFn"
>,
TResponse = EndpointResponseType<T, M, SuccessStatusCode>,
TError = EndpointResponseType<T, M, ErrorStatusCode>,
>(
endpoint: T & { $url: () => URL | { toString: () => string } },
method: M,
params: Params,
options?: Options,
): UseQueryResult<InferSelectReturnType<TResponse, Options["select"]>, TError> => {
return useRQQuery(
queryOptions<T, M, Params, Options, TResponse, TError>(endpoint, method, params, options),
);
};
export const useSuspenseQuery = <
T extends object & { $url: () => URL | { toString: () => string } },
M extends AvailableMethodKeys<T>,
Params extends EndpointMethodParams<T, M>,
Options extends Omit<
UseSuspenseQueryOptions<
TResponse,
TError,
InferSelectReturnType<TResponse, TError>,
QueryKey<T, M, Params>
>,
"queryKey" | "queryFn"
>,
TResponse = EndpointResponseType<T, M, SuccessStatusCode>,
TError = EndpointResponseType<T, M, ErrorStatusCode>,
>(
endpoint: T & { $url: () => URL | { toString: () => string } },
method: M,
params: Params,
options?: Options,
): UseSuspenseQueryResult<InferSelectReturnType<TResponse, Options["select"]>, TError> => {
return useRQSuspenseQuery(
queryOptions<T, M, Params, Options, TResponse, TError>(endpoint, method, params, options),
);
};
export const useMutation = <
T extends object,
M extends AvailableMethodKeys<T>,
TResponse = EndpointResponseType<T, M, SuccessStatusCode>,
TError = EndpointResponseType<T, M, ErrorStatusCode>,
TVariables = EndpointMethodParams<T, M>,
TContext = unknown,
>(
endpoint: T & { $url: () => URL | { toString: () => string } },
method: M,
options?: Omit<
UseMutationOptions<TResponse, TError, TVariables, TContext>,
"mutationFn" | "mutationKey"
>,
): UseMutationResult<TResponse, TError, TVariables, TContext> => {
return useRQMutation(
mutationOptions<T, M, TResponse, TError, TVariables, TContext>(endpoint, method, options),
);
}; Example const client = hc('/api');
const data = useQuery(
client.posts,
"$get",
{ query: { filter: "active" } },
{ staleTime: 60000 }
);
const data =useQuery(
queryOptions(
client.posts,
"$get",
{ query: { filter: "active" } },
{ staleTime: 60000 }
)
);
const data =useQuery(
queryOptions(
client.posts,
"$post",
{ json: { name: "john" } }
)
); |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Currently the documentation shows different methods to use @tanstack/react-query (on the Hono Stack page) and SWR on the RPC page.
However both felt a bit clunky to me. My preference would be to have a hook in which you add the endpoint, and the data needed and done (expect if you want to add extra options). Somethings like:
This would be ideal as:
After some fighting with the types, I've make a small POC that works quite well for get requests (open for suggestions / improvements), mainly one issue I haven't resolved yet:
As a workaround I've now added one extra argument, in which I add the $url as key, i.e.:
The POC for the endpoint hook atm looks like this:
So not really satisfied yet, but does save some boiler plate each useQuery call and enforces consistency throughout the codebase. As I found it difficult to infer the types I was able to use "client.api.echo[':echo']" as param (i.e. without .$get), as then we would be able to call $url and $get inside useEndpoint(...) and the key could be automatically set instead of manually added via an extra param.
Maybe a .$meta can be added which returns related data (path, method, endpoint itself so request and response types can be inferred from it), and as a sidecar I think that would also make it easier to make extensions on it (for instance adding axios instead of fetch).
Beta Was this translation helpful? Give feedback.
All reactions