Open iffa opened 3 months ago
@kerwanp I'll work on that if you want, this feature will help me a lot.
@iffa after taking closer look, I don't think you'll need ability to inject QueryClient. Methods from openapi-react-query are using QueryClient
provided by QueryClientProvider
.
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient();
const QueryProvider = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export {QueryProvider, queryClient}
Just wrap your app with QueryProvider
, and than invalidate using queryClient
. It works for me.
@iffa after taking closer look, I don't think you'll need ability to inject QueryClient. Methods from openapi-react-query are using
QueryClient
provided byQueryClientProvider
.import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; const queryClient = new QueryClient(); const QueryProvider = ({ children }) => { return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ); }; export {QueryProvider, queryClient}
Just wrap your app with
QueryProvider
, and than invalidate usingqueryClient
. It works for me.
Ah, nice. Just need an easier way to get the generated query keys and this is a good approach
@iffa Could you provide an example of how interface for that should look like?
@iffa Could you provide an example of how interface for that should look like?
Not sure if it is an ideal approach but one way would be to make the use*
functions return the original result + the generated querykey, like this:
return {queryKey: [...], ...useQuery({
queryKey: [method, path, init],
queryFn: async () => {
const mth = method.toUpperCase() as keyof typeof client;
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
const { data, error } = await fn(path, init as any); // TODO: find a way to avoid as any
if (error || !data) {
throw error;
}
return data;
},
...options,
})};
But one may also want to invalidate based on the first 2 parts of the query key (method, path) so I dunno how that would work
@iffa I think it should be done in @tanstack/react-query
, not here. When you call useQuery from openapi-react-query
you're providing keys there, so you have access to them. You could always run queryClient.clear()
to clear all caches or just refetch on particular query. So in my opinion it shouldn't be done like in your example.
@iffa I think it should be done in
@tanstack/react-query
, not here. When you call useQuery fromopenapi-react-query
you're providing keys there, so you have access to them. You could always runqueryClient.clear()
to clear all caches or just refetch on particular query. So in my opinion it shouldn't be done like in your example.
Maybe something along the lines of this:
export type GetQueryKeyMethod<
Paths extends Record<string, Record<HttpMethod, {}>>,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends FetchOptions<FilterKeys<Paths[Path], Method>> | undefined,
>(
method: Method,
url: Path,
init?: Init,
) => ReadonlyArray<unknown>;
export interface OpenapiQueryClient<
Paths extends {},
Media extends MediaType = MediaType,
> {
useQuery: UseQueryMethod<Paths, Media>;
getQueryKey: GetQueryKeyMethod<Paths>;
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
useMutation: UseMutationMethod<Paths, Media>;
}
export default function createClient<
Paths extends {},
Media extends MediaType = MediaType,
>(client: FetchClient<Paths, Media>): OpenapiQueryClient<Paths, Media> {
return {
getQueryKey: (method, path, init) => {
return [method, path, ...(init ? [init] : [])] as const;
},
// ... rest
I think it looks better just as a helper function. I think you can open PR with that and discuss that with maintainers @drwpow @kerwanp
Hey! We clearly have to find a way to give the ability to easily invalidate queries. Unfortunately, simply returning the query keys from the hook is not enough as we might want to invalidate queries outside a component (or a hook).
I think the idea of an helper function works great, your example should work @iffa. I think the function could even be defined outside the createClient so it can be used when generating the real query keys.
An other idea I had in mind was to abstract even more around tanstack:
const usePostsQuery = $api.createQuery('/api/posts/{id}');
const useCreatePostMutation = $api.createMutation('/api/posts/{id}');
export const Example1 = () => {
const { query } = usePostsQuery();
const { mutate } = useCreatePostMutation(data, {
onSuccess: {
queryClient.invalidateQueries({ queryKey: usePostsQuery.queryKey })
}
});
}
export const Example2 = () => {
const { query } = usePostsQuery();
const { mutate } = useCreatePostMutation(data, {
onSuccess: () => {
usePostsQuery.invalidate()
}
});
}
We could then imagine some kind of factory to work with CRUDS:
const $posts = $api.createCrud({
create: '/api/posts',
get: '/api/posts/{id}',
delete: '/api/posts/{id}'
});
export const Example = () => {
const { query } = $posts.useGet(5);
const { mutate } = $posts.useCreate();
}
With optimistic updates, automatic invalidation, etc. But this would require to move away from tanstack and be more than just a wrapper. Maybe in a separate library.
In my opinion openapi-react-query
should be just thin wrapper around react-query
providing type safety.
In my opinion
openapi-react-query
should be just thin wrapper aroundreact-query
providing type safety.
I agree with that, so let's go with @iffa example.
We just need to remove the context of query
as keys can also be used in mutations. More like:
$api.keys('get', '/api/posts');
$api.getKeys('get', '/api/posts');
$api.generateKeys('get', '/api/posts');
React Query has a queryOptions
helper ideally we can do the following:
const options = queryOptions('get', '/endpoint', {})
queryClient.invalidateQueries(options)
// mutate cache
queryClient.setQueryData(options, (prevData) => ({
...prevData
})
// and this one is important to be able to fully use useSuspenseQuery
queryClient.prefetchQuery(options)
Added this to my project, working nicely.
import {
queryOptions as tanstackQueryOptions,
QueryOptions,
DefinedInitialDataOptions,
} from '@tanstack/react-query'
import type {
ClientMethod,
FetchResponse,
MaybeOptionalInit,
Client as FetchClient,
} from 'openapi-fetch'
import type {
HttpMethod,
MediaType,
PathsWithMethod,
} from 'openapi-typescript-helpers'
import { getClient } from './api'
type QueryOptionsMethod<
Paths extends Record<string, Record<HttpMethod, {}>>,
Media extends MediaType = MediaType,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>,
Options extends Omit<
QueryOptions<Response['data'], Response['error']>,
'queryKey' | 'queryFn'
>,
>(
method: Method,
path: Path,
init?: Init & { [key: string]: unknown },
options?: Options,
) => DefinedInitialDataOptions<Response['data'], Response['error']>
interface QueryOptionsReturn<
Paths extends {},
Media extends MediaType = MediaType,
> {
queryOptions: QueryOptionsMethod<Paths, Media>
}
const createQueryOptions = <
Paths extends {},
Media extends MediaType = MediaType,
>(
client: FetchClient<Paths, Media>,
): QueryOptionsReturn<Paths, Media> => {
return {
queryOptions: (method, path, init, options) => {
return tanstackQueryOptions({
queryKey: [method, path, init],
queryFn: async () => {
const mth = method.toUpperCase() as keyof typeof client
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>
const { data, error } = await fn(path, init as any) // TODO: find a way to avoid as any
if (error || !data) {
throw error
}
return data
},
...(options as any),
})
},
}
}
export const { queryOptions } = createQueryOptions(getClient())
export type InferQueryData<T> =
T extends DefinedInitialDataOptions<infer D, any> ? Partial<D> : never
React Query has a
queryOptions
helper ideally we can do the following:const options = queryOptions('get', '/endpoint', {}) queryClient.invalidateQueries(options) // mutate cache queryClient.setQueryData(options, (prevData) => ({ ...prevData }) // and this one is important to be able to fully use useSuspenseQuery queryClient.prefetchQuery(options)
I like a lot this solution as it follows the api of @tanstack/react-query.
$api.queryOptions('get', '/api/posts');
This should also make the implementation of the following issues much easier and cleaner:
@zsugabubu the proposed solution is really similar to your PR, do you want to take this issue?
queryOptions
would cover exact matching usecase only, that is a serious limitation compared to the API provided by react-query, and it returns query options, not query filters, so theoretically it could not even be used in this context.
What about creating top-level (outside createClient
) queryFilters
and mutationFilters
?
// { queryKey: ['get', '/items/{id}'], stale: true }
queryFilters<Paths>('get', '/items/{id}', undefined, { stale: true }));
// { queryKey: ['get', '/items/{id}', { params: ... }] }
queryFilters<Paths>('get', '/items/{id}', { params: { path: { id: 'foo' } } });
// { queryKey: ['get', '/items/{id}'], predicate: () => ... }
queryFilters<Paths>('get', '/items/{id}', init => init.params.path.id == 'foo');
// And use it like:
queryClient.invalidateQueries(queryFilters<Paths>(...));
@zsugabubus the init and options param is optional, so you can also support non exact matching queries.
There is currently no easy way to invalidate queries. #1804 and #1805 together would solve this by allowing us to do it ourselves, but a built-in mechanism could also be helpful.