Skip to content

Commit 1fe4418

Browse files
authored
feat(tanstack-query): default query/mutation options (#1260)
Fixes #1051 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Experimental centralized default options for procedures and routers, applied to queries, mutations, infinite/streamed/live operations and overridable per call. * **Tests** * Added tests covering default propagation, nested/router-level resolution, and per-call override behavior. * **Documentation** * Added a "Default Options" section to the TanStack Query integration docs (duplicated in two places). <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent b4897a0 commit 1fe4418

File tree

7 files changed

+774
-11
lines changed

7 files changed

+774
-11
lines changed

apps/content/docs/integrations/tanstack-query.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,47 @@ const query = useQuery(computed(
226226

227227
:::
228228

229+
## Default Options
230+
231+
You can configure default options for all query/mutation utilities using `experimental_defaults`. These options are [spread merged](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) with user-provided options, allowing you to set defaults while still enabling customization on a per-call basis.
232+
233+
```ts
234+
const orpc = createTanstackQueryUtils(client, {
235+
experimental_defaults: {
236+
planet: {
237+
find: {
238+
queryOptions: {
239+
staleTime: 60 * 1000, // 1 minute
240+
retry: 3,
241+
},
242+
},
243+
list: {
244+
infiniteOptions: {
245+
staleTime: 30 * 1000,
246+
},
247+
},
248+
create: {
249+
mutationOptions: {
250+
onSuccess: (output, input, _, ctx) => {
251+
ctx.client.invalidateQueries({ queryKey: orpc.planet.key() })
252+
},
253+
},
254+
},
255+
},
256+
},
257+
})
258+
259+
// These will automatically use the default options
260+
const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 } }))
261+
const mutation = useMutation(orpc.planet.create.mutationOptions())
262+
263+
// User-provided options override defaults
264+
const customQuery = useQuery(orpc.planet.find.queryOptions({
265+
input: { id: 123 },
266+
staleTime: 0, // overrides the default
267+
}))
268+
```
269+
229270
## Client Context
230271

231272
::: warning

packages/tanstack-query/src/procedure-utils.test-d.ts

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Client } from '@orpc/client'
22
import type { ErrorFromErrorMap } from '@orpc/contract'
33
import type { GetNextPageParamFunction, InfiniteData, InfiniteQueryObserverOptions, MutationObserverOptions, QueryFunction, QueryKey, QueryObserverOptions } from '@tanstack/query-core'
44
import type { baseErrorMap } from '../../contract/tests/shared'
5-
import type { ProcedureUtils } from './procedure-utils'
5+
import type { experimental_ProcedureUtilsDefaults, ProcedureUtils } from './procedure-utils'
66
import { QueryClient, skipToken } from '@tanstack/query-core'
77

88
describe('ProcedureUtils', () => {
@@ -746,3 +746,194 @@ describe('ProcedureUtils', () => {
746746
})
747747
})
748748
})
749+
750+
describe('ProcedureUtilsDefaults', () => {
751+
type TestClientContext = { batch?: boolean }
752+
type TestInput = { search?: string }
753+
type TestOutput = { title: string }
754+
type TestError = Error
755+
756+
type TestDefaults = experimental_ProcedureUtilsDefaults<TestClientContext, TestInput, TestOutput, TestError>
757+
758+
it('should have exact same keys as ProcedureUtils excluding call', () => {
759+
// Ensures every utility method in ProcedureUtils (except 'call') has a corresponding key in ProcedureUtilsDefaults
760+
type ProcedureUtilsKeys = Exclude<keyof ProcedureUtils<any, any, any, any>, 'call'>
761+
type DefaultsKeys = keyof experimental_ProcedureUtilsDefaults<any, any, any, any>
762+
763+
expectTypeOf<DefaultsKeys>().toEqualTypeOf<ProcedureUtilsKeys>()
764+
})
765+
766+
it('all properties should be optional', () => {
767+
const emptyDefaults: TestDefaults = {}
768+
expectTypeOf(emptyDefaults).toExtend<TestDefaults>()
769+
})
770+
771+
it('queryKey should accept Partial<QueryKeyOptions>', () => {
772+
const defaults: TestDefaults = {
773+
queryKey: {
774+
input: { search: 'test' },
775+
},
776+
}
777+
expectTypeOf(defaults).toExtend<TestDefaults>()
778+
779+
const _invalid: TestDefaults = {
780+
queryKey: {
781+
// @ts-expect-error - invalid input type
782+
input: { invalid: 'test' },
783+
},
784+
}
785+
})
786+
787+
it('queryOptions should accept Partial<QueryOptionsIn>', () => {
788+
const defaults: TestDefaults = {
789+
queryOptions: {
790+
input: { search: 'test' },
791+
staleTime: 1000,
792+
context: { batch: true },
793+
},
794+
}
795+
expectTypeOf(defaults).toExtend<TestDefaults>()
796+
797+
const _invalid: TestDefaults = {
798+
queryOptions: {
799+
// @ts-expect-error - invalid input type
800+
input: { invalid: 'test' },
801+
},
802+
}
803+
})
804+
805+
it('experimental_streamedKey should accept Partial<experimental_StreamedKeyOptions>', () => {
806+
const defaults: TestDefaults = {
807+
experimental_streamedKey: {
808+
input: { search: 'test' },
809+
queryFnOptions: { maxChunks: 10 },
810+
},
811+
}
812+
expectTypeOf(defaults).toExtend<TestDefaults>()
813+
814+
const _invalid: TestDefaults = {
815+
experimental_streamedKey: {
816+
// @ts-expect-error - invalid input type
817+
input: { invalid: 'test' },
818+
},
819+
}
820+
})
821+
822+
it('experimental_streamedOptions should accept Partial<StreamedOptionsIn>', () => {
823+
const defaults: TestDefaults = {
824+
experimental_streamedOptions: {
825+
input: { search: 'test' },
826+
staleTime: 1000,
827+
},
828+
}
829+
expectTypeOf(defaults).toExtend<TestDefaults>()
830+
831+
const _invalid: TestDefaults = {
832+
experimental_streamedOptions: {
833+
// @ts-expect-error - invalid input type
834+
input: { invalid: 'test' },
835+
},
836+
}
837+
})
838+
839+
it('experimental_liveKey should accept Partial<QueryKeyOptions>', () => {
840+
const defaults: TestDefaults = {
841+
experimental_liveKey: {
842+
input: { search: 'test' },
843+
},
844+
}
845+
expectTypeOf(defaults).toExtend<TestDefaults>()
846+
847+
const _invalid: TestDefaults = {
848+
experimental_liveKey: {
849+
// @ts-expect-error - invalid input type
850+
input: { invalid: 'test' },
851+
},
852+
}
853+
})
854+
855+
it('experimental_liveOptions should accept Partial<StreamedOptionsIn>', () => {
856+
const defaults: TestDefaults = {
857+
experimental_liveOptions: {
858+
input: { search: 'test' },
859+
staleTime: 1000,
860+
},
861+
}
862+
expectTypeOf(defaults).toExtend<TestDefaults>()
863+
864+
const _invalid: TestDefaults = {
865+
experimental_liveOptions: {
866+
// @ts-expect-error - invalid input type
867+
input: { invalid: 'test' },
868+
},
869+
}
870+
})
871+
872+
it('infiniteKey should accept Partial input, initialPageParam, queryKey', () => {
873+
const defaults: TestDefaults = {
874+
infiniteKey: {
875+
initialPageParam: 0,
876+
queryKey: ['custom-key'],
877+
},
878+
}
879+
expectTypeOf(defaults).toExtend<TestDefaults>()
880+
881+
const _invalid: TestDefaults = {
882+
infiniteKey: {
883+
// @ts-expect-error - invalid input type
884+
input: { invalid: 'test' },
885+
},
886+
}
887+
})
888+
889+
it('infiniteOptions should accept Partial<InfiniteOptionsIn>', () => {
890+
const defaults: TestDefaults = {
891+
infiniteOptions: {
892+
staleTime: 1000,
893+
},
894+
}
895+
expectTypeOf(defaults).toExtend<TestDefaults>()
896+
897+
const _invalid: TestDefaults = {
898+
infiniteOptions: {
899+
// @ts-expect-error - invalid input type
900+
input: { invalid: 'test' },
901+
},
902+
}
903+
})
904+
905+
it('mutationKey should accept Partial mutationKey', () => {
906+
const defaults: TestDefaults = {
907+
mutationKey: {
908+
mutationKey: ['custom-mutation-key'],
909+
},
910+
}
911+
expectTypeOf(defaults).toExtend<TestDefaults>()
912+
913+
const _invalid: TestDefaults = {
914+
mutationKey: {
915+
// @ts-expect-error - invalid mutationKey type
916+
mutationKey: 1,
917+
},
918+
}
919+
})
920+
921+
it('mutationOptions should accept Partial<MutationOptionsIn>', () => {
922+
const defaults: TestDefaults = {
923+
mutationOptions: {
924+
onSuccess: (output) => {
925+
expectTypeOf(output).toEqualTypeOf<TestOutput>()
926+
},
927+
context: { batch: true },
928+
},
929+
}
930+
expectTypeOf(defaults).toExtend<TestDefaults>()
931+
932+
const _invalid: TestDefaults = {
933+
mutationOptions: {
934+
// @ts-expect-error - invalid context type
935+
context: { batch: 'invalid' },
936+
},
937+
}
938+
})
939+
})

0 commit comments

Comments
 (0)