Custom queryKey generation for @tanstack/react-query plugin
The current implementation of queryKey generation in the @tanstack/react-query plugin includes the baseUrl in the queryKey:
[ { "_id": "getAllCountry", "baseUrl": "https://api.test.local/nsi" }]
This creates significant issues when working with multiple environments. For example:
server: http://localhost:8000/nsi/v1/countries client (brouser): https://api.test.local/nsi/v1/countries
Including the baseUrl in the queryKey means that:
Switching environments creates new cache entries instead of reusing existing ones Testing becomes more difficult since queryKeys differ between environments Manual cache manipulation requires knowing the exact baseUrl for each environment
Current Implementation The current implementation looks like this:
const createQueryKey = <TOptions extends Options>(
id: string,
options?: TOptions,
infinite?: boolean
): [QueryKey<TOptions>[0]] => {
const params: QueryKey<TOptions>[0] = {
_id: id,
baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl,
} as QueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (options?.body) {
params.body = options.body;
}
if (options?.headers) {
params.headers = options.headers;
}
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [params];
};
Feature Request I'd like the ability to customize the queryKey generation, particularly to exclude the baseUrl or modify how it's structured. Proposed Solution Add a queryKeyBuilder option to the @tanstack/react-query plugin config:
export default {
plugins: [
{
name: '@tanstack/react-query',
queryKeyBuilder: (id, options, infinite) => {
// Custom implementation to build queryKey that omits baseUrl
const params = { _id: id };
if (infinite) params._infinite = infinite;
if (options?.body) params.body = options.body;
if (options?.path) params.path = options.path;
if (options?.query) params.query = options.query;
return [params]; // Return without baseUrl
}
}
]
}
Or a simpler option to just disable baseUrl inclusion:
export default {
plugins: [
{
name: '@tanstack/react-query',
includeBaseUrlInQueryKey: false // Simple flag to exclude baseUrl
}
]
}
Impact
This enhancement would:
Improve cache consistency across environments
Reduce the size of queryKeys Make manual cache operations more predictable Support more complex caching strategies
Technical Details
The implementation would need to modify the queryKey generation to either:
Use a user-provided function for queryKey generation
Apply a set of configuration options to control what gets included
Workarounds
Until this feature is implemented, users need to either:
Override queryKey for each hook call
Use a global QueryClient with a custom queryKeyFactory Create wrapper hooks to transform the queryKeys
These solutions add extra code and complexity that could be avoided with a proper configuration option.
@mrlubos do you have any idea how to solve this without creating a wrapper hooks? as we are using this for both SSR and Client side, on the server side we have different baseURL while on the client it is going through our ingress. this is causing the queries to be executed twice, once on the server, and again on the client since the keys are different screwing the whole SSR :(
@omridevk can you explain why you're unable to set the same base url on both? If that's the key issue sounds like we might want to focus on that. Of course another option would be to remove base url from keys entirely..
@mrlubos I would suggest to remove the baseURL all together if we can, not sure if it needed to cache invalidation We are using k8s, and when I do SSR, since the server is inside the k8s cluster, I am using the k8s internal URL to make the API calls (setting the baseURL to an internal URL) On the client however, I cannot go through the internal URL since it is not available, rather i have to go through the ingress, thus having two different base URLs for each step, one for SSR and one for CSR
I don't want the server to go outside the cluster, doing egresses, this can increase our cost and can break some of our ingress rules
Sounds like offering an option to not include baseUrl would help your case. I'll need to check why we include it but I'm sure there was a reason, so making it optional should satisfy both requirements
Love it!
Feature Request: Customizable Query Key Generation in @tanstack/react-query Plugin
Problem
The current implementation of createQueryKey function automatically includes all request parameters in the key, including baseUrl and headers. This creates an issue when using the same request in different contexts (e.g., internal/external client) where these parameters might differ, but we want the query key to remain the same for proper caching.
Current Behavior
Generated code:
const createQueryKey = <TOptions extends Options>(
id: string,
options?: TOptions,
infinite?: boolean
): [QueryKey<TOptions>[0]] => {
const params: QueryKey<TOptions>[0] = {
admin: 'admin',
_id: id,
baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl,
} as QueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (options?.body) {
params.body = options.body;
}
if (options?.headers) {
params.headers = options.headers;
}
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [params];
};
###My code
//sdk/openapi-configs/_base.ts
const createQueryKey = <TOptions extends Options>(
id: string,
options?: TOptions,
infinite?: boolean
): [QueryKey<TOptions>[0]] => {
const params: QueryKey<TOptions>[0] = {
admin: 'admin',
_id: id,
// baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl,
} as QueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (options?.body) {
params.body = options.body;
}
// if (options?.headers) {
// params.headers = options.headers;
// }
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [params];
};
//sdk/openapi-configs/admin.ts
import { defineConfig } from '@hey-api/openapi-ts';
import { getConfig } from './_base';
export default defineConfig(getConfig('admin'));
//package.json
"scripts": {
....
"openapi-ts:admin": "openapi-ts --file openapi-configs/admin.config.ts",
....
}
Current Workaround
We have to manually comment out lines in the generated code after each generation:
// baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl,
// if (options?.headers) {
// params.headers = options.headers;
// }
Proposed Solution
Add a configuration option to the @tanstack/react-query plugin that allows customizing which fields should be included in the query key.
Option 1: Field whitelist
{
name: '@tanstack/react-query',
queryKeyFields: ['path', 'query', 'body'] // exclude baseUrl and headers
}
Option 2: Custom function
{
name: '@tanstack/react-query',
createQueryKey: (params) => {
// custom key creation logic
const { baseUrl, headers, ...keyParams } = params;
return keyParams;
}
}
Example Configuration
// sdk/openapi-configs/_base.ts
import { UserConfig } from '@hey-api/openapi-ts';
export const getConfig = (api: string): UserConfig => ({
input: `./src/swagger-docs/${api}.json`,
output: {
indexFile: false,
format: 'prettier',
lint: 'eslint',
path: `./src/openapi-ts/${api}`,
},
plugins: [
'@hey-api/client-next',
'@hey-api/schemas',
'zod',
{
dates: true,
name: '@hey-api/transformers',
},
{
enums: 'javascript',
name: '@hey-api/typescript',
},
{
name: '@hey-api/sdk',
},
{
name: '@tanstack/react-query',
queryKeyFields: ['path', 'query', 'body'] // <-- Proposed new option
}
],
});
Benefits
- No manual editing: Eliminates the need to manually edit generated code after each generation
- Flexibility: Provides control over cache management for different use cases
- Backward compatibility: Default behavior can include all fields to maintain compatibility
- Type safety: Configuration can be type-checked at compile time
Use Case
In our project, we use the same API endpoints with different clients (internal/external), and they have different base URLs and headers. However, we want them to share the same cache because the data is identical. Currently, this is impossible without manually modifying the generated code.
@SergGrey1992 can you update the original description? It's your issue! 😀
Also used this workaround for now:
import {hashKey, QueryClient} from '@tanstack/react-query'
new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: (args) => {
const key = args.map(({baseUrl, ...key}) => ({
...key,
}))
return hashKey(key)
},
},
},
})
FYI @SergGrey1992 if you want, you can use this workaround until the fix is merged
@mrlubos unfortunately this workaround doesn't really work as expected, the client is still being hydrated with the wrong baseURL causing the refetch to use the queryFn defined on the server :(
do you think a fix will be merged any time soon? allowing us to remove the baseURL as queryKey all together?
https://github.com/hey-api/openapi-ts/pull/1709 I see this PR this may help no?
@omridevk do you want to open a pull request adding a baseUrl option to queryKeys and infiniteQueryKeys? It would work the same way as tags, except we'd want it enabled by default to preserve the current functionality https://heyapi.dev/openapi-ts/plugins/tanstack-query#tags
Custom query key is a whole another beast. Adding the baseUrl option is the most straightforward fix for your issue
I have another use-case that I think would have been more easily solved with the custom key function.
The default query keys use a single composite object with baseUrl, _id (operationId), and all input parameters (query, path, body, headers) mashed into one object. However, tanstack query keys are also able to work hierarchically, allowing matching/filtering multiple queries via prefix elements at the start of the array. In my case, I would love to have been able to match against all queries in the cache that have the same operationId, regardless of the other parameters. If the keys had been set up in a hierarchy like [ baseUrl, id, params ] then it would be trivial to match and invalidate or refresh all queries with the same operation id, or even all queries from the same server (baseUrl).
As it is, I've worked around this by using a custom predicate to match the queries against the _id field. No further action needed for my case, but I'm posting this in case anyone else runs into something similar.
I have the same issue as original poster.
I need separate baseUrls because my React Router 7 server loader hits the direct API URL (api.myapp.com) while the browser hits a BFF proxy route (webapp.myapp.com/api) that parses auth cookies into bearer token and forwards to main api.
If I edit the generated file to delete the baseUrl out of the createQueryKey function, I get the desired behaviour — the server state matches the client state and no reload is attempted.
My workaround for now is to add this patch command to package.json:
{
"codegen:patch": "sed -i '' 's/, baseUrl: options\\?\\.baseUrl || (options\\?\\.client ?? _heyApiClient)\\.getConfig()\\.baseUrl//' ../src/api-client/generated/react-query/@tanstack/react-query.gen.ts"
}