Invalidate generated React Query
kallebornemark opened this issue · comments
I want to invalidate the query in useMyQuery
when the call in useMyMutation
succeeds. To do this, I need to provide the query key from the former to the onSuccess
callback on the latter.
const { data } = useMyQuery(); // generated hook
const { mutateAsync } = useMyMutation({ // generated hook
onSuccess: () => {
queryClient.invalidateQueries(
// query key from useMyQuery - how?
);
},
});
So, as I see it, I either need to:
- Be able to provide a custom query key when using
useMyQuery
- Get access to
useMyQuery
's generated query key - Manually provide the query key to
useMyMutation
'sonSuccess
as it's predictable (but I'd rather not do this)
What's the best way to achieve this?
Not pretty, but I ended up doing something similar to what @fabien0102 talked about in #147 (comment) by adding a $queryKey
property to the return data for all types.
function generateConfig(filenamePrefix: string, url: string): Config {
return {
// ...
to: async (context) => {
Object.entries(context.openAPIDocument.components!.schemas!).forEach(
([_, value]) => {
if ("properties" in value) {
value.properties!.$queryKey = {
type: "array",
};
}
}
);
},
};
}
This adds the property to all of the generated return types. Then in the fetcher file I can calculate the query key using the url
and the pathParams
.
const generateQueryKeyFromUrl = (
url: string,
pathParams: Record<string, string | number> | undefined
) =>
url
.split("/")
.map((part) => {
if (!pathParams || !part.startsWith("{") || !part.endsWith("}")) {
return part;
}
const queryParamWithoutDelimiters = part.substring(1, part.length - 1);
const interpolatedQueryParam = pathParams[queryParamWithoutDelimiters];
return interpolatedQueryParam ?? "";
})
.slice(1);
and add it to the return data
if (response.headers.get("content-type")?.includes("json")) {
const json = await response.json();
const $queryKey = generateQueryKeyFromUrl(url, pathParams);
return {
...json,
$queryKey,
}
}
which finally lets me access it in the consumer component
const { data } = useFoo();
const { mutateAsync } = useUpdateFoo({
onSuccess: (newData) => {
if (data?.$queryKey) {
queryClient.setQueryData(data.$queryKey, () => newData);
}
},
});
Hey, in the generated {namespace}Context.ts
, you have this function:
// {namespace}Context.ts
export const queryKeyFn = (operation: QueryOperation) => {
const queryKey: unknown[] = hasPathParams(operation)
? operation.path
.split("/")
.filter(Boolean)
.map((i) => resolvePathParam(i, operation.variables.pathParams))
: operation.path.split("/").filter(Boolean);
if (hasQueryParams(operation)) {
queryKey.push(operation.variables.queryParams);
}
if (hasBody(operation)) {
queryKey.push(operation.variables.body);
}
return queryKey;
}
This function is used by every generated useQuery
hooks, so normally, you can just do this:
import { queryKeyFn } from "./myAppContext"
const { data } = useMyQuery(); // generated hook
const { mutateAsync } = useMyMutation({ // generated hook
onSuccess: () => {
queryClient.invalidateQueries(
queryKeyFn({ path: "/something", operationId: "something", variables: {} }); // Typescript should help you here
);
},
});
You can also use the devtool (https://tanstack.com/query/v3/docs/react/devtools) to check the key and see if everything works as expected
This is actually something I would love to change (with a breaking change) to mimic how trpc is doing it, they are using a useContext()
with all the proper helpers, and I love it! But time…
Anyway, this should fit your needs for now 😃
Ah, that's pretty sweet. Noticed that the type of the object parameter expected by queryKeyFn
is a union of all the endpoints, so IntelliSense is nice. Aaand I don't have to pollute the types with a property that's only going to be used like 2% of the time. 😁
Thanks!
This works, but I'm curious. Would I be able to expose the internally generated query key to the return value of the generated hooks? Basically what I want is this:
const { data, queryKey } = useMyQuery(); // generated
I'm sorry, this can't work the way you want since the queryKey
depends on the query params. useMyQuery
is just a proxy to useQuery
and queryKey
is not part of the API.
I guess you could wrap the useMyQuery()
if you really want this API, but be careful with query params.
That sounds pretty easy to me, internally you seem to generate this:
export const useGetUser = <TData = Schemas.User>(
variables: GetUserVariables,
options?: Omit<
reactQuery.UseQueryOptions<Schemas.User, GetUserError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const {fetcherOptions, queryOptions, queryKeyFn} = useApiContext(options);
return reactQuery.useQuery<Schemas.User, GetUserError, TData>({
queryKey: queryKeyFn({
path: '/user/{userId}',
operationId: 'getUser',
variables,
}),
queryFn: ({signal}) => fetchGetUser({...fetcherOptions, ...variables}, signal),
...options,
...queryOptions,
});
};
You could just expose another function that only computes the key:
export const useGetUserKey = (
variables: GetUserVariables,
options?: Omit<
reactQuery.UseQueryOptions<Schemas.User, GetUserError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const {queryKeyFn} = useApiContext(options);
return queryKeyFn({
path: '/user/{userId}',
operationId: 'getUser',
variables,
});
};
Which can also be used in the useGetUser
to avoid duplication:
export const useGetUser = <TData = Schemas.User>(
variables: GetUserVariables,
options?: Omit<
reactQuery.UseQueryOptions<Schemas.User, GetUserError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const {fetcherOptions, queryOptions} = useApiContext(options);
const queryKey = useGetUserKey(variables, options);
return reactQuery.useQuery<Schemas.User, GetUserError, TData>({
queryKey,
queryFn: ({signal}) => fetchGetUser({...fetcherOptions, ...variables}, signal),
...options,
...queryOptions,
});
};
That only expose the key value to the outside world, but doesn't change the internals (well it does call useApiContext
twice, but it shouldn't make any difference).
Btw @fabien0102, I don't thni the solution you proposed is ideal:
import { queryKeyFn } from "./myAppContext"
const { data } = useMyQuery(); // generated hook
const { mutateAsync } = useMyMutation({ // generated hook
onSuccess: () => {
queryClient.invalidateQueries(
queryKeyFn({ path: "/something", operationId: "something", variables: {} }); // Typescript should help you here
);
},
});
Because here, we (as end users) don't know the endpoint URL (its not exported either). Therefore, with this solution you have a high risk of not updating your (backend) code the day you change the endpoint's path from /something
to /something-else
.
On the other hand, if the generated code was simply exposing the path, it would solve this issue too, with the getUser
example, that would give:
export const GET_USER_OPERATION = {
path: '/user/{userId}',
operationId: getUser'
}
export const useGetUser = <TData = Schemas.User>(
variables: GetUserVariables,
options?: Omit<
reactQuery.UseQueryOptions<Schemas.User, GetUserError, TData>,
'queryKey' | 'queryFn' | 'initialData'
>
) => {
const {fetcherOptions, queryOptions, queryKeyFn} = useApiContext(options);
return reactQuery.useQuery<Schemas.User, GetUserError, TData>({
queryKey: queryKeyFn({
path: GET_USER_OPERATION.path,
operationId: GET_USER_OPERATION.operationId,
variables,
}),
queryFn: ({signal}) => fetchGetUser({...fetcherOptions, ...variables}, signal),
...options,
...queryOptions,
});
};
Or directly:
queryKeyFn({
...GET_USER_OPERATION,
variables,
})