snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
506
packages/solid-query/CHANGELOG.md
Normal file
506
packages/solid-query/CHANGELOG.md
Normal file
@@ -0,0 +1,506 @@
|
||||
# @tanstack/solid-query
|
||||
|
||||
## 5.102.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.8
|
||||
|
||||
## 5.102.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.7
|
||||
|
||||
## 5.102.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.6
|
||||
|
||||
## 5.102.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`578e5c2`](https://github.com/TanStack/query/commit/578e5c26e8ebd0d7351b4b8e2bafba695e672b8d)]:
|
||||
- @tanstack/query-core@5.102.5
|
||||
|
||||
## 5.102.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a05df6a`](https://github.com/TanStack/query/commit/a05df6aefb0e2489ec2c879ae16e2ee7cb3123ec)]:
|
||||
- @tanstack/query-core@5.102.4
|
||||
|
||||
## 5.102.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.3
|
||||
|
||||
## 5.102.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`80fbf73`](https://github.com/TanStack/query/commit/80fbf73e77892d702c107e14a84c219a8ed825dc)]:
|
||||
- @tanstack/query-core@5.102.2
|
||||
|
||||
## 5.102.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`134890d`](https://github.com/TanStack/query/commit/134890dbdc60e4fb0313e44b512d29284ca82f96)]:
|
||||
- @tanstack/query-core@5.102.1
|
||||
|
||||
## 5.102.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10373](https://github.com/TanStack/query/pull/10373) [`6e3d521`](https://github.com/TanStack/query/commit/6e3d521fe54f78624e53c1c6f9cadd678504bee8) - fix(types): propagate generic type parameters to `useMutationState` select callback
|
||||
|
||||
- [#11224](https://github.com/TanStack/query/pull/11224) [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b) - Fix `queryOptions` and `infiniteQueryOptions` return types so exported inferred options can be emitted in declaration files without leaking internal data tag symbols.
|
||||
|
||||
- [#8737](https://github.com/TanStack/query/pull/8737) [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e) - fix: make mutation variables optional when `undefined extends TVariables`
|
||||
|
||||
- [#11221](https://github.com/TanStack/query/pull/11221) [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93) - Remove experimental render-time prefetching and the `promise` property from query results.
|
||||
|
||||
- [#10571](https://github.com/TanStack/query/pull/10571) [`47d49d3`](https://github.com/TanStack/query/commit/47d49d31aa16684811257da071679d08d8726ddc) - Resolve the query client context outside reactive memo callbacks.
|
||||
|
||||
- Updated dependencies [[`34f7cee`](https://github.com/TanStack/query/commit/34f7ceed09c10e4a3aa2df31a106ddf02ec4e787), [`b4368c4`](https://github.com/TanStack/query/commit/b4368c43792349f6c29d1fb41f7ee1ef3a8bdd2c), [`5bb089d`](https://github.com/TanStack/query/commit/5bb089d153d33933983b7ea80f8d69b51d423698), [`ba4650c`](https://github.com/TanStack/query/commit/ba4650c05e61e33c609e051b948c9f7d31ce70d1), [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b), [`1f631b3`](https://github.com/TanStack/query/commit/1f631b3604aab6567fb3f6c90646a5a304641546), [`01a02bf`](https://github.com/TanStack/query/commit/01a02bfad32f9efbc17796be31d6399f1197a655), [`18c1c1e`](https://github.com/TanStack/query/commit/18c1c1ef94883e781ad36d36f9ea4a043ce4260b), [`5448063`](https://github.com/TanStack/query/commit/5448063f828d2270dccd41ae375e1afde35e91f2), [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e), [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93), [`5981771`](https://github.com/TanStack/query/commit/5981771abec9330b344a2617543eb05ba7c99e24), [`4a9bef6`](https://github.com/TanStack/query/commit/4a9bef6cf19b1cd6b014032d696129f12a848185), [`bef4bc7`](https://github.com/TanStack/query/commit/bef4bc780ce7cca32b7e3dea85f77d92f82a62a2), [`9656dc4`](https://github.com/TanStack/query/commit/9656dc4e5fef5f8c502579b52459f9e8c72e787c), [`326aaf1`](https://github.com/TanStack/query/commit/326aaf1333e5d9cbc46569c53f218d31684162d2), [`3e83601`](https://github.com/TanStack/query/commit/3e836010032c2586e9f1c66a271fa9114a6401f9), [`c6fc17c`](https://github.com/TanStack/query/commit/c6fc17cfad6e452261c585fabfb8c210e60531ed)]:
|
||||
- @tanstack/query-core@5.102.0
|
||||
|
||||
## 5.101.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.4
|
||||
|
||||
## 5.101.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]:
|
||||
- @tanstack/query-core@5.101.3
|
||||
|
||||
## 5.101.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.2
|
||||
|
||||
## 5.101.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]:
|
||||
- @tanstack/query-core@5.101.1
|
||||
|
||||
## 5.101.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.0
|
||||
|
||||
## 5.100.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.14
|
||||
|
||||
## 5.100.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d423168`](https://github.com/TanStack/query/commit/d423168f6261a5cb3d353e53b27c8150cc271151)]:
|
||||
- @tanstack/query-core@5.100.13
|
||||
|
||||
## 5.100.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.12
|
||||
|
||||
## 5.100.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.11
|
||||
|
||||
## 5.100.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(query-devtools): remove experimentalDts to prevent solid-js type leak ([#10694](https://github.com/TanStack/query/pull/10694))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.10
|
||||
|
||||
## 5.100.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fcee7bd`](https://github.com/TanStack/query/commit/fcee7bdc429385ae8ffa224fa8a7a9ec7b8ee380)]:
|
||||
- @tanstack/query-core@5.100.9
|
||||
|
||||
## 5.100.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.8
|
||||
|
||||
## 5.100.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.7
|
||||
|
||||
## 5.100.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- refactor(solid-query): remove 'Solid' prefix from 'SolidQueryOptions', 'SolidInfiniteQueryOptions', and 'SolidMutationOptions' ([#10500](https://github.com/TanStack/query/pull/10500))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.6
|
||||
|
||||
## 5.100.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a53ef97`](https://github.com/TanStack/query/commit/a53ef97f87decb8ea2431710f5199431d3c94c8d)]:
|
||||
- @tanstack/query-core@5.100.5
|
||||
|
||||
## 5.100.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.4
|
||||
|
||||
## 5.100.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f85d825`](https://github.com/TanStack/query/commit/f85d825e02efbbff02e2081528ed28f5e5382f7a)]:
|
||||
- @tanstack/query-core@5.100.3
|
||||
|
||||
## 5.100.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ea4497e`](https://github.com/TanStack/query/commit/ea4497e8aa00d8c1c3a36fb1e17563a889d6ab31), [`d6a7bf3`](https://github.com/TanStack/query/commit/d6a7bf3e3e024c1a77d0536813238cc8007a5fa7), [`645d5d1`](https://github.com/TanStack/query/commit/645d5d130f5e8017cb1bf1a37987f7b980aed705)]:
|
||||
- @tanstack/query-core@5.100.2
|
||||
|
||||
## 5.100.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1bb0d23`](https://github.com/TanStack/query/commit/1bb0d234280fd4ae1725c439088426a20593a8df)]:
|
||||
- @tanstack/query-core@5.100.1
|
||||
|
||||
## 5.100.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6540a41`](https://github.com/TanStack/query/commit/6540a4126b1c087d86d64525e78f32d9920dcd31)]:
|
||||
- @tanstack/query-core@5.100.0
|
||||
|
||||
## 5.99.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.2
|
||||
|
||||
## 5.99.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.1
|
||||
|
||||
## 5.99.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.0
|
||||
|
||||
## 5.98.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.98.0
|
||||
|
||||
## 5.97.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`2bfb12c`](https://github.com/TanStack/query/commit/2bfb12cc44f1d8495106136e4ddacb817135f8f9)]:
|
||||
- @tanstack/query-core@5.97.0
|
||||
|
||||
## 5.96.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.2
|
||||
|
||||
## 5.96.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.1
|
||||
|
||||
## 5.96.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.0
|
||||
|
||||
## 5.95.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`cd5a35b`](https://github.com/TanStack/query/commit/cd5a35b328837781aa4f9305bb2bd7877ca934e9)]:
|
||||
- @tanstack/query-core@5.95.2
|
||||
|
||||
## 5.95.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f1775c`](https://github.com/TanStack/query/commit/1f1775ca92f2b6c035682947ff3b3424804ff31a)]:
|
||||
- @tanstack/query-core@5.95.1
|
||||
|
||||
## 5.95.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.95.0
|
||||
|
||||
## 5.94.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(\*): resolve issue about excluded build directory ([#10312](https://github.com/TanStack/query/pull/10312))
|
||||
|
||||
- Updated dependencies [[`4b6536d`](https://github.com/TanStack/query/commit/4b6536dfce99036f4e37f52943c6fed3ad0e0a18)]:
|
||||
- @tanstack/query-core@5.94.5
|
||||
|
||||
## 5.94.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: fixed version ([#10064](https://github.com/TanStack/query/pull/10064))
|
||||
|
||||
- Updated dependencies [[`4c75210`](https://github.com/TanStack/query/commit/4c75210ce8235fe3d39b67e1029eff11278927cc)]:
|
||||
- @tanstack/query-core@5.94.4
|
||||
|
||||
## 5.91.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: stop node types from leaking into browser ([#10302](https://github.com/TanStack/query/pull/10302))
|
||||
|
||||
## 5.91.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(streamedQuery): maintain error state on reset refetch with initialData defined ([#10287](https://github.com/TanStack/query/pull/10287))
|
||||
|
||||
- Updated dependencies [[`248975e`](https://github.com/TanStack/query/commit/248975e896f585f6eaa505c796e73fcf7bfd1eec)]:
|
||||
- @tanstack/query-core@5.91.2
|
||||
|
||||
## 5.91.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(core): cancel paused initial fetch when last observer unsubscribes ([#10291](https://github.com/TanStack/query/pull/10291))
|
||||
|
||||
- Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]:
|
||||
- @tanstack/query-core@5.91.1
|
||||
|
||||
## 5.91.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]:
|
||||
- @tanstack/query-core@5.91.0
|
||||
|
||||
## 5.91.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat(solid-query): add 'mutationOptions' ([#10138](https://github.com/TanStack/query/pull/10138))
|
||||
|
||||
## 5.90.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(solid-router): should only have useQueryClient/useIsRestoring due to context read ([#10262](https://github.com/TanStack/query/pull/10262))
|
||||
|
||||
## 5.90.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: allow both create/use for solid ([#10260](https://github.com/TanStack/query/pull/10260))
|
||||
|
||||
## 5.90.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: forward generic parameters in deprecated type aliases ([#10093](https://github.com/TanStack/query/pull/10093))
|
||||
|
||||
## 5.90.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]:
|
||||
- @tanstack/query-core@5.90.20
|
||||
|
||||
## 5.90.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]:
|
||||
- @tanstack/query-core@5.90.19
|
||||
|
||||
## 5.90.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]:
|
||||
- @tanstack/query-core@5.90.18
|
||||
|
||||
## 5.90.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]:
|
||||
- @tanstack/query-core@5.90.17
|
||||
|
||||
## 5.90.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]:
|
||||
- @tanstack/query-core@5.90.16
|
||||
|
||||
## 5.90.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]:
|
||||
- @tanstack/query-core@5.90.15
|
||||
|
||||
## 5.90.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]:
|
||||
- @tanstack/query-core@5.90.14
|
||||
|
||||
## 5.90.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]:
|
||||
- @tanstack/query-core@5.90.13
|
||||
|
||||
## 5.90.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]:
|
||||
- @tanstack/query-core@5.90.12
|
||||
|
||||
## 5.90.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]:
|
||||
- @tanstack/query-core@5.90.11
|
||||
|
||||
## 5.90.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8e2e174`](https://github.com/TanStack/query/commit/8e2e174e9fd2e7b94cd232041e49c9d014d74e26), [`eb559a6`](https://github.com/TanStack/query/commit/eb559a66dc0d77dd46435f624fa64fc068bef9ae)]:
|
||||
- @tanstack/query-core@5.90.10
|
||||
|
||||
## 5.90.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]:
|
||||
- @tanstack/query-core@5.90.9
|
||||
|
||||
## 5.90.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]:
|
||||
- @tanstack/query-core@5.90.8
|
||||
|
||||
## 5.90.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]:
|
||||
- @tanstack/query-core@5.90.7
|
||||
|
||||
## 5.90.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Skip errors if returned data is undefined ([#9841](https://github.com/TanStack/query/pull/9841))
|
||||
|
||||
## 5.90.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]:
|
||||
- @tanstack/query-core@5.90.6
|
||||
|
||||
## 5.90.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(solid-query): enable experimental_prefetchInRender by default for… ([#9822](https://github.com/TanStack/query/pull/9822))
|
||||
|
||||
## 5.90.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]:
|
||||
- @tanstack/query-core@5.90.5
|
||||
|
||||
## 5.90.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]:
|
||||
- @tanstack/query-core@5.90.4
|
||||
|
||||
## 5.90.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]:
|
||||
- @tanstack/query-core@5.90.3
|
||||
62
packages/solid-query/README.md
Normal file
62
packages/solid-query/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />
|
||||
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://tanstack.com/api/readme/query.png?framework=solid&theme=dark"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://tanstack.com/api/readme/query.png?framework=solid"
|
||||
/>
|
||||
<img
|
||||
src="https://tanstack.com/api/readme/query.png?framework=solid"
|
||||
alt="TanStack Solid Query"
|
||||
width="900"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
Hooks for fetching, caching and updating asynchronous data in Solid
|
||||
|
||||
<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
|
||||
<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
|
||||
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
|
||||
</a><a href="https://github.com/TanStack/query/actions?query=workflow%3A%ci%22">
|
||||
<img src="https://github.com/TanStack/query/workflows/ci/badge.svg" />
|
||||
</a><a href="https://www.npmjs.com/package/@tanstack/query-core" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/query-core.svg" />
|
||||
</a><a href="https://bundlejs.com/?q=%40tanstack%2Fsolid-query&config=%7B%22esbuild%22%3A%7B%22external%22%3A%5B%22solid-js%22%2C%5D%7D%7D&badge=" target="\_parent">
|
||||
<img alt="" src="https://deno.bundlejs.com/?q=@tanstack/solid-query&config={%22esbuild%22:{%22external%22:[%22solid-js%22]}}&badge=detailed" />
|
||||
</a><a href="#badge">
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
</a><a href="https://github.com/TanStack/query/discussions">
|
||||
<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
|
||||
</a><a href="https://bestofjs.org/projects/tanstack-query"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%2Fquery%26since=daily" /></a><a href="https://github.com/TanStack/query/" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/github/stars/TanStack/query.svg?style=social&label=Star" />
|
||||
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
|
||||
</a> <a href="https://gitpod.io/from-referrer/">
|
||||
<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
|
||||
</a>
|
||||
|
||||
Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual)
|
||||
|
||||
## Visit [tanstack.com/query](https://tanstack.com/query) for docs, guides, API and more!
|
||||
|
||||
## Quick Features
|
||||
|
||||
- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
|
||||
- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
|
||||
- Parallel + Dependent Queries
|
||||
- Mutations + Reactive Query Refetching
|
||||
- Multi-layer Cache + Automatic Garbage Collection
|
||||
- Paginated + Cursor-based Queries
|
||||
- Load-More + Infinite Scroll Queries w/ Scroll Recovery
|
||||
- Request Cancellation
|
||||
- Suspense + Fetch-As-You-Render Query Prefetching
|
||||
- Dedicated Devtools
|
||||
|
||||
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
|
||||
|
||||
<!-- Use the force, Luke -->
|
||||
5
packages/solid-query/eslint.config.js
Normal file
5
packages/solid-query/eslint.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
import rootConfig from './root.eslint.config.js'
|
||||
|
||||
export default [...rootConfig]
|
||||
79
packages/solid-query/package.json
Normal file
79
packages/solid-query/package.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "@tanstack/solid-query",
|
||||
"version": "5.102.8",
|
||||
"description": "Primitives for managing, caching and syncing asynchronous and remote data in Solid",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/solid-query"
|
||||
},
|
||||
"homepage": "https://tanstack.com/query",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "premove ./build ./coverage ./dist-ts ./.cache/test-types",
|
||||
"compile": "tsc --project tsconfig.json",
|
||||
"test:eslint": "eslint --concurrency=auto ./src",
|
||||
"test:types": "npm-run-all --serial test:types:*",
|
||||
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts56 --tsBuildInfoFile .cache/test-types/ts56.tsbuildinfo",
|
||||
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts57 --tsBuildInfoFile .cache/test-types/ts57.tsbuildinfo",
|
||||
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts58 --tsBuildInfoFile .cache/test-types/ts58.tsbuildinfo",
|
||||
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts59 --tsBuildInfoFile .cache/test-types/ts59.tsbuildinfo",
|
||||
"test:types:tscurrent": "tsc --project tsconfig.json --outDir .cache/test-types/tscurrent --tsBuildInfoFile .cache/test-types/tscurrent.tsbuildinfo",
|
||||
"test:types:ts70": "node ../../node_modules/typescript70/lib/tsc.js --project tsconfig.legacy.json --outDir .cache/test-types/ts70 --tsBuildInfoFile .cache/test-types/ts70.tsbuildinfo",
|
||||
"test:lib": "vitest --retry=3",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "tsdown --tsconfig tsconfig.prod.json",
|
||||
"build:watch": "tsdown --watch"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./build/index.cjs",
|
||||
"module": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"browser": {},
|
||||
"exports": {
|
||||
"@tanstack/custom-condition": "./src/index.ts",
|
||||
"development": {
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/dev.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/index.d.cts",
|
||||
"default": "./build/dev.cjs"
|
||||
}
|
||||
},
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/index.d.cts",
|
||||
"default": "./build/index.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"build",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@solidjs/testing-library": "^0.8.10",
|
||||
"@tanstack/query-test-utils": "workspace:*",
|
||||
"npm-run-all2": "^5.0.0",
|
||||
"solid-js": "^1.9.7",
|
||||
"unplugin-solid": "^2.0.0",
|
||||
"vite-plugin-solid": "^2.11.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.6.0"
|
||||
}
|
||||
}
|
||||
1
packages/solid-query/root.eslint.config.js
Symbolic link
1
packages/solid-query/root.eslint.config.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../eslint.config.js
|
||||
117
packages/solid-query/src/QueryClient.ts
Normal file
117
packages/solid-query/src/QueryClient.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { QueryClient as QueryCoreClient } from '@tanstack/query-core'
|
||||
import type {
|
||||
DefaultOptions as CoreDefaultOptions,
|
||||
DefaultError,
|
||||
OmitKeyof,
|
||||
QueryClientConfig as QueryCoreClientConfig,
|
||||
InfiniteQueryObserverOptions as QueryCoreInfiniteQueryObserverOptions,
|
||||
QueryObserverOptions as QueryCoreObserverOptions,
|
||||
QueryKey,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* The core `QueryObserverOptions`, with Solid's `reconcile` option added.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryData - The type of the data actually held in the query cache.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page, when this type
|
||||
* is shared with an infinite query's observer options. Defaults to `never` for regular queries.
|
||||
*/
|
||||
export interface QueryObserverOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = never,
|
||||
> extends OmitKeyof<
|
||||
QueryCoreObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
'structuralSharing'
|
||||
> {
|
||||
/**
|
||||
* Set this to a reconciliation key to enable reconciliation between query results.
|
||||
* Set this to `false` to disable reconciliation between query results.
|
||||
* Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom reconciliation logic.
|
||||
* Defaults reconciliation to false.
|
||||
*/
|
||||
reconcile?:
|
||||
| string
|
||||
| false
|
||||
| ((oldData: TData | undefined, newData: TData) => TData)
|
||||
}
|
||||
|
||||
/**
|
||||
* The core `InfiniteQueryObserverOptions`, with Solid's `reconcile` option added.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export interface InfiniteQueryObserverOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> extends OmitKeyof<
|
||||
QueryCoreInfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
'structuralSharing'
|
||||
> {
|
||||
/**
|
||||
* Set this to a reconciliation key to enable reconciliation between query results.
|
||||
* Set this to `false` to disable reconciliation between query results.
|
||||
* Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom reconciliation logic.
|
||||
* Defaults reconciliation to false.
|
||||
*/
|
||||
reconcile?:
|
||||
| string
|
||||
| false
|
||||
| ((oldData: TData | undefined, newData: TData) => TData)
|
||||
}
|
||||
|
||||
/**
|
||||
* The default options a `QueryClient` applies to every query, with Solid's `reconcile` option added to
|
||||
* `queries`.
|
||||
*
|
||||
* @template TError - The default type of errors thrown by queries and mutations using this `QueryClient`.
|
||||
*/
|
||||
export interface DefaultOptions<
|
||||
TError = DefaultError,
|
||||
> extends CoreDefaultOptions<TError> {
|
||||
queries?: OmitKeyof<QueryObserverOptions<unknown, TError>, 'queryKey'>
|
||||
}
|
||||
|
||||
/**
|
||||
* The config accepted by `new QueryClient(config)`, with Solid's extended {@link DefaultOptions}.
|
||||
*/
|
||||
export interface QueryClientConfig extends QueryCoreClientConfig {
|
||||
defaultOptions?: DefaultOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* The core `@tanstack/query-core` `QueryClient`, typed so its `defaultOptions.queries` accepts Solid's
|
||||
* `reconcile` option.
|
||||
*/
|
||||
export class QueryClient extends QueryCoreClient {
|
||||
constructor(config: QueryClientConfig = {}) {
|
||||
super(config)
|
||||
}
|
||||
}
|
||||
110
packages/solid-query/src/QueryClientProvider.tsx
Normal file
110
packages/solid-query/src/QueryClientProvider.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
createContext,
|
||||
createRenderEffect,
|
||||
onCleanup,
|
||||
useContext,
|
||||
} from 'solid-js'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type { Accessor, JSX } from 'solid-js'
|
||||
|
||||
/**
|
||||
* The context that `useQueryClient` reads from. `QueryClientProvider` is the normal way to set it.
|
||||
*/
|
||||
export const QueryClientContext = createContext<
|
||||
(() => QueryClient) | undefined
|
||||
>(undefined)
|
||||
|
||||
const queryClientContextError =
|
||||
'No QueryClient set, use QueryClientProvider to set one'
|
||||
|
||||
/**
|
||||
* The `useQueryClient` hook returns the current `QueryClient` instance.
|
||||
*
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns The current `QueryClient` instance.
|
||||
* @throws If no `queryClient` argument is passed and no `QueryClientProvider` is found in the component tree.
|
||||
*/
|
||||
export const useQueryClient = (queryClient?: QueryClient) => {
|
||||
if (queryClient) {
|
||||
return queryClient
|
||||
}
|
||||
const client = useContext(QueryClientContext)
|
||||
|
||||
if (!client) {
|
||||
throw new Error(queryClientContextError)
|
||||
}
|
||||
|
||||
return client()
|
||||
}
|
||||
|
||||
export const useQueryClientResolver = (
|
||||
queryClient?: Accessor<QueryClient | undefined>,
|
||||
): Accessor<QueryClient> => {
|
||||
const contextClient = useContext(QueryClientContext)
|
||||
|
||||
return () => {
|
||||
const resolvedClient = queryClient?.()
|
||||
if (resolvedClient) {
|
||||
return resolvedClient
|
||||
}
|
||||
|
||||
if (!contextClient) {
|
||||
throw new Error(queryClientContextError)
|
||||
}
|
||||
|
||||
return contextClient()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The props accepted by `QueryClientProvider`.
|
||||
*/
|
||||
export type QueryClientProviderProps = {
|
||||
/**
|
||||
* **Required**
|
||||
*
|
||||
* The `QueryClient` instance to provide.
|
||||
*/
|
||||
client: QueryClient
|
||||
/**
|
||||
* The components that get access to the provided `QueryClient`.
|
||||
*/
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the `QueryClientProvider` component to connect and provide a `QueryClient` to your application. Also
|
||||
* calls `client.mount()`/`client.unmount()` as this component mounts/unmounts, which subscribes the client to
|
||||
* focus/online events (resuming any paused mutations and refetching as needed when the app regains focus or
|
||||
* comes back online).
|
||||
*
|
||||
* @returns The provided `children`, wrapped so they can read the `QueryClient` via `useQueryClient`.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
|
||||
*
|
||||
* const queryClient = new QueryClient()
|
||||
*
|
||||
* function App() {
|
||||
* return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const QueryClientProvider = (
|
||||
props: QueryClientProviderProps,
|
||||
): JSX.Element => {
|
||||
createRenderEffect<() => void>((unmount) => {
|
||||
unmount?.()
|
||||
props.client.mount()
|
||||
return props.client.unmount.bind(props.client)
|
||||
})
|
||||
onCleanup(() => props.client.unmount())
|
||||
|
||||
return (
|
||||
<QueryClientContext.Provider value={() => props.client}>
|
||||
{props.children}
|
||||
</QueryClientContext.Provider>
|
||||
)
|
||||
}
|
||||
225
packages/solid-query/src/__tests__/QueryClientProvider.test.tsx
Normal file
225
packages/solid-query/src/__tests__/QueryClientProvider.test.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render } from '@solidjs/testing-library'
|
||||
import { QueryCache } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { createMemo, createRoot } from 'solid-js'
|
||||
import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '..'
|
||||
import { useQueryClientResolver } from '../QueryClientProvider'
|
||||
|
||||
describe('QueryClientProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('sets a specific cache for all queries to use', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryCache = new QueryCache()
|
||||
const queryClient = new QueryClient({ queryCache })
|
||||
|
||||
function Page() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Page />
|
||||
</QueryClientProvider>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('test')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.state.data).toBe('test')
|
||||
})
|
||||
|
||||
it('allows multiple caches to be partitioned', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const queryCache1 = new QueryCache()
|
||||
const queryCache2 = new QueryCache()
|
||||
|
||||
const queryClient1 = new QueryClient({ queryCache: queryCache1 })
|
||||
const queryClient2 = new QueryClient({ queryCache: queryCache2 })
|
||||
|
||||
function Page1() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 'test1'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function Page2() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(10).then(() => 'test2'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => (
|
||||
<>
|
||||
<QueryClientProvider client={queryClient1}>
|
||||
<Page1 />
|
||||
</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient2}>
|
||||
<Page2 />
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('test1')).toBeInTheDocument()
|
||||
expect(rendered.getByText('test2')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache1.find({ queryKey: key1 })?.state.data).toBe('test1')
|
||||
expect(queryCache1.find({ queryKey: key2 })).toBeUndefined()
|
||||
expect(queryCache2.find({ queryKey: key1 })).toBeUndefined()
|
||||
expect(queryCache2.find({ queryKey: key2 })?.state.data).toBe('test2')
|
||||
})
|
||||
|
||||
it("uses defaultOptions for queries when they don't provide their own config", async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryCache = new QueryCache()
|
||||
const queryClient = new QueryClient({
|
||||
queryCache,
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function Page() {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{query.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Page />
|
||||
</QueryClientProvider>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('test')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity)
|
||||
})
|
||||
|
||||
describe('useQueryClient', () => {
|
||||
it('should throw an error if no query client has been set', () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
useQueryClient()
|
||||
return null
|
||||
}
|
||||
|
||||
expect(() => render(() => <Page />)).toThrow(
|
||||
'No QueryClient set, use QueryClientProvider to set one',
|
||||
)
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw an error if user provides custom query client', () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const client = new QueryClient()
|
||||
useQueryClient(client)
|
||||
return null
|
||||
}
|
||||
|
||||
render(() => <Page />)
|
||||
expect(consoleMock).not.toHaveBeenCalled()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('creates a query client resolver that is safe to call in reactive callbacks', () => {
|
||||
const queryClient = new QueryClient()
|
||||
let resolveClient!: () => QueryClient
|
||||
|
||||
function Page() {
|
||||
resolveClient = useQueryClientResolver()
|
||||
return null
|
||||
}
|
||||
|
||||
render(() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Page />
|
||||
</QueryClientProvider>
|
||||
))
|
||||
|
||||
createRoot((dispose) => {
|
||||
const client = createMemo(() => resolveClient())
|
||||
|
||||
expect(client()).toBe(queryClient)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it('defers missing provider errors until a resolver is called', () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
let resolveClient!: () => QueryClient
|
||||
|
||||
function Page() {
|
||||
resolveClient = useQueryClientResolver()
|
||||
return null
|
||||
}
|
||||
|
||||
expect(() => render(() => <Page />)).not.toThrow()
|
||||
|
||||
expect(() =>
|
||||
createRoot((dispose) => {
|
||||
const client = createMemo(() => resolveClient())
|
||||
client()
|
||||
dispose()
|
||||
}),
|
||||
).toThrow('No QueryClient set, use QueryClientProvider to set one')
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
import type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from '../infiniteQueryOptions'
|
||||
|
||||
// Regression test for exported infiniteQueryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedInfiniteQueryOptions = infiniteQueryOptions({
|
||||
queryKey: ['invalid'],
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should work when passed to infiniteQuery', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<{ wow: boolean }, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with select', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
select: (data) => data.pages,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Array<{ wow: boolean }>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with enabled: false', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<{ wow: boolean }, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with skipToken', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: ['key'],
|
||||
queryFn: skipToken,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<unknown, number>>()
|
||||
})
|
||||
|
||||
it('should infer defined types', () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => 10,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: {
|
||||
pageParams: [undefined],
|
||||
pages: [{ wow: true }],
|
||||
},
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
expectTypeOf(useInfiniteQuery(() => options).data).toEqualTypeOf<
|
||||
InfiniteData<{ wow: boolean }, unknown>
|
||||
>()
|
||||
|
||||
expectTypeOf(options).toExtend<
|
||||
ReturnType<
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
{ wow: boolean },
|
||||
Error,
|
||||
InfiniteData<{ wow: boolean }, unknown>,
|
||||
Array<string>,
|
||||
number | undefined
|
||||
>
|
||||
>
|
||||
>()
|
||||
|
||||
expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<
|
||||
InfiniteData<{ wow: boolean }>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should work without defined types', () => {
|
||||
const options = infiniteQueryOptions({
|
||||
getNextPageParam: () => undefined,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
expectTypeOf(() => useInfiniteQuery(() => options).data).toEqualTypeOf<
|
||||
() => InfiniteData<{ wow: boolean }, unknown> | undefined
|
||||
>()
|
||||
|
||||
expectTypeOf(options).toExtend<
|
||||
ReturnType<
|
||||
UndefinedInitialDataInfiniteOptions<
|
||||
{ wow: boolean },
|
||||
Error,
|
||||
InfiniteData<{ wow: boolean }, unknown>,
|
||||
Array<string>,
|
||||
number
|
||||
>
|
||||
>
|
||||
>()
|
||||
|
||||
expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<
|
||||
InfiniteData<{
|
||||
wow: boolean
|
||||
}>
|
||||
>()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { infiniteQueryOptions } from '../infiniteQueryOptions'
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object = {
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
getNextPageParam: () => null,
|
||||
initialPageParam: null,
|
||||
} as const
|
||||
|
||||
expect(infiniteQueryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
218
packages/solid-query/src/__tests__/mutationOptions.test-d.tsx
Normal file
218
packages/solid-query/src/__tests__/mutationOptions.test-d.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useIsMutating, useMutation, useMutationState } from '..'
|
||||
import { mutationOptions } from '../mutationOptions'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationState,
|
||||
WithRequired,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MutationOptions, UseMutationResult } from '../types'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error this is a good error, because onMutates does not exist!
|
||||
onMutates: 1000,
|
||||
mutationKey: queryKey(),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for callbacks', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: queryKey(),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for onError callback', () => {
|
||||
mutationOptions({
|
||||
mutationFn: () => {
|
||||
throw new Error('fail')
|
||||
},
|
||||
mutationKey: queryKey(),
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for variables', () => {
|
||||
mutationOptions<number, DefaultError, { id: string }>({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: queryKey(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer result type correctly', () => {
|
||||
mutationOptions<number, DefaultError, void, { name: string }>({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: queryKey(),
|
||||
onMutate: () => {
|
||||
return { name: 'onMutateResult' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer context type correctly', () => {
|
||||
mutationOptions<number>({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: queryKey(),
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should error if mutationFn return type mismatches TData', () => {
|
||||
assertType(
|
||||
mutationOptions<number>({
|
||||
// @ts-expect-error this is a good error, because return type is string, not number
|
||||
mutationFn: async () => Promise.resolve('wrong return'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow mutationKey to be omitted', () => {
|
||||
return mutationOptions({
|
||||
mutationFn: () => Promise.resolve(123),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer all types when not explicitly provided', () => {
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
mutationKey: queryKey(),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
WithRequired<MutationOptions<number, DefaultError, string>, 'mutationKey'>
|
||||
>()
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
Omit<MutationOptions<number, DefaultError, string>, 'mutationKey'>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should infer types when used with useMutation', () => {
|
||||
const mutation = useMutation(() =>
|
||||
mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
UseMutationResult<string, DefaultError, void, unknown>
|
||||
>()
|
||||
|
||||
useMutation(() =>
|
||||
// should allow when used with useMutation without mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types when used with useIsMutating', () => {
|
||||
const isMutating = useIsMutating(() =>
|
||||
mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<() => number>()
|
||||
|
||||
useIsMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
() =>
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types when used with queryClient.isMutating', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const isMutating = queryClient.isMutating(
|
||||
mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<number>()
|
||||
|
||||
queryClient.isMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types when used with useMutationState', () => {
|
||||
const mutationState = useMutationState(() => ({
|
||||
filters: mutationOptions({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
}))
|
||||
expectTypeOf(mutationState).toEqualTypeOf<
|
||||
() => Array<MutationState<unknown, Error, unknown, unknown>>
|
||||
>()
|
||||
|
||||
useMutationState(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
() => ({
|
||||
filters: mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
511
packages/solid-query/src/__tests__/mutationOptions.test.tsx
Normal file
511
packages/solid-query/src/__tests__/mutationOptions.test.tsx
Normal file
@@ -0,0 +1,511 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import { createEffect, createRenderEffect } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, useIsMutating, useMutation, useMutationState } from '..'
|
||||
import { mutationOptions } from '../mutationOptions'
|
||||
import { renderWithClient } from './utils'
|
||||
import type { MutationState } from '@tanstack/query-core'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating()
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[1]).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[2]).toEqual(0)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating()
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[1]).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[2]).toEqual(0)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating()
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[2]).toEqual(2)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const isMutating = useIsMutating(() => ({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}))
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(isMutatingArray[1]).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(isMutatingArray[2]).toEqual(0)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating(mutationOpts))
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating(mutationOpts))
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
})
|
||||
|
||||
isMutatingArray.push(queryClient.isMutating())
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(2)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
isMutatingArray.push(
|
||||
queryClient.isMutating({
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
}),
|
||||
)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
expect(isMutatingArray[0]).toEqual(0)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
// Use Math.max because subscribe callback count is implementation-dependent
|
||||
expect(Math.max(...isMutatingArray)).toEqual(1)
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { mutationKey: mutationOpts.mutationKey, status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(1)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate } = useMutation(() => mutationOpts)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate()}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(1)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(2)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data1')
|
||||
expect(lastSnapshot[1]?.data).toEqual('data2')
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => {
|
||||
const mutationStateArray: Array<Array<MutationState>> = []
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
function Mutation() {
|
||||
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
|
||||
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
|
||||
const states = useMutationState(() => ({
|
||||
filters: { mutationKey: mutationOpts1.mutationKey, status: 'success' },
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutationStateArray.push(states())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutate1()}>mutate1</button>
|
||||
<button onClick={() => mutate2()}>mutate2</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Mutation />)
|
||||
|
||||
expect(mutationStateArray[0]).toEqual([])
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
|
||||
expect(lastSnapshot.length).toEqual(1)
|
||||
expect(lastSnapshot[0]?.data).toEqual('data1')
|
||||
})
|
||||
})
|
||||
209
packages/solid-query/src/__tests__/queryOptions.test-d.tsx
Normal file
209
packages/solid-query/src/__tests__/queryOptions.test-d.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import {
|
||||
QueryClient,
|
||||
dataTagErrorSymbol,
|
||||
dataTagSymbol,
|
||||
skipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useQuery } from '../useQuery'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
|
||||
// Regression test for exported queryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedQueryOptions = queryOptions({
|
||||
queryKey: ['invalid'],
|
||||
})
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
assertType(
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error this is a good error, because stallTime does not exist!
|
||||
stallTime: 1000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
it('should infer types for callbacks', () => {
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
staleTime: 1000,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
it('should work when passed to useQuery', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const { data } = useQuery(() => options)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should work when passed to query', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should work when passed to query with select', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
})
|
||||
it('should work when passed to query with enabled: false', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should work when passed to query with skipToken', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: skipToken,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
it('should work when passed to fetchQuery', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const data = await new QueryClient().fetchQuery(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey even if no promise is returned', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => 5,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with unknown if there is no queryFn', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<unknown>()
|
||||
})
|
||||
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
it('should tag the queryKey with the default error type', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagErrorSymbol]).toEqualTypeOf<Error>()
|
||||
})
|
||||
it('should return the proper type when passed to getQueryData', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(tagged)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should return the proper type when passed to getQueryState', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const state = queryClient.getQueryState(tagged)
|
||||
expectTypeOf(state?.data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should properly type updaterFn when passed to setQueryData', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(tagged, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
it('should properly type value when passed to setQueryData', () => {
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, '5')
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, () => '5')
|
||||
|
||||
const data = queryClient.setQueryData(tagged, 5)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer even if there is a conditional skipToken', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(options.queryKey)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer to unknown if we disable a query with just a skipToken', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: skipToken,
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(options.queryKey)
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
})
|
||||
13
packages/solid-query/src/__tests__/queryOptions.test.tsx
Normal file
13
packages/solid-query/src/__tests__/queryOptions.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { queryOptions } from '../queryOptions'
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object = {
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
} as const
|
||||
|
||||
expect(queryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
898
packages/solid-query/src/__tests__/suspense.test.tsx
Normal file
898
packages/solid-query/src/__tests__/suspense.test.tsx
Normal file
@@ -0,0 +1,898 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import {
|
||||
ErrorBoundary,
|
||||
Show,
|
||||
Suspense,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
on,
|
||||
} from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryCache, QueryClient, useInfiniteQuery, useQuery } from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from '..'
|
||||
|
||||
describe("useQuery's in Suspense mode", () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should render the correct amount of times in Suspense mode', async () => {
|
||||
const key = queryKey()
|
||||
const states: Array<UseQueryResult<number>> = []
|
||||
|
||||
let count = 0
|
||||
let renders = 0
|
||||
|
||||
function Page() {
|
||||
const [stateKey, setStateKey] = createSignal(key)
|
||||
|
||||
const state = useQuery(() => ({
|
||||
queryKey: stateKey(),
|
||||
queryFn: () => sleep(10).then(() => ++count),
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
states.push({ ...state })
|
||||
})
|
||||
|
||||
createRenderEffect(
|
||||
on([() => ({ ...state }), () => key], () => {
|
||||
renders++
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
|
||||
data: {String(state.data)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 1')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 2')).toBeInTheDocument()
|
||||
|
||||
expect(renders).toBe(4)
|
||||
expect(states.length).toBe(4)
|
||||
expect(states[1]).toMatchObject({ data: 1, status: 'success' })
|
||||
expect(states[3]).toMatchObject({ data: 2, status: 'success' })
|
||||
})
|
||||
|
||||
it('should return the correct states for a successful infinite query', async () => {
|
||||
const key = queryKey()
|
||||
const states: Array<UseInfiniteQueryResult<InfiniteData<number>>> = []
|
||||
|
||||
function Page() {
|
||||
const [multiplier, setMultiplier] = createSignal(1)
|
||||
const state = useInfiniteQuery(() => ({
|
||||
queryKey: [`${key}_${multiplier()}`],
|
||||
queryFn: ({ pageParam }) =>
|
||||
sleep(10).then(() => pageParam * multiplier()),
|
||||
initialPageParam: 1,
|
||||
suspense: true,
|
||||
getNextPageParam: (lastPage) => lastPage + 1,
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
states.push({ ...state })
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setMultiplier(2)}>next</button>
|
||||
data: {state.data?.pages.join(',')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 1')).toBeInTheDocument()
|
||||
// eslint-disable-next-line cspell/spellchecker
|
||||
// TODO(lukemurray): in react this is 1 in solid this is 2 because suspense
|
||||
// occurs on read.
|
||||
expect(states.length).toBe(2)
|
||||
expect(states[1]).toMatchObject({
|
||||
data: { pages: [1], pageParams: [1] },
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByText('next'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: 2')).toBeInTheDocument()
|
||||
// eslint-disable-next-line cspell/spellchecker
|
||||
// TODO(lukemurray): in react this is 2 and in solid it is 4
|
||||
expect(states.length).toBe(4)
|
||||
expect(states[3]).toMatchObject({
|
||||
data: { pages: [2], pageParams: [1] },
|
||||
status: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call the queryFn twice when used in Suspense mode', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryFn = vi.fn(() => sleep(10).then(() => 'data'))
|
||||
|
||||
function Page() {
|
||||
useQuery(() => ({
|
||||
queryKey: [key],
|
||||
queryFn,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return <>rendered</>
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should remove query instance when component unmounted', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
return <>rendered</>
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [show, setShow] = createSignal(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback="loading">{show() && <Page />}</Suspense>
|
||||
<button
|
||||
aria-label="toggle"
|
||||
onClick={() => setShow((prev) => !prev)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.queryByText('rendered')).not.toBeInTheDocument()
|
||||
expect(queryCache.find({ queryKey: key })).toBeUndefined()
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(1)
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
expect(rendered.queryByText('rendered')).not.toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(0)
|
||||
})
|
||||
|
||||
// https://github.com/tannerlinsley/react-query/issues/468
|
||||
it('should reset error state if new component instances are mounted', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
let succeed = false
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return 'data'
|
||||
}),
|
||||
retryDelay: 10,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
// Suspense only triggers if used in JSX
|
||||
return (
|
||||
<Show when={state.data}>
|
||||
<div>rendered</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<ErrorBoundary
|
||||
fallback={(_err, resetSolid) => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
succeed = true
|
||||
resetSolid()
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
expect(rendered.getByText('retry')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('retry'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should retry fetch if the reset error boundary has been reset', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
let succeed = false
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return 'data'
|
||||
}),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
// Suspense only triggers if used in JSX
|
||||
return (
|
||||
<Show when={state.data}>
|
||||
<div>rendered</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<ErrorBoundary
|
||||
fallback={(_err, resetSolid) => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
resetSolid()
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
expect(rendered.getByText('retry')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('retry'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
expect(rendered.getByText('retry')).toBeInTheDocument()
|
||||
|
||||
succeed = true
|
||||
|
||||
fireEvent.click(rendered.getByText('retry'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should refetch when re-mounting', async () => {
|
||||
const key = queryKey()
|
||||
let count = 0
|
||||
|
||||
function Component() {
|
||||
const result = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(100).then(() => ++count),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
staleTime: 0,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>data: {result.data}</span>
|
||||
<span>fetching: {result.isFetching ? 'true' : 'false'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [show, setShow] = createSignal(true)
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShow(!show())
|
||||
}}
|
||||
>
|
||||
{show() ? 'hide' : 'show'}
|
||||
</button>
|
||||
<Suspense fallback="loading">{show() && <Component />}</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('data: 1')).toBeInTheDocument()
|
||||
expect(rendered.getByText('fetching: false')).toBeInTheDocument()
|
||||
expect(rendered.getByText('hide')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('hide'))
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('show')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('show'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('fetching: true')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('data: 2')).toBeInTheDocument()
|
||||
expect(rendered.getByText('fetching: false')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should suspend when switching to a new query', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
function Component(props: { queryKey: Array<string> }) {
|
||||
const result = useQuery(() => ({
|
||||
queryKey: props.queryKey,
|
||||
queryFn: () => sleep(100).then(() => props.queryKey),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return <div>data: {result.data}</div>
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [key, setKey] = createSignal(key1)
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setKey(key2)
|
||||
}}
|
||||
>
|
||||
switch
|
||||
</button>
|
||||
<Suspense fallback="loading">
|
||||
<Component queryKey={key()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText(`data: ${key1}`)).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByText('switch'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText(`data: ${key2}`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should throw errors to the error boundary by default', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Suspense Error a1x'))),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should not throw errors to the error boundary when throwOnError: false', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Suspense Error a2x'))),
|
||||
retry: false,
|
||||
throwOnError: false,
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should throw errors to the error boundary when a throwOnError function returns true', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Remote Error'))),
|
||||
retry: false,
|
||||
throwOnError: (err) => err.message !== 'Local Error',
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should not throw errors to the error boundary when a throwOnError function returns false', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Local Error'))),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
throwOnError: (err) => err.message !== 'Local Error',
|
||||
}))
|
||||
|
||||
// read state.data to trigger suspense.
|
||||
createRenderEffect(() => {
|
||||
state.data
|
||||
})
|
||||
|
||||
return <div>rendered</div>
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={() => (
|
||||
<div>
|
||||
<div>error boundary</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not call the queryFn when not enabled', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const queryFn = vi.fn(() => sleep(10).then(() => '23'))
|
||||
|
||||
function Page() {
|
||||
const [enabled, setEnabled] = createSignal(false)
|
||||
|
||||
const result = useQuery(() => ({
|
||||
queryKey: [key],
|
||||
queryFn,
|
||||
suspense: true,
|
||||
enabled: enabled(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setEnabled(true)}>fire</button>
|
||||
<h1>{result.data}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
fireEvent.click(rendered.getByRole('button', { name: /fire/i }))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByRole('heading').textContent).toBe('23')
|
||||
expect(queryFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should error caught in error boundary without infinite loop', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
let succeed = true
|
||||
|
||||
function Page() {
|
||||
const [nonce] = createSignal(0)
|
||||
const queryKeys = [`${key}-${succeed}`]
|
||||
|
||||
const result = useQuery(() => ({
|
||||
queryKey: queryKeys,
|
||||
queryFn: () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return nonce()
|
||||
}),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span> <span>{result.data}</span>
|
||||
<button aria-label="fail" onClick={() => queryClient.resetQueries()}>
|
||||
fail
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => <div>error boundary</div>}>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
// render suspense fallback (Loading...)
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// resolve promise -> render Page (rendered)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
// change query key
|
||||
succeed = false
|
||||
// reset query -> and throw error
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('fail'))
|
||||
// render error boundary fallback (error boundary)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should error caught in error boundary without infinite loop when query keys changed', async () => {
|
||||
let succeed = true
|
||||
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const [key, setKey] = createSignal(0)
|
||||
|
||||
const result = useQuery(() => ({
|
||||
queryKey: [`${key()}-${succeed}`],
|
||||
queryFn: async () =>
|
||||
sleep(10).then(() => {
|
||||
if (!succeed) throw new Error('Suspense Error Bingo')
|
||||
return 'data'
|
||||
}),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span> <span>{result.data}</span>
|
||||
<button aria-label="fail" onClick={() => setKey((k) => k + 1)}>
|
||||
fail
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => <div>error boundary</div>}>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
// render suspense fallback (Loading...)
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// resolve promise -> render Page (rendered)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
// change promise result to error
|
||||
succeed = false
|
||||
// change query key
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('fail'))
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// render error boundary fallback (error boundary)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should error caught in error boundary without infinite loop when enabled changed', async () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
function Page() {
|
||||
const queryKeys = '1'
|
||||
const [enabled, setEnabled] = createSignal(false)
|
||||
|
||||
const result = useQuery<string>(() => ({
|
||||
queryKey: [queryKeys],
|
||||
queryFn: () =>
|
||||
sleep(10).then(() =>
|
||||
Promise.reject(new Error('Suspense Error Bingo')),
|
||||
),
|
||||
retry: false,
|
||||
suspense: true,
|
||||
enabled: enabled(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span> <span>{result.data}</span>
|
||||
<button
|
||||
aria-label="fail"
|
||||
onClick={() => {
|
||||
setEnabled(true)
|
||||
}}
|
||||
>
|
||||
fail
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => <div>error boundary</div>}>
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <App />)
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
// render empty data with 'rendered' when enabled is false
|
||||
expect(rendered.getByText('rendered')).toBeInTheDocument()
|
||||
|
||||
// change enabled to true
|
||||
fireEvent.click(rendered.getByLabelText('fail'))
|
||||
// render pending fallback
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
// render error boundary fallback (error boundary)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('error boundary')).toBeInTheDocument()
|
||||
|
||||
consoleMock.mockRestore()
|
||||
})
|
||||
|
||||
it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => {
|
||||
const key = queryKey()
|
||||
let state: UseQueryResult<number> | null = null
|
||||
|
||||
let count = 0
|
||||
let renders = 0
|
||||
|
||||
function Page() {
|
||||
state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => ++count),
|
||||
gcTime: 0,
|
||||
}))
|
||||
|
||||
createRenderEffect(
|
||||
on([() => ({ ...state })], () => {
|
||||
renders++
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>rendered</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<Suspense fallback="loading">
|
||||
<Page />
|
||||
</Suspense>
|
||||
))
|
||||
|
||||
expect(rendered.getByText('loading')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(state).toMatchObject({
|
||||
data: 1,
|
||||
status: 'success',
|
||||
})
|
||||
expect(renders).toBe(2)
|
||||
expect(rendered.queryByText('rendered')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
66
packages/solid-query/src/__tests__/transition.test.tsx
Normal file
66
packages/solid-query/src/__tests__/transition.test.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import { Show, Suspense, createSignal, startTransition } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryCache, QueryClient, useQuery } from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
|
||||
describe("useQuery's in Suspense mode with transitions", () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should render the content when the transition is done', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Suspended() {
|
||||
const state = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => true),
|
||||
}))
|
||||
return <Show when={state.data}>Message</Show>
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [showSignal, setShowSignal] = createSignal(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
aria-label="toggle"
|
||||
onClick={() =>
|
||||
startTransition(() => setShowSignal((value) => !value))
|
||||
}
|
||||
>
|
||||
{showSignal() ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
<Suspense fallback="Loading">
|
||||
<Show when={showSignal()}>
|
||||
<Suspended />
|
||||
</Show>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('Show')).toBeInTheDocument()
|
||||
fireEvent.click(rendered.getByLabelText('toggle'))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('Message')).toBeInTheDocument()
|
||||
// verify that the button also updated. See https://github.com/solidjs/solid/issues/1249
|
||||
expect(rendered.getByText('Hide')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
210
packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx
Normal file
210
packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { useInfiniteQuery } from '../useInfiniteQuery'
|
||||
import { QueryClient } from '../QueryClient'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
describe('useInfiniteQuery', () => {
|
||||
describe('pageParam', () => {
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext', () => {
|
||||
useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('direction should be passed to queryFn of useInfiniteQuery', () => {
|
||||
useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ direction }) => {
|
||||
expectTypeOf(direction).toEqualTypeOf<'forward' | 'backward'>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext for fetchInfiniteQuery', () => {
|
||||
const queryClient = new QueryClient()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.fetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => {
|
||||
const queryClient = new QueryClient()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.prefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialData', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
initialData: { pages: [5], pageParams: [1] },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, unknown>>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('select', () => {
|
||||
it('should still return paginated data if no select result', () => {
|
||||
const infiniteQuery = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('should be able to transform data to arbitrary result', () => {
|
||||
const infiniteQuery = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, number>>()
|
||||
return 'selected' as const
|
||||
},
|
||||
}))
|
||||
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextPageParam / getPreviousPageParam', () => {
|
||||
it('should get typed params', () => {
|
||||
const infiniteQuery = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return String(pageParam)
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (
|
||||
lastPage,
|
||||
allPages,
|
||||
lastPageParam,
|
||||
allPageParams,
|
||||
) => {
|
||||
expectTypeOf(lastPage).toEqualTypeOf<string>()
|
||||
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
|
||||
expectTypeOf(lastPageParam).toEqualTypeOf<number>()
|
||||
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
|
||||
return undefined
|
||||
},
|
||||
getPreviousPageParam: (
|
||||
firstPage,
|
||||
allPages,
|
||||
firstPageParam,
|
||||
allPageParams,
|
||||
) => {
|
||||
expectTypeOf(firstPage).toEqualTypeOf<string>()
|
||||
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
|
||||
expectTypeOf(firstPageParam).toEqualTypeOf<number>()
|
||||
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
|
||||
return undefined
|
||||
},
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('error booleans', () => {
|
||||
it('should not be permanently `false`', () => {
|
||||
const {
|
||||
isFetchNextPageError,
|
||||
isFetchPreviousPageError,
|
||||
isLoadingError,
|
||||
isRefetchError,
|
||||
} = useInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(isFetchNextPageError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isFetchPreviousPageError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isLoadingError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isRefetchError).toEqualTypeOf<boolean>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryClient', () => {
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const infiniteQuery = useInfiniteQuery(
|
||||
() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
})
|
||||
1983
packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx
Normal file
1983
packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
292
packages/solid-query/src/__tests__/useIsFetching.test.tsx
Normal file
292
packages/solid-query/src/__tests__/useIsFetching.test.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@solidjs/testing-library'
|
||||
import { Show, createEffect, createRenderEffect, createSignal } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryCache, QueryClient, useIsFetching, useQuery } from '..'
|
||||
import { renderWithClient, setActTimeout } from './utils'
|
||||
|
||||
describe('useIsFetching', () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// See https://github.com/tannerlinsley/react-query/issues/105
|
||||
it('should update as queries start and stop fetching', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function IsFetching() {
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
return <div>isFetching: {isFetching()}</div>
|
||||
}
|
||||
|
||||
function Query() {
|
||||
const [ready, setReady] = createSignal(false)
|
||||
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(50).then(() => 'test'),
|
||||
enabled: ready(),
|
||||
}))
|
||||
|
||||
return <button onClick={() => setReady(true)}>setReady</button>
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<IsFetching />
|
||||
<Query />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /setReady/i }))
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not update state while rendering', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const isFetchingArray: Array<number> = []
|
||||
|
||||
function IsFetching() {
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
createRenderEffect(() => {
|
||||
isFetchingArray.push(isFetching())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function FirstQuery() {
|
||||
useQuery(() => ({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(150).then(() => 'data'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function SecondQuery() {
|
||||
useQuery(() => ({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(200).then(() => 'data'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [renderSecond, setRenderSecond] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
setActTimeout(() => {
|
||||
setRenderSecond(true)
|
||||
}, 100)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<IsFetching />
|
||||
<FirstQuery />
|
||||
<Show when={renderSecond()}>
|
||||
<SecondQuery />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
// unlike react, Updating renderSecond wont cause a rerender for FirstQuery
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
|
||||
expect(isFetchingArray).toEqual([0, 1, 2, 1, 0])
|
||||
})
|
||||
|
||||
it('should be able to filter', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const isFetchingArray: Array<number> = []
|
||||
|
||||
function One() {
|
||||
useQuery(() => ({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Two() {
|
||||
useQuery(() => ({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(20).then(() => 'test'),
|
||||
}))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [started, setStarted] = createSignal(false)
|
||||
const isFetching = useIsFetching(() => ({
|
||||
queryKey: key1,
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
isFetchingArray.push(isFetching())
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setStarted(true)}>setStarted</button>
|
||||
<div>isFetching: {isFetching()}</div>
|
||||
<Show when={started()}>
|
||||
<>
|
||||
<One />
|
||||
<Two />
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /setStarted/i }))
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
// at no point should we have isFetching: 2
|
||||
expect(isFetchingArray).toEqual(expect.not.arrayContaining([2]))
|
||||
})
|
||||
|
||||
it('should show the correct fetching state when mounted after a query', async () => {
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
|
||||
const isFetching = useIsFetching()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>isFetching: {isFetching()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use provided custom queryClient', async () => {
|
||||
const customClient = new QueryClient()
|
||||
const key = queryKey()
|
||||
|
||||
function Page() {
|
||||
useQuery(
|
||||
() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}),
|
||||
() => customClient,
|
||||
)
|
||||
|
||||
const isFetching = useIsFetching(undefined, () => customClient)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>isFetching: {isFetching()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page></Page>)
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should resubscribe when a custom queryClient changes', async () => {
|
||||
const queryClient1 = new QueryClient()
|
||||
const queryClient2 = new QueryClient()
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const [client, setClient] = createSignal(queryClient1)
|
||||
const queryCache1 = queryClient1.getQueryCache()
|
||||
const originalSubscribe1 = queryCache1.subscribe.bind(queryCache1)
|
||||
const unsubscribe1 = vi.fn()
|
||||
|
||||
vi.spyOn(queryCache1, 'subscribe').mockImplementation((listener) => {
|
||||
const cleanup = originalSubscribe1(listener)
|
||||
|
||||
return () => {
|
||||
unsubscribe1()
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
function Page() {
|
||||
const isFetching = useIsFetching(undefined, client)
|
||||
|
||||
return <div>isFetching: {isFetching()}</div>
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page />)
|
||||
|
||||
const firstQuery = queryClient1.query({
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(20).then(() => 'test1'),
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
|
||||
setClient(queryClient2)
|
||||
|
||||
expect(unsubscribe1).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
const secondQuery = queryClient2.query({
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(20).then(() => 'test2'),
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
await Promise.all([firstQuery, secondQuery])
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
303
packages/solid-query/src/__tests__/useIsMutating.test.tsx
Normal file
303
packages/solid-query/src/__tests__/useIsMutating.test.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@solidjs/testing-library'
|
||||
import { Show, createEffect, createRenderEffect, createSignal } from 'solid-js'
|
||||
import * as QueryCore from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, useIsMutating, useMutation } from '..'
|
||||
import { renderWithClient, setActTimeout } from './utils'
|
||||
|
||||
describe('useIsMutating', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationKey1 = queryKey()
|
||||
const mutationKey2 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
const isMutating = useIsMutating()
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Mutations() {
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(150).then(() => 'data'),
|
||||
}))
|
||||
const { mutate: mutate2 } = useMutation(() => ({
|
||||
mutationKey: mutationKey2,
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
setActTimeout(() => {
|
||||
mutate2()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<IsMutating />
|
||||
<Mutations />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 2, 1, 0])
|
||||
})
|
||||
|
||||
it('should filter correctly by mutationKey', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationKey1 = queryKey()
|
||||
const mutationKey2 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
const isMutating = useIsMutating(() => ({ mutationKey: mutationKey1 }))
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
const { mutate: mutate2 } = useMutation(() => ({
|
||||
mutationKey: mutationKey2,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
mutate2()
|
||||
})
|
||||
|
||||
return <IsMutating />
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
// Unlike React, IsMutating Wont re-render twice with mutation2
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should filter correctly by predicate', async () => {
|
||||
const isMutatingArray: Array<number> = []
|
||||
const mutationKey1 = queryKey()
|
||||
const mutationKey2 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
const isMutating = useIsMutating(() => ({
|
||||
predicate: (mutation) =>
|
||||
mutation.options.mutationKey?.[0] === mutationKey1[0],
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
isMutatingArray.push(isMutating())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
const { mutate: mutate2 } = useMutation(() => ({
|
||||
mutationKey: mutationKey2,
|
||||
mutationFn: () => sleep(100).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
mutate2()
|
||||
})
|
||||
|
||||
return <IsMutating />
|
||||
}
|
||||
|
||||
renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
// Again, No unnecessary re-renders like React
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
expect(isMutatingArray).toEqual([0, 1, 0])
|
||||
})
|
||||
|
||||
it('should use provided custom queryClient', async () => {
|
||||
const customClient = new QueryClient()
|
||||
const mutationKey1 = queryKey()
|
||||
|
||||
function Page() {
|
||||
const isMutating = useIsMutating(undefined, () => customClient)
|
||||
const { mutate } = useMutation(
|
||||
() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(20).then(() => 'data'),
|
||||
}),
|
||||
() => customClient,
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
setActTimeout(() => {
|
||||
mutate()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>mutating: {isMutating()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page></Page>)
|
||||
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should resubscribe when a custom queryClient changes', async () => {
|
||||
const queryClient1 = new QueryClient()
|
||||
const queryClient2 = new QueryClient()
|
||||
const [client, setClient] = createSignal(queryClient1)
|
||||
const mutationCache1 = queryClient1.getMutationCache()
|
||||
const originalSubscribe1 = mutationCache1.subscribe.bind(mutationCache1)
|
||||
const unsubscribe1 = vi.fn()
|
||||
|
||||
vi.spyOn(mutationCache1, 'subscribe').mockImplementation((listener) => {
|
||||
const cleanup = originalSubscribe1(listener)
|
||||
|
||||
return () => {
|
||||
unsubscribe1()
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
function Page() {
|
||||
const isMutating = useIsMutating(undefined, client)
|
||||
|
||||
return <div>mutating: {isMutating()}</div>
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page />)
|
||||
|
||||
const firstMutation = queryClient1.getMutationCache().build(queryClient1, {
|
||||
mutationFn: () => sleep(20).then(() => 'data1'),
|
||||
})
|
||||
const firstMutationPromise = firstMutation.execute(undefined)
|
||||
|
||||
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
|
||||
|
||||
setClient(queryClient2)
|
||||
|
||||
expect(unsubscribe1).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
|
||||
const secondMutation = queryClient2.getMutationCache().build(queryClient2, {
|
||||
mutationFn: () => sleep(20).then(() => 'data2'),
|
||||
})
|
||||
const secondMutationPromise = secondMutation.execute(undefined)
|
||||
|
||||
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
await Promise.all([firstMutationPromise, secondMutationPromise])
|
||||
|
||||
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line vitest/expect-expect
|
||||
it('should not change state if unmounted', async () => {
|
||||
// We have to mock the MutationCache to not unsubscribe
|
||||
// the listener when the component is unmounted
|
||||
class MutationCacheMock extends QueryCore.MutationCache {
|
||||
subscribe(listener: any) {
|
||||
super.subscribe(listener)
|
||||
return () => void 0
|
||||
}
|
||||
}
|
||||
|
||||
const MutationCacheSpy = vi
|
||||
.spyOn(QueryCore, 'MutationCache')
|
||||
.mockImplementation((fn) => {
|
||||
return new MutationCacheMock(fn)
|
||||
})
|
||||
|
||||
// Create the client after mocking MutationCache so it uses the mock,
|
||||
// not the centralized client from beforeEach
|
||||
const spiedClient = new QueryClient()
|
||||
const mutationKey1 = queryKey()
|
||||
|
||||
function IsMutating() {
|
||||
useIsMutating()
|
||||
return null
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [mounted, setMounted] = createSignal(true)
|
||||
|
||||
const { mutate: mutate1 } = useMutation(() => ({
|
||||
mutationKey: mutationKey1,
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
mutate1()
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setMounted(false)}>unmount</button>
|
||||
<Show when={mounted()}>
|
||||
<IsMutating />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(spiedClient, () => <Page />)
|
||||
|
||||
fireEvent.click(rendered.getByText('unmount'))
|
||||
|
||||
// Should not display the console error
|
||||
// "Warning: Can't perform a React state update on an unmounted component"
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
MutationCacheSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
204
packages/solid-query/src/__tests__/useMutation.test-d.tsx
Normal file
204
packages/solid-query/src/__tests__/useMutation.test-d.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { useMutation } from '../useMutation'
|
||||
import { QueryClient } from '../QueryClient'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationKey,
|
||||
QueryClient as QueryCoreClient,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseMutationResult } from '../types'
|
||||
|
||||
describe('useMutation', () => {
|
||||
it('should infer TData from mutationFn return type', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<DefaultError | null>()
|
||||
})
|
||||
|
||||
it('should infer TVariables from mutationFn parameter', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: (vars: { id: string }) => Promise.resolve(vars.id),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' })
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer TOnMutateResult from onMutate return type', () => {
|
||||
useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onMutate: () => {
|
||||
return { token: 'abc' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ token: string }>()
|
||||
},
|
||||
onError: (_error, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<
|
||||
{ token: string } | undefined
|
||||
>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should allow explicit generic types', () => {
|
||||
const mutation = useMutation<string, Error, { id: number }>(() => ({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: number }>()
|
||||
return Promise.resolve('result')
|
||||
},
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<Error | null>()
|
||||
})
|
||||
|
||||
it('should return correct UseMutationResult type', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve(42),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
UseMutationResult<number, DefaultError, void, unknown>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should type mutateAsync with correct return type', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith('test')
|
||||
expectTypeOf(mutation.mutateAsync('test')).toEqualTypeOf<Promise<number>>()
|
||||
})
|
||||
|
||||
it('should default TVariables to void when mutationFn has no parameters', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should allow calling mutate with no arguments when TVariables is optional undefinable', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: (_variables: number | undefined) => Promise.resolve(1),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should infer custom TError type', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
const mutation = useMutation<string, CustomError>(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<CustomError | null>()
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types for onSettled callback', () => {
|
||||
useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve(42),
|
||||
onSettled: (data, error, _variables, _onMutateResult) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should infer custom TError in onError callback', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
useMutation<string, CustomError>(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<CustomError>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const mutation = useMutation(
|
||||
() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should type context as the last argument for mutationFn and every hook-level callback', () => {
|
||||
useMutation(() => ({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
expectTypeOf(context.client).toEqualTypeOf<QueryCoreClient>()
|
||||
return Promise.resolve('data')
|
||||
},
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should type context as the last argument for every per-call mutate option', () => {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
mutation.mutate(undefined, {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should type context.mutationKey as MutationKey', () => {
|
||||
useMutation(() => ({
|
||||
mutationKey: ['todos', 'add'] as const,
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context.mutationKey).toEqualTypeOf<
|
||||
MutationKey | undefined
|
||||
>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
})
|
||||
1845
packages/solid-query/src/__tests__/useMutation.test.tsx
Normal file
1845
packages/solid-query/src/__tests__/useMutation.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { useMutationState } from '../useMutationState'
|
||||
import type {
|
||||
Mutation,
|
||||
MutationState,
|
||||
MutationStatus,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
describe('useMutationState', () => {
|
||||
it('should default to QueryState', () => {
|
||||
const result = useMutationState(() => ({
|
||||
filters: { status: 'pending' },
|
||||
}))
|
||||
|
||||
expectTypeOf(result()).toEqualTypeOf<Array<MutationState>>()
|
||||
})
|
||||
it('should infer with select', () => {
|
||||
const result = useMutationState(() => ({
|
||||
filters: { status: 'pending' },
|
||||
select: (mutation) => mutation.state.status,
|
||||
}))
|
||||
|
||||
expectTypeOf(result()).toEqualTypeOf<Array<MutationStatus>>()
|
||||
})
|
||||
it('should propagate generics to select callback when TResult is typed MutationState', () => {
|
||||
type MyData = { data: Array<string> }
|
||||
type MyError = { code: number; message: string }
|
||||
type MyVars = { id: number }
|
||||
|
||||
const result = useMutationState<MutationState<MyData, MyError, MyVars>>(
|
||||
() => ({
|
||||
filters: { mutationKey: ['key'] },
|
||||
select: (mutation) => {
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
Mutation<MyData, MyError, MyVars, unknown>
|
||||
>()
|
||||
return mutation.state
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expectTypeOf(result()).toEqualTypeOf<
|
||||
Array<MutationState<MyData, MyError, MyVars, unknown>>
|
||||
>()
|
||||
})
|
||||
})
|
||||
114
packages/solid-query/src/__tests__/useMutationState.test.tsx
Normal file
114
packages/solid-query/src/__tests__/useMutationState.test.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent } from '@solidjs/testing-library'
|
||||
import { createEffect } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, useMutation, useMutationState } from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
|
||||
describe('useMutationState', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return all mutation states when called without options', async () => {
|
||||
const mutationKey = queryKey()
|
||||
|
||||
function States() {
|
||||
const mutationStates = useMutationState()
|
||||
|
||||
return <div>count: {mutationStates().length}</div>
|
||||
}
|
||||
|
||||
function Mutate() {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationKey,
|
||||
mutationFn: (input: number) => sleep(150).then(() => 'data' + input),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => mutation.mutate(1)}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<States />
|
||||
<Mutate />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('count: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('count: 1')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
expect(rendered.getByText('count: 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return variables after calling mutate', async () => {
|
||||
const variables: Array<Array<unknown>> = []
|
||||
const mutationKey = queryKey()
|
||||
|
||||
function Variables() {
|
||||
const states = useMutationState(() => ({
|
||||
filters: { mutationKey, status: 'pending' },
|
||||
select: (mutation) => mutation.state.variables,
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
variables.push(states())
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Mutate() {
|
||||
const mutation = useMutation(() => ({
|
||||
mutationKey,
|
||||
mutationFn: (input: number) => sleep(150).then(() => 'data' + input),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
data: {mutation.data ?? 'null'}
|
||||
<button onClick={() => mutation.mutate(1)}>mutate</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<Variables />
|
||||
<Mutate />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
expect(rendered.getByText('data: null')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
expect(rendered.getByText('data: data1')).toBeInTheDocument()
|
||||
|
||||
expect(variables).toEqual([[], [1], []])
|
||||
})
|
||||
})
|
||||
894
packages/solid-query/src/__tests__/useQueries.test-d.tsx
Normal file
894
packages/solid-query/src/__tests__/useQueries.test-d.tsx
Normal file
@@ -0,0 +1,894 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { queryOptions, useQueries } from '..'
|
||||
import { QueryClient } from '../QueryClient'
|
||||
import type * as QueryCore from '@tanstack/query-core'
|
||||
import type { OmitKeyof } from '@tanstack/query-core'
|
||||
import type {
|
||||
QueryFunction,
|
||||
QueryFunctionContext,
|
||||
QueryKey,
|
||||
UseQueryResult,
|
||||
} from '..'
|
||||
import type { QueryOptions } from '../types'
|
||||
|
||||
describe('useQueries', () => {
|
||||
it('TData should have undefined in the union even when initialData is provided as an object', () => {
|
||||
const query1 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: false,
|
||||
},
|
||||
}
|
||||
|
||||
const query2 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => 'Query Data',
|
||||
initialData: 'initial data',
|
||||
}
|
||||
|
||||
const query3 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => 'Query Data',
|
||||
}
|
||||
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [query1, query2, query3],
|
||||
}))
|
||||
|
||||
const query1Data = queryResults[0].data
|
||||
const query2Data = queryResults[1].data
|
||||
const query3Data = queryResults[2].data
|
||||
|
||||
expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
expectTypeOf(query2Data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(query3Data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when passed through queryOptions', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
wow: true,
|
||||
},
|
||||
})
|
||||
const queryResults = useQueries(() => ({ queries: [options] }))
|
||||
|
||||
const data = queryResults[0].data
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => {
|
||||
return {
|
||||
wow: true,
|
||||
}
|
||||
},
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const data = queryResults[0].data
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types from explicit object type parameter', () => {
|
||||
const queryResults = useQueries<
|
||||
[
|
||||
{ queryFnData: number },
|
||||
{ queryFnData: string; error: Error },
|
||||
{ queryFnData: boolean; data: string },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(true),
|
||||
select: () => 'selected',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(queryResults[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(queryResults[1].error).toEqualTypeOf<Error | null>()
|
||||
expectTypeOf(queryResults[2].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types from explicit tuple type parameter', () => {
|
||||
const queryResults = useQueries<
|
||||
[[number], [string, Error], [boolean, Error, string]]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(true),
|
||||
select: () => 'selected',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(queryResults[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(queryResults[1].error).toEqualTypeOf<Error | null>()
|
||||
expectTypeOf(queryResults[2].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => {
|
||||
const query1 = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
select: (data) => data > 1,
|
||||
})
|
||||
|
||||
const query2 = {
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
select: (data: number) => data > 1,
|
||||
}
|
||||
|
||||
const queryResults = useQueries(() => ({ queries: [query1, query2] }))
|
||||
const query1Data = queryResults[0].data
|
||||
const query2Data = queryResults[1].data
|
||||
|
||||
expectTypeOf(query1Data).toEqualTypeOf<boolean | undefined>()
|
||||
expectTypeOf(query2Data).toEqualTypeOf<boolean | undefined>()
|
||||
})
|
||||
|
||||
describe('custom hook', () => {
|
||||
it('should allow custom hooks using QueryOptions', () => {
|
||||
type Data = string
|
||||
|
||||
const useCustomQueries = (
|
||||
options?: OmitKeyof<QueryOptions<Data>, 'queryKey' | 'queryFn'>,
|
||||
) => {
|
||||
return useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
...options,
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
const queryResults = useCustomQueries()
|
||||
const data = queryResults[0].data
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Data | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer custom TError from throwOnError', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
throwOnError: (_error: CustomError) => false,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
expectTypeOf(queryResults[0].error).toEqualTypeOf<CustomError | null>()
|
||||
})
|
||||
|
||||
it('TData should have correct type when conditional skipToken is passed', () => {
|
||||
const queryResults = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const firstResult = queryResults[0]
|
||||
|
||||
expectTypeOf(firstResult).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(firstResult.data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should return correct data for dynamic queries with mixed result types', () => {
|
||||
const Queries1 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
}),
|
||||
}
|
||||
const Queries2 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(true),
|
||||
}),
|
||||
}
|
||||
|
||||
const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
|
||||
const result = useQueries(() => ({
|
||||
queries: [...queries1List, { ...Queries2.get() }],
|
||||
}))
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
[...Array<UseQueryResult<number, Error>>, UseQueryResult<boolean, Error>]
|
||||
>()
|
||||
})
|
||||
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const queryResults = useQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer correct types for combine callback parameter', () => {
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve(1),
|
||||
},
|
||||
{
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => Promise.resolve('hello'),
|
||||
},
|
||||
],
|
||||
combine: (results) => {
|
||||
expectTypeOf(results[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(results[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
return results
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('type parameters', () => {
|
||||
it('should handle type parameter - tuple of tuples', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
const result1 = useQueries<
|
||||
[[number], [string], [Array<string>, boolean]]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result1[0]).toEqualTypeOf<UseQueryResult<number>>()
|
||||
expectTypeOf(result1[1]).toEqualTypeOf<UseQueryResult<string>>()
|
||||
expectTypeOf(result1[2]).toEqualTypeOf<
|
||||
UseQueryResult<Array<string>, boolean>
|
||||
>()
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
|
||||
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
|
||||
|
||||
// TData (3rd element) takes precedence over TQueryFnData (1st element)
|
||||
const result2 = useQueries<
|
||||
[[string, unknown, string], [string, unknown, number]]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result2[0]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[1]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// types should be enforced
|
||||
useQueries<[[string, unknown, string], [string, boolean, number]]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
// field names should be enforced
|
||||
useQueries<[[string]]>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle type parameter - tuple of objects', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
const result1 = useQueries<
|
||||
[
|
||||
{ queryFnData: number },
|
||||
{ queryFnData: string },
|
||||
{ queryFnData: Array<string>; error: boolean },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result1[0]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result1[1]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result1[2]).toEqualTypeOf<
|
||||
UseQueryResult<Array<string>, boolean>
|
||||
>()
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
|
||||
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
|
||||
|
||||
// TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
|
||||
const result2 = useQueries<
|
||||
[
|
||||
{ queryFnData: string; data: string },
|
||||
{ queryFnData: string; data: number },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result2[0]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[1]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// can pass only TData (data prop) although TQueryFnData will be left unknown
|
||||
const result3 = useQueries<[{ data: string }, { data: number }]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<unknown>()
|
||||
return a as string
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<unknown>()
|
||||
return a as number
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
expectTypeOf(result3[0]).toEqualTypeOf<
|
||||
UseQueryResult<string, unknown>
|
||||
>()
|
||||
expectTypeOf(result3[1]).toEqualTypeOf<
|
||||
UseQueryResult<number, unknown>
|
||||
>()
|
||||
expectTypeOf(result3[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result3[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// types should be enforced
|
||||
useQueries<
|
||||
[
|
||||
{ queryFnData: string; data: string },
|
||||
{ queryFnData: string; data: number; error: boolean },
|
||||
]
|
||||
>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// field names should be enforced
|
||||
useQueries<[{ queryFnData: string }]>(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle array literal without type parameter to infer result type', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
const key4 = queryKey()
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
// Array.map preserves TQueryFnData
|
||||
const result1 = useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
})),
|
||||
}))
|
||||
expectTypeOf(result1).toEqualTypeOf<
|
||||
Array<UseQueryResult<number, Error>>
|
||||
>()
|
||||
if (result1[0]) {
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
}
|
||||
|
||||
// Array.map preserves TData
|
||||
const result2 = useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}))
|
||||
expectTypeOf(result2).toEqualTypeOf<
|
||||
Array<UseQueryResult<string, Error>>
|
||||
>()
|
||||
|
||||
const result3 = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
select: () => 123,
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result3[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(result3[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result3[2]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(result3[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result3[1].data).toEqualTypeOf<string | undefined>()
|
||||
// select takes precedence over queryFn
|
||||
expectTypeOf(result3[2].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// initialData/placeholderData are enforced
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 123,
|
||||
// @ts-expect-error (placeholderData: number)
|
||||
placeholderData: 'string',
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// select params are "indirectly" enforced
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
// unfortunately TS will not suggest the type for you
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
// however you can add a type to the callback
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
// the type you do pass is enforced
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key4,
|
||||
queryFn: () => 'string',
|
||||
select: (a: string) => parseInt(a),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// callbacks are also indirectly enforced with Array.map
|
||||
useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}))
|
||||
|
||||
useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// results inference works when all the handlers are defined
|
||||
const result4 = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key4,
|
||||
queryFn: () => 'string',
|
||||
select: (a: string) => parseInt(a),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result4[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result4[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result4[2]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
|
||||
// handles when queryFn returns a Promise
|
||||
const result5 = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result5[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
|
||||
// Array as const does not throw error
|
||||
const result6 = useQueries(
|
||||
() =>
|
||||
({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['key1'],
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: ['key1'],
|
||||
queryFn: () => 123,
|
||||
},
|
||||
],
|
||||
}) as const,
|
||||
)
|
||||
expectTypeOf(result6[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
expectTypeOf(result6[1]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
|
||||
// field names should be enforced - array literal
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// field names should be enforced - Array.map() result
|
||||
useQueries(() => ({
|
||||
// @ts-expect-error (invalidField)
|
||||
queries: Array(10).map(() => ({
|
||||
someInvalidField: '',
|
||||
})),
|
||||
}))
|
||||
|
||||
// field names should be enforced - array literal
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// supports queryFn using fetch() to return Promise<any> - Array.map() result
|
||||
useQueries(() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () =>
|
||||
fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
})),
|
||||
}))
|
||||
|
||||
// supports queryFn using fetch() to return Promise<any> - array literal
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () =>
|
||||
fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle strongly typed queryFn factories and useQueries wrappers', () => {
|
||||
// QueryKey + queryFn factory
|
||||
type QueryKeyA = ['queryA']
|
||||
const getQueryKeyA = (): QueryKeyA => ['queryA']
|
||||
type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>
|
||||
const getQueryFunctionA: GetQueryFunctionA = () => () => {
|
||||
return 1
|
||||
}
|
||||
type SelectorA = (data: number) => [number, string]
|
||||
const getSelectorA = (): SelectorA => (data) => [data, data.toString()]
|
||||
|
||||
type QueryKeyB = ['queryB', string]
|
||||
const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id]
|
||||
type GetQueryFunctionB = () => QueryFunction<string, QueryKeyB>
|
||||
const getQueryFunctionB: GetQueryFunctionB = () => () => {
|
||||
return '1'
|
||||
}
|
||||
type SelectorB = (data: string) => [string, number]
|
||||
const getSelectorB = (): SelectorB => (data) => [data, +data]
|
||||
|
||||
// Wrapper with strongly typed array-parameter
|
||||
function useWrappedQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(queries: Array<QueryOptions<TQueryFnData, TError, TData, TQueryKey>>) {
|
||||
return useQueries(() => ({
|
||||
queries: queries.map(
|
||||
// no need to type the mapped query
|
||||
(query) => {
|
||||
const { queryFn: fn, queryKey: key } = query
|
||||
expectTypeOf(fn).toEqualTypeOf<
|
||||
| typeof QueryCore.skipToken
|
||||
| QueryCore.QueryFunction<TQueryFnData, TQueryKey, never>
|
||||
| undefined
|
||||
>()
|
||||
return {
|
||||
queryKey: key,
|
||||
queryFn: fn
|
||||
? (ctx: QueryFunctionContext<TQueryKey>) => {
|
||||
// eslint-disable-next-line vitest/valid-expect
|
||||
expectTypeOf<TQueryKey>(ctx.queryKey)
|
||||
return (
|
||||
fn as QueryFunction<TQueryFnData, TQueryKey>
|
||||
).call({}, ctx)
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
},
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
// @ts-expect-error (Page component is not rendered)
|
||||
function Page() {
|
||||
const result = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
},
|
||||
{
|
||||
queryKey: getQueryKeyB('id'),
|
||||
queryFn: getQueryFunctionB(),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(result[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
|
||||
expectTypeOf(result[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
|
||||
|
||||
const withSelector = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
select: getSelectorA(),
|
||||
},
|
||||
{
|
||||
queryKey: getQueryKeyB('id'),
|
||||
queryFn: getQueryFunctionB(),
|
||||
select: getSelectorB(),
|
||||
},
|
||||
],
|
||||
}))
|
||||
expectTypeOf(withSelector[0]).toEqualTypeOf<
|
||||
UseQueryResult<[number, string], Error>
|
||||
>()
|
||||
expectTypeOf(withSelector[1]).toEqualTypeOf<
|
||||
UseQueryResult<[string, number], Error>
|
||||
>()
|
||||
|
||||
const withWrappedQueries = useWrappedQueries(
|
||||
Array(10).map(() => ({
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
select: getSelectorA(),
|
||||
})),
|
||||
)
|
||||
|
||||
expectTypeOf(withWrappedQueries).toEqualTypeOf<
|
||||
Array<UseQueryResult<number, Error>>
|
||||
>()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
216
packages/solid-query/src/__tests__/useQueries.test.tsx
Normal file
216
packages/solid-query/src/__tests__/useQueries.test.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@solidjs/testing-library'
|
||||
import * as QueryCore from '@tanstack/query-core'
|
||||
import { createRenderEffect, createSignal } from 'solid-js'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import {
|
||||
IsRestoringProvider,
|
||||
QueriesObserver,
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
useQueries,
|
||||
} from '..'
|
||||
import { renderWithClient } from './utils'
|
||||
import type { UseQueryResult } from '..'
|
||||
|
||||
describe('useQueries', () => {
|
||||
let queryCache: QueryCache
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryCache = new QueryCache()
|
||||
queryClient = new QueryClient({ queryCache })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the correct states', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const results: Array<Array<UseQueryResult>> = []
|
||||
|
||||
function Page() {
|
||||
const result = useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 1),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => sleep(100).then(() => 2),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
createRenderEffect(() => {
|
||||
results.push([{ ...result[0] }, { ...result[1] }])
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
data1: {String(result[0].data ?? 'null')}, data2:{' '}
|
||||
{String(result[1].data ?? 'null')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(rendered.getByText('data1: 1, data2: 2')).toBeInTheDocument()
|
||||
|
||||
expect(results.length).toBe(3)
|
||||
expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }])
|
||||
expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
|
||||
expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
|
||||
})
|
||||
|
||||
// eslint-disable-next-line vitest/expect-expect
|
||||
it('should not change state if unmounted', async () => {
|
||||
const key1 = queryKey()
|
||||
|
||||
// We have to mock the QueriesObserver to not unsubscribe
|
||||
// the listener when the component is unmounted
|
||||
class QueriesObserverMock extends QueriesObserver {
|
||||
subscribe(listener: any) {
|
||||
super.subscribe(listener)
|
||||
return () => void 0
|
||||
}
|
||||
}
|
||||
|
||||
const QueriesObserverSpy = vi
|
||||
.spyOn(QueryCore, 'QueriesObserver')
|
||||
.mockImplementation(function (
|
||||
client: InstanceType<typeof QueryCore.QueryClient>,
|
||||
queries: Array<QueryCore.QueryObserverOptions>,
|
||||
) {
|
||||
return new QueriesObserverMock(client, queries)
|
||||
})
|
||||
|
||||
function Queries() {
|
||||
useQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => sleep(10).then(() => 1),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>queries</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
const [mounted, setMounted] = createSignal(true)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setMounted(false)}>unmount</button>
|
||||
{mounted() && <Queries />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => <Page />)
|
||||
|
||||
fireEvent.click(rendered.getByText('unmount'))
|
||||
|
||||
// Should not display the console error
|
||||
// "Warning: Can't perform a React state update on an unmounted component"
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
QueriesObserverSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should use provided custom queryClient', async () => {
|
||||
const key = queryKey()
|
||||
const queryFn = () => sleep(10).then(() => 'custom client')
|
||||
|
||||
function Page() {
|
||||
const queries = useQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key,
|
||||
queryFn,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
return <div>data: {queries[0].data}</div>
|
||||
}
|
||||
|
||||
const rendered = render(() => <Page />)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('data: custom client')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not fetch for the duration of the restoring period when isRestoring is true', async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const queryFn1 = vi.fn(() => sleep(10).then(() => 'data1'))
|
||||
const queryFn2 = vi.fn(() => sleep(10).then(() => 'data2'))
|
||||
|
||||
function Page() {
|
||||
const results = useQueries(() => ({
|
||||
queries: [
|
||||
{ queryKey: key1, queryFn: queryFn1 },
|
||||
{ queryKey: key2, queryFn: queryFn2 },
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="status1">{results[0].status}</div>
|
||||
<div data-testid="status2">{results[1].status}</div>
|
||||
<div data-testid="fetchStatus1">{results[0].fetchStatus}</div>
|
||||
<div data-testid="fetchStatus2">{results[1].fetchStatus}</div>
|
||||
<div data-testid="data1">{results[0].data ?? 'undefined'}</div>
|
||||
<div data-testid="data2">{results[1].data ?? 'undefined'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = renderWithClient(queryClient, () => (
|
||||
<IsRestoringProvider value={() => true}>
|
||||
<Page />
|
||||
</IsRestoringProvider>
|
||||
))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
310
packages/solid-query/src/__tests__/useQuery.test-d.tsx
Normal file
310
packages/solid-query/src/__tests__/useQuery.test-d.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { queryOptions, useQuery } from '../index'
|
||||
import type { OmitKeyof, QueryFunction, UseQueryOptions } from '..'
|
||||
|
||||
describe('useQuery', () => {
|
||||
const key = queryKey()
|
||||
|
||||
// unspecified query function should default to unknown
|
||||
const noQueryFn = useQuery(() => ({ queryKey: key }))
|
||||
expectTypeOf(noQueryFn.data).toEqualTypeOf<unknown>()
|
||||
expectTypeOf(noQueryFn.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should infer the result type from the query function
|
||||
const fromQueryFn = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => 'test',
|
||||
}))
|
||||
expectTypeOf(fromQueryFn.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(fromQueryFn.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should be possible to specify the result type
|
||||
const withResult = useQuery<string>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => 'test',
|
||||
}))
|
||||
expectTypeOf(withResult.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(withResult.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should be possible to specify the error type
|
||||
const withError = useQuery<string, Error>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => 'test',
|
||||
}))
|
||||
expectTypeOf(withError.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(withError.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
// it should provide the result type in the configuration
|
||||
useQuery(() => ({
|
||||
queryKey: [key],
|
||||
queryFn: () => true,
|
||||
}))
|
||||
|
||||
// it should be possible to specify a union type as result type
|
||||
const unionTypeSync = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)),
|
||||
}))
|
||||
expectTypeOf(unionTypeSync.data).toEqualTypeOf<'a' | 'b' | undefined>()
|
||||
const unionTypeAsync = useQuery<'a' | 'b'>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'),
|
||||
}))
|
||||
expectTypeOf(unionTypeAsync.data).toEqualTypeOf<'a' | 'b' | undefined>()
|
||||
|
||||
// should error when the query function result does not match with the specified type
|
||||
// @ts-expect-error
|
||||
useQuery<number>(() => ({ queryKey: key, queryFn: () => 'test' }))
|
||||
|
||||
// it should infer the result type from a generic query function
|
||||
function queryFn<T = string>(): Promise<T> {
|
||||
return Promise.resolve({} as T)
|
||||
}
|
||||
|
||||
const fromGenericQueryFn = useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => queryFn(),
|
||||
}))
|
||||
expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(fromGenericQueryFn.error).toEqualTypeOf<Error | null>()
|
||||
|
||||
type MyData = number
|
||||
type MyQueryKey = readonly ['my-data', number]
|
||||
|
||||
const getMyDataArrayKey: QueryFunction<MyData, MyQueryKey> = ({
|
||||
queryKey: [, n],
|
||||
}) => {
|
||||
return n + 42
|
||||
}
|
||||
|
||||
useQuery(() => ({
|
||||
queryKey: ['my-data', 100] as const,
|
||||
queryFn: getMyDataArrayKey,
|
||||
}))
|
||||
|
||||
const getMyDataStringKey: QueryFunction<MyData, ['1']> = (context) => {
|
||||
expectTypeOf(context.queryKey).toEqualTypeOf<['1']>()
|
||||
return Number(context.queryKey[0]) + 42
|
||||
}
|
||||
|
||||
useQuery(() => ({
|
||||
queryKey: ['1'] as ['1'],
|
||||
queryFn: getMyDataStringKey,
|
||||
}))
|
||||
|
||||
// it should handle query-functions that return Promise<any>
|
||||
useQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
}))
|
||||
|
||||
// handles wrapped queries with custom fetcher passed as inline queryFn
|
||||
const useWrappedQuery = <
|
||||
TQueryKey extends [string, Record<string, unknown>?],
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData = TQueryFnData,
|
||||
>(
|
||||
qk: TQueryKey,
|
||||
fetcher: (
|
||||
obj: TQueryKey[1],
|
||||
token: string,
|
||||
// return type must be wrapped with TQueryFnReturn
|
||||
) => Promise<TQueryFnData>,
|
||||
options?: OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryKey' | 'queryFn' | 'initialData',
|
||||
'safely'
|
||||
>,
|
||||
) =>
|
||||
useQuery(() => ({
|
||||
queryKey: qk,
|
||||
queryFn: () => fetcher(qk[1], 'token'),
|
||||
...options,
|
||||
}))
|
||||
const test = useWrappedQuery([''], () => Promise.resolve('1'))
|
||||
expectTypeOf(test.data).toEqualTypeOf<string | undefined>()
|
||||
|
||||
// handles wrapped queries with custom fetcher passed directly to useQuery
|
||||
const useWrappedFuncStyleQuery = <
|
||||
TQueryKey extends [string, Record<string, unknown>?],
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData = TQueryFnData,
|
||||
>(
|
||||
qk: TQueryKey,
|
||||
fetcher: () => Promise<TQueryFnData>,
|
||||
options?: OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryKey' | 'queryFn' | 'initialData',
|
||||
'safely'
|
||||
>,
|
||||
) => useQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options }))
|
||||
const testFuncStyle = useWrappedFuncStyleQuery([''], () =>
|
||||
Promise.resolve(true),
|
||||
)
|
||||
expectTypeOf(testFuncStyle.data).toEqualTypeOf<boolean | undefined>()
|
||||
|
||||
it('should return the correct states for a successful query', () => {
|
||||
const state = useQuery<string, Error>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('test'),
|
||||
}))
|
||||
|
||||
if (state.isPending) {
|
||||
expectTypeOf(state.data).toEqualTypeOf<undefined>()
|
||||
expectTypeOf(state.error).toEqualTypeOf<null>()
|
||||
} else if (state.isLoadingError) {
|
||||
expectTypeOf(state.data).toEqualTypeOf<undefined>()
|
||||
expectTypeOf(state.error).toEqualTypeOf<Error>()
|
||||
} else {
|
||||
expectTypeOf(state.data).toEqualTypeOf<string>()
|
||||
expectTypeOf(state.error).toEqualTypeOf<Error | null>()
|
||||
}
|
||||
})
|
||||
|
||||
describe('initialData', () => {
|
||||
describe('Config object overload', () => {
|
||||
it('TData should always be defined when initialData is provided as an object', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should be defined when passed through queryOptions', () => {
|
||||
const options = queryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
})
|
||||
const { data } = useQuery(() => options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query key overload', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query key and func', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('generic indexed access TData', () => {
|
||||
// https://github.com/TanStack/query/issues/9937
|
||||
it('should be assignable back to its source indexed type when passed to a generic function parameter', () => {
|
||||
enum DataType {
|
||||
Account = 'account',
|
||||
Product = 'product',
|
||||
}
|
||||
|
||||
interface Account {
|
||||
name: string
|
||||
}
|
||||
interface Product {
|
||||
code: string
|
||||
}
|
||||
|
||||
type DataTypeToEntity = {
|
||||
[DataType.Account]: Account
|
||||
[DataType.Product]: Product
|
||||
}
|
||||
|
||||
const getData = <TDataType extends DataType>(
|
||||
_dataType: TDataType,
|
||||
): Promise<DataTypeToEntity[TDataType]> =>
|
||||
Promise.resolve({} as DataTypeToEntity[TDataType])
|
||||
|
||||
const getLabel = <TDataType extends DataType>(
|
||||
_dataType: TDataType,
|
||||
_data: DataTypeToEntity[TDataType],
|
||||
) => 'test'
|
||||
|
||||
function Test<TDataType extends DataType>(props: {
|
||||
dataType: TDataType
|
||||
}) {
|
||||
const { data } = useQuery(() => ({
|
||||
queryKey: ['test'],
|
||||
queryFn: () => getData(props.dataType),
|
||||
}))
|
||||
|
||||
// Regression guard: this call must compile. With the previous
|
||||
// hand-rolled NoInfer, `data` failed to flow back into the generic
|
||||
// indexed-access parameter `DataTypeToEntity[TDataType]`.
|
||||
return data ? getLabel(props.dataType, data) : null
|
||||
}
|
||||
|
||||
expectTypeOf(Test).toBeFunction()
|
||||
})
|
||||
})
|
||||
})
|
||||
5816
packages/solid-query/src/__tests__/useQuery.test.tsx
Normal file
5816
packages/solid-query/src/__tests__/useQuery.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
49
packages/solid-query/src/__tests__/utils.tsx
Normal file
49
packages/solid-query/src/__tests__/utils.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { vi } from 'vitest'
|
||||
import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
|
||||
import { onlineManager } from '@tanstack/query-core'
|
||||
import { render } from '@solidjs/testing-library'
|
||||
import { QueryClientProvider } from '..'
|
||||
import type { JSX, ParentProps } from 'solid-js'
|
||||
import type { MockInstance } from 'vitest'
|
||||
import type { QueryClient } from '..'
|
||||
|
||||
export function renderWithClient(
|
||||
client: QueryClient,
|
||||
ui: () => JSX.Element,
|
||||
): ReturnType<typeof render> {
|
||||
return render(() => (
|
||||
<QueryClientProvider client={client}>{ui()}</QueryClientProvider>
|
||||
))
|
||||
}
|
||||
|
||||
export function Blink(
|
||||
props: {
|
||||
duration: number
|
||||
} & ParentProps,
|
||||
) {
|
||||
const [shouldShow, setShouldShow] = createSignal<boolean>(true)
|
||||
|
||||
createEffect(() => {
|
||||
setShouldShow(true)
|
||||
const timeout = setActTimeout(() => setShouldShow(false), props.duration)
|
||||
onCleanup(() => clearTimeout(timeout))
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={shouldShow()} fallback={<>off</>}>
|
||||
<>{props.children}</>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function mockOnlineManagerIsOnline(
|
||||
value: boolean,
|
||||
): MockInstance<() => boolean> {
|
||||
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
|
||||
}
|
||||
|
||||
export function setActTimeout(fn: () => void, ms?: number) {
|
||||
return setTimeout(() => {
|
||||
fn()
|
||||
}, ms)
|
||||
}
|
||||
87
packages/solid-query/src/index.ts
Normal file
87
packages/solid-query/src/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { useQuery } from './useQuery'
|
||||
import { useInfiniteQuery } from './useInfiniteQuery'
|
||||
import { useMutation } from './useMutation'
|
||||
import { useQueries } from './useQueries'
|
||||
|
||||
// Re-export core
|
||||
export * from '@tanstack/query-core'
|
||||
|
||||
// Solid Query
|
||||
export * from './types'
|
||||
|
||||
export type {
|
||||
DefinedUseBaseQueryResult,
|
||||
DefinedUseInfiniteQueryResult,
|
||||
DefinedUseQueryResult,
|
||||
InfiniteQueryOptions,
|
||||
MutationOptions,
|
||||
QueryOptions,
|
||||
UseBaseMutationResult,
|
||||
UseBaseQueryOptions,
|
||||
UseBaseQueryResult,
|
||||
UseInfiniteQueryOptions,
|
||||
UseInfiniteQueryResult,
|
||||
UseMutateAsyncFunction,
|
||||
UseMutateFunction,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
// Aliases (create* and use* are both supported)
|
||||
UseBaseQueryOptions as CreateBaseQueryOptions,
|
||||
UseBaseQueryResult as CreateBaseQueryResult,
|
||||
UseInfiniteQueryOptions as CreateInfiniteQueryOptions,
|
||||
UseInfiniteQueryResult as CreateInfiniteQueryResult,
|
||||
UseMutateAsyncFunction as CreateMutateAsyncFunction,
|
||||
UseMutateFunction as CreateMutateFunction,
|
||||
UseMutationOptions as CreateMutationOptions,
|
||||
UseMutationResult as CreateMutationResult,
|
||||
UseBaseMutationResult as CreateBaseMutationResult,
|
||||
UseQueryOptions as CreateQueryOptions,
|
||||
UseQueryResult as CreateQueryResult,
|
||||
DefinedUseBaseQueryResult as DefinedCreateBaseQueryResult,
|
||||
DefinedUseInfiniteQueryResult as DefinedCreateInfiniteQueryResult,
|
||||
DefinedUseQueryResult as DefinedCreateQueryResult,
|
||||
} from './types'
|
||||
|
||||
export { QueryClient } from './QueryClient'
|
||||
export type {
|
||||
QueryObserverOptions,
|
||||
DefaultOptions,
|
||||
QueryClientConfig,
|
||||
InfiniteQueryObserverOptions,
|
||||
} from './QueryClient'
|
||||
export { useQuery } from './useQuery'
|
||||
export const createQuery = useQuery
|
||||
export { queryOptions } from './queryOptions'
|
||||
export type {
|
||||
DefinedInitialDataOptions,
|
||||
UndefinedInitialDataOptions,
|
||||
} from './queryOptions'
|
||||
export {
|
||||
QueryClientContext,
|
||||
QueryClientProvider,
|
||||
useQueryClient,
|
||||
} from './QueryClientProvider'
|
||||
export type { QueryClientProviderProps } from './QueryClientProvider'
|
||||
export { useIsFetching } from './useIsFetching'
|
||||
export { useIsFetching as createIsFetching } from './useIsFetching'
|
||||
export { useInfiniteQuery }
|
||||
export const createInfiniteQuery = useInfiniteQuery
|
||||
export { infiniteQueryOptions } from './infiniteQueryOptions'
|
||||
export type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions'
|
||||
export { useMutation } from './useMutation'
|
||||
export { mutationOptions } from './mutationOptions'
|
||||
export const createMutation = useMutation
|
||||
export { useIsMutating } from './useIsMutating'
|
||||
export { useIsMutating as createIsMutating } from './useIsMutating'
|
||||
export { useMutationState } from './useMutationState'
|
||||
export { useMutationState as createMutationState } from './useMutationState'
|
||||
export { useQueries } from './useQueries'
|
||||
export const createQueries = useQueries
|
||||
export { useIsRestoring, IsRestoringProvider } from './isRestoring'
|
||||
203
packages/solid-query/src/infiniteQueryOptions.ts
Normal file
203
packages/solid-query/src/infiniteQueryOptions.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
NonUndefinedGuard,
|
||||
QueryKey,
|
||||
QueryKeyWithDataTag,
|
||||
} from '@tanstack/query-core'
|
||||
import type { InfiniteQueryOptions } from './types'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
/**
|
||||
* The options accepted by the `infiniteQueryOptions` overload selected when no `initialData` is set — `data`
|
||||
* may be `undefined` while the query is `pending`. `infiniteQueryOptions` itself accepts and returns a plain
|
||||
* object (its parameter type is `ReturnType<UndefinedInitialDataInfiniteOptions<...>>`, i.e. this `Accessor`
|
||||
* called); Solid's reactivity applies where the result is consumed instead, e.g.
|
||||
* `useInfiniteQuery(() => options)`.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs — defaults to `InfiniteData<TQueryFnData>`,
|
||||
* the shape of all fetched pages plus their page params.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export type UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = Accessor<
|
||||
InfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
|
||||
initialData?: undefined
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* The options accepted by the `infiniteQueryOptions` overload selected when `initialData` is set — `data` is
|
||||
* never `undefined`.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs — defaults to `InfiniteData<TQueryFnData>`,
|
||||
* the shape of all fetched pages plus their page params.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export type DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
// should we handle page param correctly
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = Accessor<
|
||||
InfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
|
||||
initialData:
|
||||
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`.
|
||||
* These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`.
|
||||
* `options.queryKey` is required and is the query key to generate options for.
|
||||
*
|
||||
* This overload is selected when `initialData` is set.
|
||||
*
|
||||
* @see {@link useInfiniteQuery} to run an infinite query with these options.
|
||||
* @param options - The {@link DefinedInitialDataInfiniteOptions} to use — everything you can pass to `useInfiniteQuery`, with `initialData` set.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { For } from 'solid-js'
|
||||
* import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* const projectsOptions = infiniteQueryOptions({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* initialData: { pages: [], pageParams: [] },
|
||||
* })
|
||||
*
|
||||
* function Projects() {
|
||||
* // `projectsQuery.data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the
|
||||
* // list stays visible alongside the error.
|
||||
* const projectsQuery = useInfiniteQuery(() => projectsOptions)
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {projectsQuery.isError ? <span>Error: {projectsQuery.error.message}</span> : null}
|
||||
* <ul>
|
||||
* <For each={projectsQuery.data.pages}>
|
||||
* {(page) => <For each={page.projects}>{(p) => <li>{p.name}</li>}</For>}
|
||||
* </For>
|
||||
* </ul>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: ReturnType<
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): ReturnType<
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`.
|
||||
* These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`.
|
||||
* `options.queryKey` is required and is the query key to generate options for.
|
||||
*
|
||||
* @see {@link useInfiniteQuery} to run an infinite query with these options.
|
||||
* @param options - The {@link UndefinedInitialDataInfiniteOptions} to use — everything you can pass to `useInfiniteQuery`.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* A parameterized factory, so the same options object can be reused per `postId`:
|
||||
* ```tsx
|
||||
* import { For, Match, Switch } from 'solid-js'
|
||||
* import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* const commentsOptions = (postId: string) =>
|
||||
* infiniteQueryOptions({
|
||||
* queryKey: ['post', postId, 'comments'],
|
||||
* queryFn: ({ pageParam }) => fetchComments(postId, pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* })
|
||||
*
|
||||
* function Comments(props: { postId: string }) {
|
||||
* const commentsQuery = useInfiniteQuery(() => commentsOptions(props.postId))
|
||||
*
|
||||
* return (
|
||||
* <Switch>
|
||||
* <Match when={commentsQuery.isPending}>Loading...</Match>
|
||||
* <Match when={commentsQuery.isError}>Error: {commentsQuery.error.message}</Match>
|
||||
* <Match when={commentsQuery.isSuccess}>
|
||||
* <ul>
|
||||
* <For each={commentsQuery.data.pages}>
|
||||
* {(page) => <For each={page.comments}>{(c) => <li>{c.text}</li>}</For>}
|
||||
* </For>
|
||||
* </ul>
|
||||
* </Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: ReturnType<
|
||||
UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
): ReturnType<
|
||||
UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
|
||||
export function infiniteQueryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
19
packages/solid-query/src/isRestoring.ts
Normal file
19
packages/solid-query/src/isRestoring.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
const IsRestoringContext = createContext<Accessor<boolean>>(() => false)
|
||||
|
||||
/**
|
||||
* If you are using `PersistQueryClientProvider`, you can also use the `useIsRestoring` hook alongside it to
|
||||
* check if a restore is currently in progress. `useQuery` and friends also check this internally to avoid
|
||||
* race conditions between the restore and mounting queries.
|
||||
*
|
||||
* @returns An accessor that reads `true` while a persisted client is being restored, `false` otherwise.
|
||||
*/
|
||||
export const useIsRestoring = () => useContext(IsRestoringContext)
|
||||
|
||||
/**
|
||||
* The Provider that `PersistQueryClientProvider` uses to signal whether a persisted client is currently
|
||||
* being restored, read by `useIsRestoring`.
|
||||
*/
|
||||
export const IsRestoringProvider = IsRestoringContext.Provider
|
||||
96
packages/solid-query/src/mutationOptions.ts
Normal file
96
packages/solid-query/src/mutationOptions.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { DefaultError, WithRequired } from '@tanstack/query-core'
|
||||
import type { MutationOptions } from './types'
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `mutationOptions` that you can also pass to `useMutation`. A
|
||||
* `mutationKey` is required on this overload so the mutation can be looked up later, e.g. with
|
||||
* `useMutationState`.
|
||||
*
|
||||
* @see {@link useMutation} to run the mutation these options describe.
|
||||
* @param options - The mutation options to use, identical to what you'd pass to `useMutation`, with a
|
||||
* required `mutationKey`.
|
||||
* @returns The same options object, unchanged.
|
||||
*
|
||||
* @example
|
||||
* Looking the mutation up elsewhere via its `mutationKey`, e.g. for a global "saving…" indicator:
|
||||
* ```tsx
|
||||
* import { mutationOptions, useMutationState } from '@tanstack/solid-query'
|
||||
*
|
||||
* const createPostOptions = mutationOptions({
|
||||
* mutationKey: ['posts', 'create'],
|
||||
* mutationFn: createPost,
|
||||
* })
|
||||
*
|
||||
* function SavingIndicator() {
|
||||
* const isCreatingPost = useMutationState(() => ({
|
||||
* filters: { mutationKey: createPostOptions.mutationKey, status: 'pending' },
|
||||
* }))
|
||||
*
|
||||
* return isCreatingPost().length > 0 ? <span>Saving…</span> : null
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: WithRequired<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): WithRequired<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
/**
|
||||
* You can generally pass everything to `mutationOptions` that you can also pass to `useMutation`. No
|
||||
* `mutationKey` is required on this overload — use this when you don't need to target the mutation via a
|
||||
* `mutationKey` filter later (e.g. with `useMutationState`); it can still be observed through other filters,
|
||||
* such as `status`.
|
||||
*
|
||||
* @see {@link useMutation} to run the mutation these options describe.
|
||||
* @param options - The mutation options to use, identical to what you'd pass to `useMutation`, without a
|
||||
* `mutationKey`.
|
||||
* @returns The same options object, unchanged.
|
||||
* @remarks See the other overload's example for looking a mutation up via `useMutationState`.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { mutationOptions, useMutation } from '@tanstack/solid-query'
|
||||
*
|
||||
* const createPostOptions = mutationOptions({
|
||||
* mutationFn: createPost,
|
||||
* })
|
||||
*
|
||||
* function CreatePost() {
|
||||
* const createPostMutation = useMutation(() => createPostOptions)
|
||||
* return <button onClick={() => createPostMutation.mutate({ title: 'Hello' })}>Create</button>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: Omit<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): Omit<
|
||||
MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: MutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
): MutationOptions<TData, TError, TVariables, TOnMutateResult> {
|
||||
return options
|
||||
}
|
||||
155
packages/solid-query/src/queryOptions.ts
Normal file
155
packages/solid-query/src/queryOptions.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
QueryKey,
|
||||
QueryKeyWithDataTag,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryOptions } from './types'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
/**
|
||||
* The options accepted by the `queryOptions` overload selected when no `initialData` is set — `data` may be
|
||||
* `undefined` while the query is `pending`. `queryOptions` itself accepts and returns a plain object (its
|
||||
* parameter type is `ReturnType<UndefinedInitialDataOptions<...>>`, i.e. this `Accessor` called); Solid's
|
||||
* reactivity applies where the result is consumed instead, e.g. `useQuery(() => options)`.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type UndefinedInitialDataOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = Accessor<
|
||||
QueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
initialData?: undefined
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* The options accepted by the `queryOptions` overload selected when `initialData` is set — `data` is never
|
||||
* `undefined`.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type DefinedInitialDataOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = Accessor<
|
||||
QueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
initialData: TQueryFnData | (() => TQueryFnData)
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can
|
||||
* be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and
|
||||
* is the query key to generate options for.
|
||||
*
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* @see {@link useQuery} to run a query with these options.
|
||||
* @param options - The {@link DefinedInitialDataOptions} to use — everything you can pass to `useQuery`, with `initialData` set.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { For } from 'solid-js'
|
||||
* import { queryOptions, useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* const postsOptions = queryOptions({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* initialData: [],
|
||||
* })
|
||||
*
|
||||
* function Posts() {
|
||||
* // `postsQuery.data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails,
|
||||
* // so the list stays visible alongside the error.
|
||||
* const postsQuery = useQuery(() => postsOptions)
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {postsQuery.isError ? <span>Error: {postsQuery.error.message}</span> : null}
|
||||
* <ul>
|
||||
* <For each={postsQuery.data}>{(post) => <li>{post.title}</li>}</For>
|
||||
* </ul>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: ReturnType<
|
||||
DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
): ReturnType<
|
||||
DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can
|
||||
* be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and
|
||||
* is the query key to generate options for.
|
||||
*
|
||||
* @see {@link useQuery} to run a query with these options.
|
||||
* @param options - The {@link UndefinedInitialDataOptions} to use — everything you can pass to `useQuery`.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* A parameterized factory, so the same options object can be reused per `id`:
|
||||
* ```tsx
|
||||
* import { Match, Switch } from 'solid-js'
|
||||
* import { queryOptions, useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* const postOptions = (id: string) =>
|
||||
* queryOptions({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })
|
||||
*
|
||||
* function Post(props: { id: string }) {
|
||||
* const postQuery = useQuery(() => postOptions(props.id))
|
||||
*
|
||||
* return (
|
||||
* <Switch>
|
||||
* <Match when={postQuery.isPending}>Loading...</Match>
|
||||
* <Match when={postQuery.isError}>Error: {postQuery.error.message}</Match>
|
||||
* <Match when={postQuery.isSuccess}>
|
||||
* <h1>{postQuery.data.title}</h1>
|
||||
* </Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: ReturnType<
|
||||
UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
): ReturnType<
|
||||
UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
|
||||
|
||||
export function queryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
339
packages/solid-query/src/types.ts
Normal file
339
packages/solid-query/src/types.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import type {
|
||||
DefaultError,
|
||||
DefinedInfiniteQueryObserverResult,
|
||||
DefinedQueryObserverResult,
|
||||
InfiniteQueryObserverResult,
|
||||
MutateFunction,
|
||||
MutationObserverOptions,
|
||||
MutationObserverResult,
|
||||
OmitKeyof,
|
||||
Override,
|
||||
QueryKey,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
InfiniteQueryObserverOptions,
|
||||
QueryObserverOptions,
|
||||
} from './QueryClient'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
/**
|
||||
* The options accepted by `useQuery`. Extends {@link QueryObserverOptions} from `@tanstack/query-core` with
|
||||
* the `solid-query`-specific `deferStream` and `suspense` options.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs. Defaults to `TQueryFnData` when no
|
||||
* `select` is used.
|
||||
* @template TQueryData - The type of the data actually held in the query cache — the input to `select` and
|
||||
* `placeholderData`. Defaults to, and is usually the same as, `TQueryFnData`.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export interface UseBaseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> extends OmitKeyof<
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
|
||||
'suspense'
|
||||
> {
|
||||
/**
|
||||
* Only applicable while rendering queries on the server with streaming.
|
||||
* Set `deferStream` to `true` to wait for the query to resolve on the server before flushing the stream.
|
||||
* This can be useful to avoid sending a loading state to the client before the query has resolved.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
deferStream?: boolean
|
||||
/**
|
||||
* @deprecated The `suspense` option has been deprecated in v5 and will be removed in the next major version.
|
||||
* The `data` property on useQuery is a SolidJS resource and will automatically suspend when the data is loading.
|
||||
* Setting `suspense` to `false` will be a no-op.
|
||||
*/
|
||||
suspense?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The options accepted by `useQuery` and `queryOptions`.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs. Defaults to `TQueryFnData` when no
|
||||
* `select` is used.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export interface QueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> extends UseBaseQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryFnData,
|
||||
TQueryKey
|
||||
> {}
|
||||
|
||||
/**
|
||||
* The accessor `useQuery` expects as its first argument — Solid re-evaluates it reactively, so `queryKey` and
|
||||
* other options can depend on signals.
|
||||
*
|
||||
* @template TQueryFnData - The type your `queryFn` resolves to.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs. Defaults to `TQueryFnData` when no
|
||||
* `select` is used.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
*/
|
||||
export type UseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = Accessor<QueryOptions<TQueryFnData, TError, TData, TQueryKey>>
|
||||
|
||||
/* --- Create Query and Create Base Query Types --- */
|
||||
|
||||
/**
|
||||
* The object `useQuery` returns when `initialData` isn't set — `data`/`error` may still be `undefined`/`null`
|
||||
* while the query is `pending`. Re-exports {@link QueryObserverResult} from `@tanstack/query-core`.
|
||||
* `useInfiniteQuery` returns {@link UseInfiniteQueryResult} instead.
|
||||
*
|
||||
* @template TData - The type `data` ends up as, after `select` runs (if set).
|
||||
* @template TError - The type of errors this query may hold.
|
||||
*/
|
||||
export type UseBaseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = QueryObserverResult<TData, TError>
|
||||
|
||||
/**
|
||||
* The object `useQuery` returns — `data`/`error` may still be `undefined`/`null` while the query is
|
||||
* `pending`.
|
||||
*
|
||||
* @template TData - The type `data` ends up as, after `select` runs (if set).
|
||||
* @template TError - The type of errors this query may hold.
|
||||
*/
|
||||
export type UseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = UseBaseQueryResult<TData, TError>
|
||||
|
||||
/**
|
||||
* The object `useQuery` returns when `initialData` guarantees `data` is never `undefined`.
|
||||
*
|
||||
* @template TData - The type `data` ends up as, after `select` runs (if set).
|
||||
* @template TError - The type of errors this query may hold.
|
||||
*/
|
||||
export type DefinedUseBaseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedQueryObserverResult<TData, TError>
|
||||
|
||||
/**
|
||||
* The object `useQuery` returns when `initialData` guarantees `data` is never `undefined`.
|
||||
*
|
||||
* @template TData - The type `data` ends up as, after `select` runs (if set).
|
||||
* @template TError - The type of errors this query may hold.
|
||||
*/
|
||||
export type DefinedUseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedUseBaseQueryResult<TData, TError>
|
||||
|
||||
/* --- Create Infinite Queries Types --- */
|
||||
/**
|
||||
* The options accepted by `useInfiniteQuery`.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export interface InfiniteQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> extends OmitKeyof<
|
||||
InfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
'queryKey' | 'suspense'
|
||||
> {
|
||||
queryKey: TQueryKey
|
||||
/**
|
||||
* Only applicable while rendering queries on the server with streaming.
|
||||
* Set `deferStream` to `true` to wait for the query to resolve on the server before flushing the stream.
|
||||
* This can be useful to avoid sending a loading state to the client before the query has resolved.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
deferStream?: boolean
|
||||
/**
|
||||
* @deprecated The `suspense` option has been deprecated in v5 and will be removed in the next major version.
|
||||
* The `data` property on useInfiniteQuery is a SolidJS resource and will automatically suspend when the data is loading.
|
||||
* Setting `suspense` to `false` will be a no-op.
|
||||
*/
|
||||
suspense?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The accessor `useInfiniteQuery` expects as its first argument — Solid re-evaluates it reactively, so
|
||||
* `queryKey` and other options can depend on signals.
|
||||
*
|
||||
* @template TQueryFnData - The type of a single page, as your `queryFn` resolves it.
|
||||
* @template TError - The type of errors your `queryFn` may throw.
|
||||
* @template TData - The type `data` ends up as after `select` runs.
|
||||
* @template TQueryKey - The type of your `queryKey`.
|
||||
* @template TPageParam - The type of the parameter passed to `queryFn` to fetch a given page.
|
||||
*/
|
||||
export type UseInfiniteQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = Accessor<
|
||||
InfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
|
||||
>
|
||||
|
||||
/**
|
||||
* The object `useInfiniteQuery` returns — `data`/`error` may still be `undefined`/`null` while the query is
|
||||
* `pending`.
|
||||
*
|
||||
* @template TData - The type `data` ends up as, after `select` runs (if set).
|
||||
* @template TError - The type of errors this query may hold.
|
||||
*/
|
||||
export type UseInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = InfiniteQueryObserverResult<TData, TError>
|
||||
|
||||
/**
|
||||
* The object `useInfiniteQuery` returns when `initialData` guarantees `data` is never `undefined`.
|
||||
*
|
||||
* @template TData - The type `data` ends up as, after `select` runs (if set).
|
||||
* @template TError - The type of errors this query may hold.
|
||||
*/
|
||||
export type DefinedUseInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedInfiniteQueryObserverResult<TData, TError>
|
||||
|
||||
/* --- Create Mutation Types --- */
|
||||
/**
|
||||
* The options accepted by `useMutation` and `mutationOptions`.
|
||||
*
|
||||
* @template TData - The type your `mutationFn` resolves to.
|
||||
* @template TError - The type of errors your `mutationFn` may throw.
|
||||
* @template TVariables - The type of the variables your `mutationFn` accepts.
|
||||
* @template TOnMutateResult - The type returned by `onMutate`, passed on to `onSuccess`/`onError`/`onSettled`.
|
||||
*/
|
||||
export interface MutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> extends OmitKeyof<
|
||||
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'_defaulted'
|
||||
> {}
|
||||
|
||||
/**
|
||||
* The accessor `useMutation` expects as its first argument — Solid re-evaluates it reactively, so callbacks
|
||||
* and other options can depend on signals.
|
||||
*
|
||||
* @template TData - The type your `mutationFn` resolves to.
|
||||
* @template TError - The type of errors your `mutationFn` may throw.
|
||||
* @template TVariables - The type of the variables your `mutationFn` accepts.
|
||||
* @template TOnMutateResult - The type returned by `onMutate`, passed on to `onSuccess`/`onError`/`onSettled`.
|
||||
*/
|
||||
export type UseMutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = Accessor<MutationOptions<TData, TError, TVariables, TOnMutateResult>>
|
||||
|
||||
export type UseMutateFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = (
|
||||
...args: Parameters<
|
||||
MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
>
|
||||
) => void
|
||||
|
||||
/**
|
||||
* The type of `mutateAsync`, as returned by `useMutation`. Similar to {@link UseMutateFunction}, but returns a
|
||||
* promise which can be awaited.
|
||||
*
|
||||
* @template TData - The type your `mutationFn` resolves to.
|
||||
* @template TError - The type of errors your `mutationFn` may throw.
|
||||
* @template TVariables - The type of the variable passed to `mutateAsync`.
|
||||
* @template TOnMutateResult - The type returned by `onMutate`, passed to `onSuccess`/`onError`/`onSettled` as
|
||||
* their `onMutateResult` parameter — useful for optimistic-update rollback data.
|
||||
*/
|
||||
export type UseMutateAsyncFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
|
||||
/**
|
||||
* The result of `useMutation`. Same as {@link MutationObserverResult} from `@tanstack/query-core`, with
|
||||
* `mutate` narrowed to the fire-and-forget {@link UseMutateFunction} signature, plus the added `mutateAsync`.
|
||||
*
|
||||
* @template TData - The type your `mutationFn` resolves to.
|
||||
* @template TError - The type of errors your `mutationFn` may throw.
|
||||
* @template TVariables - The type of the variable passed to `mutate`/`mutateAsync`.
|
||||
* @template TOnMutateResult - The type returned by `onMutate`, passed to `onSuccess`/`onError`/`onSettled` as
|
||||
* their `onMutateResult` parameter — useful for optimistic-update rollback data.
|
||||
*/
|
||||
export type UseBaseMutationResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = unknown,
|
||||
TOnMutateResult = unknown,
|
||||
> = Override<
|
||||
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
|
||||
{ mutate: UseMutateFunction<TData, TError, TVariables, TOnMutateResult> }
|
||||
> & {
|
||||
/**
|
||||
* Similar to `mutate`, but returns a promise which can be awaited.
|
||||
*/
|
||||
mutateAsync: UseMutateAsyncFunction<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of `useMutation`. Same as {@link UseBaseMutationResult}.
|
||||
*
|
||||
* @template TData - The type your `mutationFn` resolves to.
|
||||
* @template TError - The type of errors your `mutationFn` may throw.
|
||||
* @template TVariables - The type of the variable passed to `mutate`/`mutateAsync`.
|
||||
* @template TOnMutateResult - The type returned by `onMutate`, passed to `onSuccess`/`onError`/`onSettled` as
|
||||
* their `onMutateResult` parameter — useful for optimistic-update rollback data.
|
||||
*/
|
||||
export type UseMutationResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = unknown,
|
||||
TOnMutateResult = unknown,
|
||||
> = UseBaseMutationResult<TData, TError, TVariables, TOnMutateResult>
|
||||
387
packages/solid-query/src/useBaseQuery.ts
Normal file
387
packages/solid-query/src/useBaseQuery.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
// Had to disable the lint rule because isServer type is defined as false
|
||||
// in solid-js/web package. I'll create a GitHub issue with them to see
|
||||
// why that happens.
|
||||
import { hydrate, notifyManager, shouldThrowError } from '@tanstack/query-core'
|
||||
import { isServer } from 'solid-js/web'
|
||||
import {
|
||||
createComputed,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
on,
|
||||
onCleanup,
|
||||
} from 'solid-js'
|
||||
import { createStore, reconcile, unwrap } from 'solid-js/store'
|
||||
import { useQueryClientResolver } from './QueryClientProvider'
|
||||
import { useIsRestoring } from './isRestoring'
|
||||
import type { UseBaseQueryOptions } from './types'
|
||||
import type { Accessor, Signal } from 'solid-js'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type {
|
||||
Query,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
function reconcileFn<TData, TError>(
|
||||
store: QueryObserverResult<TData, TError>,
|
||||
result: QueryObserverResult<TData, TError>,
|
||||
reconcileOption:
|
||||
| string
|
||||
| false
|
||||
| ((oldData: TData | undefined, newData: TData) => TData),
|
||||
queryHash?: string,
|
||||
): QueryObserverResult<TData, TError> {
|
||||
if (reconcileOption === false) return result
|
||||
if (typeof reconcileOption === 'function') {
|
||||
const newData = reconcileOption(store.data, result.data as TData)
|
||||
return { ...result, data: newData } as typeof result
|
||||
}
|
||||
let data = result.data
|
||||
if (store.data === undefined) {
|
||||
try {
|
||||
data = structuredClone(data)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (error instanceof Error) {
|
||||
console.warn(
|
||||
`Unable to correctly reconcile data for query key: ${queryHash}. ` +
|
||||
`Possibly because the query data contains data structures that aren't supported ` +
|
||||
`by the 'structuredClone' algorithm. Consider using a callback function instead ` +
|
||||
`to manage the reconciliation manually.\n\n Error Received: ${error.name} - ${error.message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const newData = reconcile(data, { key: reconcileOption })(store.data)
|
||||
return { ...result, data: newData } as typeof result
|
||||
}
|
||||
|
||||
/**
|
||||
* Solid's `onHydrated` functionality will silently "fail" (hydrate with an empty object)
|
||||
* if the resource data is not serializable.
|
||||
*/
|
||||
const hydratableObserverResult = <
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey extends QueryKey,
|
||||
TDataHydratable,
|
||||
>(
|
||||
query: Query<TQueryFnData, TError, TData, TQueryKey>,
|
||||
result: QueryObserverResult<TDataHydratable, TError>,
|
||||
) => {
|
||||
if (!isServer) return result
|
||||
const obj: any = {
|
||||
...unwrap(result),
|
||||
// During SSR, functions cannot be serialized, so we need to remove them
|
||||
// This is safe because we will add these functions back when the query is hydrated
|
||||
refetch: undefined,
|
||||
}
|
||||
|
||||
// If the query is an infinite query, we need to remove additional properties
|
||||
if ('fetchNextPage' in result) {
|
||||
obj.fetchNextPage = undefined
|
||||
obj.fetchPreviousPage = undefined
|
||||
}
|
||||
|
||||
// We will also attach the dehydrated state of the query to the result
|
||||
// This will be removed on client after hydration
|
||||
obj.hydrationData = {
|
||||
state: query.state,
|
||||
queryKey: query.queryKey,
|
||||
queryHash: query.queryHash,
|
||||
...(query.meta && { meta: query.meta }),
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// Base Query Function that is used to create the query.
|
||||
export function useBaseQuery<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(
|
||||
options: Accessor<
|
||||
UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
||||
>,
|
||||
Observer: typeof QueryObserver,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
) {
|
||||
type ResourceData = QueryObserverResult<TData, TError>
|
||||
|
||||
const resolveClient = useQueryClientResolver(queryClient)
|
||||
const client = createMemo(() => resolveClient())
|
||||
const isRestoring = useIsRestoring()
|
||||
// There are times when we run a query on the server but the resource is never read
|
||||
// This could lead to times when the queryObserver is unsubscribed before the resource has loaded
|
||||
// Causing a time out error. To prevent this we will queue the unsubscribe if the cleanup is called
|
||||
// before the resource has loaded
|
||||
let unsubscribeQueued = false
|
||||
|
||||
const defaultedOptions = createMemo(() => {
|
||||
const defaultOptions = client().defaultQueryOptions(options())
|
||||
defaultOptions._optimisticResults = isRestoring()
|
||||
? 'isRestoring'
|
||||
: 'optimistic'
|
||||
defaultOptions.structuralSharing = false
|
||||
if (isServer) {
|
||||
defaultOptions.retry = false
|
||||
defaultOptions.throwOnError = true
|
||||
}
|
||||
return defaultOptions
|
||||
})
|
||||
const initialOptions = defaultedOptions()
|
||||
|
||||
const [observer, setObserver] = createSignal(
|
||||
new Observer(client(), defaultedOptions()),
|
||||
)
|
||||
|
||||
let observerResult = observer().getOptimisticResult(defaultedOptions())
|
||||
const [state, setState] =
|
||||
createStore<QueryObserverResult<TData, TError>>(observerResult)
|
||||
|
||||
const createServerSubscriber = (
|
||||
resolve: (
|
||||
data: ResourceData | PromiseLike<ResourceData | undefined> | undefined,
|
||||
) => void,
|
||||
reject: (reason?: any) => void,
|
||||
) => {
|
||||
return observer().subscribe((result) => {
|
||||
notifyManager.batchCalls(() => {
|
||||
const query = observer().getCurrentQuery()
|
||||
const unwrappedResult = hydratableObserverResult(query, result)
|
||||
|
||||
if (result.data !== undefined && unwrappedResult.isError) {
|
||||
reject(unwrappedResult.error)
|
||||
unsubscribeIfQueued()
|
||||
} else {
|
||||
resolve(unwrappedResult)
|
||||
unsubscribeIfQueued()
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const unsubscribeIfQueued = () => {
|
||||
if (unsubscribeQueued) {
|
||||
unsubscribe?.()
|
||||
unsubscribeQueued = false
|
||||
}
|
||||
}
|
||||
|
||||
const createClientSubscriber = () => {
|
||||
const obs = observer()
|
||||
return obs.subscribe((result) => {
|
||||
observerResult = result
|
||||
queueMicrotask(() => {
|
||||
if (unsubscribe) {
|
||||
refetch()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setStateWithReconciliation(res: typeof observerResult) {
|
||||
const opts = observer().options
|
||||
// @ts-expect-error - Reconcile option is not correctly typed internally
|
||||
const reconcileOptions = opts.reconcile
|
||||
|
||||
setState((store) => {
|
||||
return reconcileFn(
|
||||
store,
|
||||
res,
|
||||
reconcileOptions === undefined ? false : reconcileOptions,
|
||||
opts.queryHash,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function createDeepSignal<T>(): Signal<T> {
|
||||
return [
|
||||
() => state,
|
||||
(v: any) => {
|
||||
const unwrapped = unwrap(state)
|
||||
if (typeof v === 'function') {
|
||||
v = v(unwrapped)
|
||||
}
|
||||
// Hydration data exists on first load after SSR,
|
||||
// and should be removed from the observer result
|
||||
if (v?.hydrationData) {
|
||||
const { hydrationData, ...rest } = v
|
||||
v = rest
|
||||
}
|
||||
setStateWithReconciliation(v)
|
||||
},
|
||||
] as Signal<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe is set lazily, so that we can subscribe after hydration when needed.
|
||||
*/
|
||||
let unsubscribe: (() => void) | null = null
|
||||
|
||||
/*
|
||||
Fixes #7275
|
||||
In a few cases, the observer could unmount before the resource is loaded.
|
||||
This leads to Suspense boundaries to be suspended indefinitely.
|
||||
This resolver will be called when the observer is unmounting
|
||||
but the resource is still in a loading state
|
||||
*/
|
||||
let resolver: ((value: ResourceData) => void) | null = null
|
||||
const [queryResource, { refetch }] = createResource<ResourceData | undefined>(
|
||||
() => {
|
||||
const obs = observer()
|
||||
return new Promise((resolve, reject) => {
|
||||
resolver = resolve
|
||||
if (isServer) {
|
||||
unsubscribe = createServerSubscriber(resolve, reject)
|
||||
} else if (!unsubscribe && !isRestoring()) {
|
||||
unsubscribe = createClientSubscriber()
|
||||
}
|
||||
obs.updateResult()
|
||||
|
||||
if (
|
||||
observerResult.isError &&
|
||||
!observerResult.isFetching &&
|
||||
!isRestoring() &&
|
||||
shouldThrowError(obs.options.throwOnError, [
|
||||
observerResult.error,
|
||||
obs.getCurrentQuery(),
|
||||
])
|
||||
) {
|
||||
setStateWithReconciliation(observerResult)
|
||||
return reject(observerResult.error)
|
||||
}
|
||||
if (!observerResult.isLoading) {
|
||||
resolver = null
|
||||
return resolve(
|
||||
hydratableObserverResult(obs.getCurrentQuery(), observerResult),
|
||||
)
|
||||
}
|
||||
|
||||
setStateWithReconciliation(observerResult)
|
||||
})
|
||||
},
|
||||
{
|
||||
storage: createDeepSignal,
|
||||
|
||||
get deferStream() {
|
||||
return options().deferStream
|
||||
},
|
||||
|
||||
/**
|
||||
* If this resource was populated on the server (either sync render, or streamed in over time), onHydrated
|
||||
* will be called. This is the point at which we can hydrate the query cache state, and setup the query subscriber.
|
||||
*
|
||||
* Leveraging onHydrated allows us to plug into the async and streaming support that solidjs resources already support.
|
||||
*
|
||||
* Note that this is only invoked on the client, for queries that were originally run on the server.
|
||||
*/
|
||||
onHydrated(_k, info) {
|
||||
if (info.value && 'hydrationData' in info.value) {
|
||||
hydrate(client(), {
|
||||
// @ts-expect-error - hydrationData is not correctly typed internally
|
||||
queries: [{ ...info.value.hydrationData }],
|
||||
})
|
||||
}
|
||||
|
||||
if (unsubscribe) return
|
||||
/**
|
||||
* Do not refetch query on mount if query was fetched on server,
|
||||
* even if `staleTime` is not set.
|
||||
*/
|
||||
const newOptions = { ...initialOptions }
|
||||
if (
|
||||
(initialOptions.staleTime || !initialOptions.initialData) &&
|
||||
info.value
|
||||
) {
|
||||
newOptions.refetchOnMount = false
|
||||
}
|
||||
// Setting the options as an immutable object to prevent
|
||||
// wonky behavior with observer subscriptions
|
||||
observer().setOptions(newOptions)
|
||||
setStateWithReconciliation(observer().getOptimisticResult(newOptions))
|
||||
unsubscribe = createClientSubscriber()
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
createComputed(
|
||||
on(
|
||||
client,
|
||||
(c) => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
}
|
||||
const newObserver = new Observer(c, defaultedOptions())
|
||||
unsubscribe = createClientSubscriber()
|
||||
setObserver(newObserver)
|
||||
},
|
||||
{
|
||||
defer: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
createComputed(
|
||||
on(
|
||||
isRestoring,
|
||||
(restoring) => {
|
||||
if (!restoring && !isServer) {
|
||||
refetch()
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (isServer && queryResource.loading) {
|
||||
unsubscribeQueued = true
|
||||
return
|
||||
}
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
unsubscribe = null
|
||||
}
|
||||
if (resolver && !isServer) {
|
||||
resolver(observerResult)
|
||||
resolver = null
|
||||
}
|
||||
})
|
||||
|
||||
createComputed(
|
||||
on(
|
||||
[observer, defaultedOptions],
|
||||
([obs, opts]) => {
|
||||
obs.setOptions(opts)
|
||||
setStateWithReconciliation(obs.getOptimisticResult(opts))
|
||||
refetch()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const handler = {
|
||||
get(
|
||||
target: QueryObserverResult<TData, TError>,
|
||||
prop: keyof QueryObserverResult<TData, TError>,
|
||||
): any {
|
||||
if (prop === 'data') {
|
||||
if (state.data !== undefined) {
|
||||
return queryResource.latest?.data
|
||||
}
|
||||
return queryResource()?.data
|
||||
}
|
||||
return Reflect.get(target, prop)
|
||||
},
|
||||
}
|
||||
|
||||
return new Proxy(state, handler)
|
||||
}
|
||||
227
packages/solid-query/src/useInfiniteQuery.ts
Normal file
227
packages/solid-query/src/useInfiniteQuery.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
||||
import { createMemo } from 'solid-js'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type {
|
||||
DefinedUseInfiniteQueryResult,
|
||||
UseInfiniteQueryOptions,
|
||||
UseInfiniteQueryResult,
|
||||
} from './types'
|
||||
import type { Accessor } from 'solid-js'
|
||||
import type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions'
|
||||
|
||||
/**
|
||||
* The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of
|
||||
* `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`.
|
||||
*
|
||||
* This overload is selected when `initialData` is set.
|
||||
*
|
||||
* @remarks Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default
|
||||
* refetch behavior, resulting in outdated data. Make sure to call these functions only in response to user
|
||||
* actions, or add conditions like `hasNextPage && !isFetching`.
|
||||
* @see {@link infiniteQueryOptions} to share these options between `useInfiniteQuery` and imperative APIs like `queryClient.infiniteQuery`.
|
||||
* @param options - An accessor returning the {@link DefinedInitialDataInfiniteOptions} to use — everything you
|
||||
* can pass to `useInfiniteQuery`, with `initialData` set.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns The same properties as `useQuery`, with the addition of `fetchNextPage`, `fetchPreviousPage`,
|
||||
* `hasNextPage`, `hasPreviousPage`, `isFetchingNextPage`, and `isFetchingPreviousPage`. `data.pages` and
|
||||
* `data.pageParams` are also added, as long as a `select` doesn't change `TData` away from its default
|
||||
* `InfiniteData<TQueryFnData>` shape.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { For } from 'solid-js'
|
||||
* import { useInfiniteQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Projects() {
|
||||
* // `projectsQuery.data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the
|
||||
* // list stays visible alongside the error.
|
||||
* const projectsQuery = useInfiniteQuery(() => ({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* initialData: { pages: [], pageParams: [] },
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {projectsQuery.isError ? <span>Error: {projectsQuery.error.message}</span> : null}
|
||||
* <ul>
|
||||
* <For each={projectsQuery.data.pages}>
|
||||
* {(page) => <For each={page.projects}>{(p) => <li>{p.name}</li>}</For>}
|
||||
* </For>
|
||||
* </ul>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): DefinedUseInfiniteQueryResult<TData, TError>
|
||||
|
||||
/**
|
||||
* The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of
|
||||
* `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`.
|
||||
*
|
||||
* @remarks Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default
|
||||
* refetch behavior, resulting in outdated data. Make sure to call these functions only in response to user
|
||||
* actions, or add conditions like `hasNextPage && !isFetching`.
|
||||
* @see {@link infiniteQueryOptions} to share these options between `useInfiniteQuery` and imperative APIs like `queryClient.infiniteQuery`.
|
||||
* @param options - An accessor returning the {@link UndefinedInitialDataInfiniteOptions} to use — everything
|
||||
* you can pass to `useInfiniteQuery`.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns The same properties as `useQuery`, with the addition of `fetchNextPage`, `fetchPreviousPage`,
|
||||
* `hasNextPage`, `hasPreviousPage`, `isFetchingNextPage`, and `isFetchingPreviousPage`. `data.pages` and
|
||||
* `data.pageParams` are also added, as long as a `select` doesn't change `TData` away from its default
|
||||
* `InfiniteData<TQueryFnData>` shape.
|
||||
*
|
||||
* @example
|
||||
* Fetching the next page from a "Load More" button click:
|
||||
* ```tsx
|
||||
* import { For, Match, Switch } from 'solid-js'
|
||||
* import { useInfiniteQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Projects() {
|
||||
* const projectsQuery = useInfiniteQuery(() => ({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch>
|
||||
* <Match when={projectsQuery.isPending}>Loading...</Match>
|
||||
* <Match when={projectsQuery.isError}>Error: {projectsQuery.error.message}</Match>
|
||||
* <Match when={projectsQuery.isSuccess}>
|
||||
* <ul>
|
||||
* <For each={projectsQuery.data.pages}>
|
||||
* {(page) => <For each={page.projects}>{(p) => <li>{p.name}</li>}</For>}
|
||||
* </For>
|
||||
* </ul>
|
||||
* <button
|
||||
* onClick={() => projectsQuery.fetchNextPage()}
|
||||
* disabled={!projectsQuery.hasNextPage || projectsQuery.isFetching}
|
||||
* >
|
||||
* {projectsQuery.isFetchingNextPage
|
||||
* ? 'Loading more...'
|
||||
* : projectsQuery.hasNextPage
|
||||
* ? 'Load More'
|
||||
* : 'Nothing more to load'}
|
||||
* </button>
|
||||
* </Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a
|
||||
* sentinel element after the list:
|
||||
* ```tsx
|
||||
* import { For, Match, Switch, createEffect, onCleanup } from 'solid-js'
|
||||
* import { useInfiniteQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Projects() {
|
||||
* const projectsQuery = useInfiniteQuery(() => ({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* }))
|
||||
*
|
||||
* let sentinelRef: HTMLDivElement | undefined
|
||||
*
|
||||
* createEffect(() => {
|
||||
* if (sentinelRef == null || !projectsQuery.hasNextPage || projectsQuery.isFetching) return
|
||||
*
|
||||
* const observer = new IntersectionObserver(([entry]) => {
|
||||
* if (entry?.isIntersecting) projectsQuery.fetchNextPage()
|
||||
* })
|
||||
* observer.observe(sentinelRef)
|
||||
*
|
||||
* onCleanup(() => observer.disconnect())
|
||||
* })
|
||||
*
|
||||
* return (
|
||||
* <Switch>
|
||||
* <Match when={projectsQuery.isPending}>Loading...</Match>
|
||||
* <Match when={projectsQuery.isError}>Error: {projectsQuery.error.message}</Match>
|
||||
* <Match when={projectsQuery.isSuccess}>
|
||||
* <ul>
|
||||
* <For each={projectsQuery.data.pages}>
|
||||
* {(page) => <For each={page.projects}>{(p) => <li>{p.name}</li>}</For>}
|
||||
* </For>
|
||||
* </ul>
|
||||
* <div ref={sentinelRef}>{projectsQuery.isFetchingNextPage ? 'Loading more...' : null}</div>
|
||||
* </Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): UseInfiniteQueryResult<TData, TError>
|
||||
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): UseInfiniteQueryResult<TData, TError> {
|
||||
return useBaseQuery(
|
||||
createMemo(() => options()),
|
||||
InfiniteQueryObserver as typeof QueryObserver,
|
||||
queryClient,
|
||||
) as UseInfiniteQueryResult<TData, TError>
|
||||
}
|
||||
50
packages/solid-query/src/useIsFetching.ts
Normal file
50
packages/solid-query/src/useIsFetching.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
|
||||
import { useQueryClientResolver } from './QueryClientProvider'
|
||||
import type { QueryFilters } from '@tanstack/query-core'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
/**
|
||||
* The `useIsFetching` hook returns the `number` of the queries that your application is loading or fetching
|
||||
* in the background (useful for app-wide loading indicators).
|
||||
*
|
||||
* @param filters - An accessor returning the {@link QueryFilters} to narrow down the matched queries.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns An accessor for the `number` of the queries that your application is currently loading or fetching
|
||||
* in the background.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useIsFetching } from '@tanstack/solid-query'
|
||||
*
|
||||
* function GlobalLoadingIndicator() {
|
||||
* // How many queries matching the posts prefix are fetching?
|
||||
* const isFetchingPosts = useIsFetching(() => ({ queryKey: ['posts'] }))
|
||||
*
|
||||
* return isFetchingPosts() > 0 ? <span>Loading posts...</span> : null
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useIsFetching(
|
||||
filters?: Accessor<QueryFilters>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): Accessor<number> {
|
||||
const resolveClient = useQueryClientResolver(queryClient)
|
||||
const client = createMemo(() => resolveClient())
|
||||
const queryCache = createMemo(() => client().getQueryCache())
|
||||
|
||||
const [fetches, setFetches] = createSignal(client().isFetching(filters?.()))
|
||||
|
||||
createEffect(() => {
|
||||
setFetches(client().isFetching(filters?.()))
|
||||
|
||||
const unsubscribe = queryCache().subscribe(() => {
|
||||
setFetches(client().isFetching(filters?.()))
|
||||
})
|
||||
|
||||
onCleanup(unsubscribe)
|
||||
})
|
||||
|
||||
return fetches
|
||||
}
|
||||
51
packages/solid-query/src/useIsMutating.ts
Normal file
51
packages/solid-query/src/useIsMutating.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
|
||||
import { useQueryClientResolver } from './QueryClientProvider'
|
||||
import type { MutationFilters } from '@tanstack/query-core'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
/**
|
||||
* The `useIsMutating` hook returns the `number` of mutations that your application currently has `pending`
|
||||
* (useful for app-wide loading indicators).
|
||||
*
|
||||
* @param filters - An accessor returning the {@link MutationFilters} to narrow down the matched mutations.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns An accessor for the `number` of the mutations that your application currently has `pending`.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useIsMutating } from '@tanstack/solid-query'
|
||||
*
|
||||
* function PostsMutatingIndicator() {
|
||||
* // How many mutations matching the posts prefix are in progress?
|
||||
* const isMutatingPosts = useIsMutating(() => ({ mutationKey: ['posts'] }))
|
||||
*
|
||||
* return isMutatingPosts() > 0 ? <span>Saving posts...</span> : null
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useIsMutating(
|
||||
filters?: Accessor<MutationFilters>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): Accessor<number> {
|
||||
const resolveClient = useQueryClientResolver(queryClient)
|
||||
const client = createMemo(() => resolveClient())
|
||||
const mutationCache = createMemo(() => client().getMutationCache())
|
||||
|
||||
const [mutations, setMutations] = createSignal(
|
||||
client().isMutating(filters?.()),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
setMutations(client().isMutating(filters?.()))
|
||||
|
||||
const unsubscribe = mutationCache().subscribe(() => {
|
||||
setMutations(client().isMutating(filters?.()))
|
||||
})
|
||||
|
||||
onCleanup(unsubscribe)
|
||||
})
|
||||
|
||||
return mutations
|
||||
}
|
||||
238
packages/solid-query/src/useMutation.ts
Normal file
238
packages/solid-query/src/useMutation.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { MutationObserver, noop, shouldThrowError } from '@tanstack/query-core'
|
||||
import { createComputed, createMemo, on, onCleanup } from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { useQueryClientResolver } from './QueryClientProvider'
|
||||
import type { DefaultError } from '@tanstack/query-core'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type {
|
||||
UseMutateFunction,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
} from './types'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
/**
|
||||
* @param options - An accessor returning the {@link UseMutationOptions} to use.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns `mutate`/`mutateAsync` also accept per-call `onSuccess`/`onError`/`onSettled` callbacks as a second
|
||||
* argument, useful for triggering call-site side effects (e.g. navigation) without coupling them to the shared
|
||||
* mutation definition. Hook-level callbacks (passed to `options`) fire for every mutation; per-call callbacks
|
||||
* fire only for the latest call you've made, and only while the component is still mounted — unmounting before
|
||||
* the mutation settles removes the subscription and prevents them from firing.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useMutation, useQueryClient } from '@tanstack/solid-query'
|
||||
*
|
||||
* function TodoItem(props: { id: number }) {
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const deleteTodoMutation = useMutation(() => ({
|
||||
* mutationFn: deleteTodo,
|
||||
* onSuccess: () => {
|
||||
* queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
* },
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <button onClick={() => deleteTodoMutation.mutate({ id: props.id })} disabled={deleteTodoMutation.isPending}>
|
||||
* Delete
|
||||
* </button>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Rendering the mutation's own state, rather than just firing it off:
|
||||
* ```tsx
|
||||
* import { Match, Switch } from 'solid-js'
|
||||
* import { useMutation, useQueryClient } from '@tanstack/solid-query'
|
||||
*
|
||||
* function AddTodo() {
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch fallback={<button onClick={() => addMutation.mutate('Item')}>Add</button>}>
|
||||
* <Match when={addMutation.isPending}>Adding todo...</Match>
|
||||
* <Match when={addMutation.isError}>
|
||||
* <div>An error occurred: {addMutation.error?.message}</div>
|
||||
* <button onClick={() => addMutation.mutate('Item')}>Add</button>
|
||||
* </Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Optimistic update via `onMutate`, rolling back on `onError`:
|
||||
* ```tsx
|
||||
* import { useMutation, useQueryClient } from '@tanstack/solid-query'
|
||||
*
|
||||
* function AddTodo() {
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onMutate: async (newTodo) => {
|
||||
* await queryClient.cancelQueries({ queryKey: ['todos'] })
|
||||
* const previousTodos = queryClient.getQueryData<Array<string>>(['todos'])
|
||||
*
|
||||
* queryClient.setQueryData<Array<string>>(['todos'], (old) => [
|
||||
* ...(old ?? []),
|
||||
* newTodo,
|
||||
* ])
|
||||
*
|
||||
* // Passed to `onError` as `onMutateResult` if the mutation fails.
|
||||
* return { previousTodos }
|
||||
* },
|
||||
* onError: (_err, _newTodo, onMutateResult) => {
|
||||
* queryClient.setQueryData(['todos'], onMutateResult?.previousTodos)
|
||||
* },
|
||||
* onSettled: () => {
|
||||
* queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
* },
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <button onClick={() => addMutation.mutate('Item')}>Add</button>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Callbacks passed per call to `mutate` only fire for the last call — `mutateAsync` gives you a
|
||||
* promise per call instead, so you can wait for all of them when they succeed:
|
||||
* ```tsx
|
||||
* import { useMutation, useQueryClient } from '@tanstack/solid-query'
|
||||
*
|
||||
* function AddTodos() {
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
*
|
||||
* async function handleAddAll(todos: Array<string>) {
|
||||
* try {
|
||||
* await Promise.all(todos.map((todo) => addMutation.mutateAsync(todo)))
|
||||
* } catch (error) {
|
||||
* console.error('Failed to add todos:', error)
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <button onClick={() => handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])}>
|
||||
* Add all
|
||||
* </button>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* If some of the mutations above can fail independently of the others, and you want to know which ones
|
||||
* did — rather than losing that information the moment the first one rejects — swap `Promise.all` for
|
||||
* `Promise.allSettled`:
|
||||
* ```tsx
|
||||
* import { useMutation, useQueryClient } from '@tanstack/solid-query'
|
||||
*
|
||||
* function AddTodos() {
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = useMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
*
|
||||
* async function handleAddAll(todos: Array<string>) {
|
||||
* const addResults = await Promise.allSettled(
|
||||
* todos.map((todo) => addMutation.mutateAsync(todo)),
|
||||
* )
|
||||
*
|
||||
* addResults.forEach((addResult, index) => {
|
||||
* if (addResult.status === 'rejected') {
|
||||
* console.error(`Failed to add "${todos[index]}":`, addResult.reason)
|
||||
* }
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <button onClick={() => handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])}>
|
||||
* Add all
|
||||
* </button>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useMutation<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): UseMutationResult<TData, TError, TVariables, TOnMutateResult> {
|
||||
const resolveClient = useQueryClientResolver(queryClient)
|
||||
const client = createMemo(() => resolveClient())
|
||||
|
||||
const observer = new MutationObserver<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult
|
||||
>(client(), options())
|
||||
|
||||
const mutate: UseMutateFunction<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult
|
||||
> = (...args) => {
|
||||
observer.mutate(args[0] as TVariables, args[1]).catch(noop)
|
||||
}
|
||||
|
||||
const [state, setState] = createStore<
|
||||
UseMutationResult<TData, TError, TVariables, TOnMutateResult>
|
||||
>({
|
||||
...observer.getCurrentResult(),
|
||||
mutate,
|
||||
mutateAsync: observer.getCurrentResult().mutate,
|
||||
})
|
||||
|
||||
createComputed(() => {
|
||||
observer.setOptions(options())
|
||||
})
|
||||
|
||||
createComputed(
|
||||
on(
|
||||
() => state.status,
|
||||
() => {
|
||||
if (
|
||||
state.isError &&
|
||||
shouldThrowError(observer.options.throwOnError, [state.error])
|
||||
) {
|
||||
throw state.error
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const unsubscribe = observer.subscribe((result) => {
|
||||
setState({
|
||||
...result,
|
||||
mutate,
|
||||
mutateAsync: result.mutate,
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(unsubscribe)
|
||||
|
||||
return state
|
||||
}
|
||||
155
packages/solid-query/src/useMutationState.ts
Normal file
155
packages/solid-query/src/useMutationState.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
|
||||
import { replaceEqualDeep } from '@tanstack/query-core'
|
||||
import { useQueryClientResolver } from './QueryClientProvider'
|
||||
import type {
|
||||
Mutation,
|
||||
MutationCache,
|
||||
MutationFilters,
|
||||
MutationState,
|
||||
} from '@tanstack/query-core'
|
||||
import type { Accessor } from 'solid-js'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
|
||||
type MutationTypeFromResult<TResult> = [TResult] extends [
|
||||
MutationState<
|
||||
infer TData,
|
||||
infer TError,
|
||||
infer TVariables,
|
||||
infer TOnMutateResult
|
||||
>,
|
||||
]
|
||||
? Mutation<TData, TError, TVariables, TOnMutateResult>
|
||||
: Mutation
|
||||
|
||||
type MutationStateOptions<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
> = {
|
||||
filters?: MutationFilters
|
||||
select?: (mutation: TMutation) => TResult
|
||||
}
|
||||
|
||||
function getResult<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
>(
|
||||
mutationCache: MutationCache,
|
||||
options: MutationStateOptions<TResult, TMutation>,
|
||||
): Array<TResult> {
|
||||
return mutationCache
|
||||
.findAll(options.filters)
|
||||
.map(
|
||||
(mutation): TResult =>
|
||||
(options.select
|
||||
? options.select(mutation as TMutation)
|
||||
: mutation.state) as TResult,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* `useMutationState` is a hook that gives you access to all mutations in the `MutationCache`. You can pass
|
||||
* `filters` ({@link MutationFilters}) to narrow down your mutations, and `select` to transform the mutation
|
||||
* state.
|
||||
*
|
||||
* @param options - An accessor returning the `filters` to narrow down matched mutations, and an optional
|
||||
* `select` to transform the mutation state.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns An accessor for an array of whatever `select` returns for each matching mutation.
|
||||
*
|
||||
* @example
|
||||
* Get all variables of all running mutations:
|
||||
* ```tsx
|
||||
* import { useMutationState } from '@tanstack/solid-query'
|
||||
*
|
||||
* function PendingPosts() {
|
||||
* const pendingVariables = useMutationState(() => ({
|
||||
* filters: { status: 'pending' },
|
||||
* select: (mutation) => mutation.state.variables,
|
||||
* }))
|
||||
*
|
||||
* return <>{pendingVariables().length} posts saving...</>
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Get all data for specific mutations via the `mutationKey`:
|
||||
* ```tsx
|
||||
* import { useMutation, useMutationState } from '@tanstack/solid-query'
|
||||
*
|
||||
* const mutationKey = ['posts']
|
||||
*
|
||||
* function Posts() {
|
||||
* // Some mutation that we want to get the state for
|
||||
* const createPostsMutation = useMutation(() => ({
|
||||
* mutationKey,
|
||||
* mutationFn: createPosts,
|
||||
* }))
|
||||
*
|
||||
* const savedPosts = useMutationState(() => ({
|
||||
* // this mutation key needs to match the mutation key of the given mutation (see above)
|
||||
* filters: { mutationKey, status: 'success' },
|
||||
* select: (mutation) => mutation.state.data,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <button onClick={() => createPostsMutation.mutate(['New Post'])}>
|
||||
* Create post ({savedPosts().length} saved so far)
|
||||
* </button>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Access the latest successful mutation data via the `mutationKey`. Each invocation of `mutate` adds a new
|
||||
* entry to the mutation cache for `gcTime` milliseconds — with the `status: 'success'` filter below, check the
|
||||
* last item that `useMutationState` returns to get the latest successful invocation:
|
||||
* ```tsx
|
||||
* import { useMutationState } from '@tanstack/solid-query'
|
||||
*
|
||||
* function LatestPost() {
|
||||
* const savedPosts = useMutationState(() => ({
|
||||
* filters: { mutationKey: ['posts'], status: 'success' },
|
||||
* select: (mutation) => mutation.state.data,
|
||||
* }))
|
||||
*
|
||||
* const latestPost = () => savedPosts()[savedPosts().length - 1]
|
||||
*
|
||||
* return <span>{latestPost()?.title}</span>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useMutationState<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
>(
|
||||
options: Accessor<MutationStateOptions<TResult, TMutation>> = () => ({}),
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): Accessor<Array<TResult>> {
|
||||
const resolveClient = useQueryClientResolver(queryClient)
|
||||
const client = createMemo(() => resolveClient())
|
||||
const mutationCache = createMemo(() => client().getMutationCache())
|
||||
|
||||
const [result, setResult] = createSignal(
|
||||
getResult(mutationCache(), options()),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const unsubscribe = mutationCache().subscribe(() => {
|
||||
const nextResult = replaceEqualDeep(
|
||||
result(),
|
||||
getResult(mutationCache(), options()),
|
||||
)
|
||||
if (result() !== nextResult) {
|
||||
setResult(nextResult)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(unsubscribe)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
431
packages/solid-query/src/useQueries.ts
Normal file
431
packages/solid-query/src/useQueries.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import { QueriesObserver, noop } from '@tanstack/query-core'
|
||||
import { createStore, unwrap } from 'solid-js/store'
|
||||
import {
|
||||
batch,
|
||||
createComputed,
|
||||
createMemo,
|
||||
createRenderEffect,
|
||||
createResource,
|
||||
mergeProps,
|
||||
on,
|
||||
onCleanup,
|
||||
onMount,
|
||||
} from 'solid-js'
|
||||
import { useQueryClientResolver } from './QueryClientProvider'
|
||||
import { useIsRestoring } from './isRestoring'
|
||||
import type { QueryOptions, UseQueryResult } from './types'
|
||||
import type { Accessor } from 'solid-js'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type {
|
||||
DefaultError,
|
||||
OmitKeyof,
|
||||
QueriesObserverOptions,
|
||||
QueriesPlaceholderDataFunction,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
QueryObserverOptions,
|
||||
QueryObserverResult,
|
||||
ThrowOnError,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
|
||||
// `placeholderData` function does not have a parameter
|
||||
type UseQueryOptionsForUseQueries<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = OmitKeyof<
|
||||
QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'placeholderData' | 'suspense'
|
||||
> & {
|
||||
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
|
||||
/**
|
||||
* @deprecated The `suspense` option has been deprecated in v5 and will be removed in the next major version.
|
||||
* The `data` property on useQueries is a plain object and not a SolidJS Resource.
|
||||
* It will not suspend when the data is loading.
|
||||
* Setting `suspense` to `true` will be a no-op.
|
||||
*/
|
||||
suspense?: boolean
|
||||
}
|
||||
|
||||
// Avoid TS depth-limit error in case of large array literal
|
||||
type MAXIMUM_DEPTH = 20
|
||||
|
||||
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
|
||||
type SkipTokenForUseQueries = symbol
|
||||
|
||||
type GetOptions<T> =
|
||||
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
|
||||
T extends {
|
||||
queryFnData: infer TQueryFnData
|
||||
error?: infer TError
|
||||
data: infer TData
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? UseQueryOptionsForUseQueries<unknown, TError, TData>
|
||||
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
|
||||
T extends [infer TQueryFnData, infer TError, infer TData]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData>
|
||||
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
unknown extends TError ? DefaultError : TError,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
TQueryKey
|
||||
>
|
||||
: // Fallback
|
||||
UseQueryOptionsForUseQueries
|
||||
|
||||
type GetResults<T> =
|
||||
// Part 1: responsible for mapping explicit type parameter to function result, if object
|
||||
T extends { queryFnData: any; error?: infer TError; data: infer TData }
|
||||
? UseQueryResult<TData, TError>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? UseQueryResult<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? UseQueryResult<TData, TError>
|
||||
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
|
||||
T extends [any, infer TError, infer TData]
|
||||
? UseQueryResult<TData, TError>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? UseQueryResult<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? UseQueryResult<TQueryFnData>
|
||||
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseQueryResult<
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: // Fallback
|
||||
UseQueryResult
|
||||
|
||||
/**
|
||||
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
|
||||
*/
|
||||
type QueriesOptions<
|
||||
T extends Array<any>,
|
||||
TResult extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseQueryOptionsForUseQueries>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResult, GetOptions<Head>]
|
||||
: T extends [infer Head, ...infer Tail]
|
||||
? QueriesOptions<
|
||||
[...Tail],
|
||||
[...TResult, GetOptions<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: ReadonlyArray<unknown> extends T
|
||||
? T
|
||||
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type!
|
||||
// use this to infer the param types in the case of Array.map() argument
|
||||
T extends Array<
|
||||
UseQueryOptionsForUseQueries<
|
||||
infer TQueryFnData,
|
||||
infer TError,
|
||||
infer TData,
|
||||
infer TQueryKey
|
||||
>
|
||||
>
|
||||
? Array<
|
||||
UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
>
|
||||
: // Fallback
|
||||
Array<UseQueryOptionsForUseQueries>
|
||||
|
||||
/**
|
||||
* QueriesResults reducer recursively maps type param to results
|
||||
*/
|
||||
type QueriesResults<
|
||||
T extends Array<any>,
|
||||
TResult extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseQueryResult>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResult, GetResults<Head>]
|
||||
: T extends [infer Head, ...infer Tail]
|
||||
? QueriesResults<
|
||||
[...Tail],
|
||||
[...TResult, GetResults<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: { [K in keyof T]: GetResults<T[K]> }
|
||||
|
||||
/**
|
||||
* The `useQueries` hook can be used to fetch a variable number of queries.
|
||||
*
|
||||
* The `queries` key accepts an array with query option objects mostly identical to `useQuery` — see
|
||||
* `placeholderData` below for the one difference. A custom `QueryClient` is supplied once, as `useQueries`'
|
||||
* own top-level second argument, rather than per query.
|
||||
*
|
||||
* Having the same query key more than once in the array of query objects may cause some data to be shared
|
||||
* between queries. To avoid this, consider de-duplicating the queries and map the results back to the desired
|
||||
* structure.
|
||||
*
|
||||
* The `combine` option can be used to combine the results of the queries into a single value. The result will
|
||||
* be structurally shared to be as referentially stable as possible.
|
||||
*
|
||||
* `placeholderData` is supported here too, but unlike `useQuery`, it doesn't receive information from
|
||||
* previously rendered queries, because the number of queries can differ between renders.
|
||||
* @param queriesOptions - An accessor returning the `queries` array to run, and an optional `combine`
|
||||
* function.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns The combined result. Without `combine`, this is an array with all the query results, in the same
|
||||
* order as the input. When `combine` is provided, this is the value returned by `combine` instead.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { For } from 'solid-js'
|
||||
* import { useQueries } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Posts(props: { ids: Array<number> }) {
|
||||
* const postQueries = useQueries(() => ({
|
||||
* queries: props.ids.map((id) => ({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* staleTime: Infinity,
|
||||
* })),
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* <For each={postQueries}>
|
||||
* {(postQuery) => {
|
||||
* if (postQuery.isPending) return <li>Loading...</li>
|
||||
* if (postQuery.isError) return <li>Error: {postQuery.error.message}</li>
|
||||
* return <li>{postQuery.data.title}</li>
|
||||
* }}
|
||||
* </For>
|
||||
* </ul>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Combining results into a single value:
|
||||
* ```tsx
|
||||
* import { For, Match, Switch } from 'solid-js'
|
||||
* import { useQueries } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Posts(props: { ids: Array<number> }) {
|
||||
* const combinedPostsQuery = useQueries(() => ({
|
||||
* queries: props.ids.map((id) => ({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })),
|
||||
* combine: (postQueries) => {
|
||||
* return {
|
||||
* data: postQueries.map((postQuery) => postQuery.data),
|
||||
* isPending: postQueries.some((postQuery) => postQuery.isPending),
|
||||
* isError: postQueries.some((postQuery) => postQuery.isError),
|
||||
* }
|
||||
* },
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch
|
||||
* fallback={
|
||||
* <ul>
|
||||
* <For each={combinedPostsQuery.data}>{(post) => <li>{post?.title}</li>}</For>
|
||||
* </ul>
|
||||
* }
|
||||
* >
|
||||
* <Match when={combinedPostsQuery.isPending}>Loading...</Match>
|
||||
* <Match when={combinedPostsQuery.isError}>Error loading posts</Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useQueries<
|
||||
T extends Array<any>,
|
||||
TCombinedResult extends QueriesResults<T> = QueriesResults<T>,
|
||||
>(
|
||||
queriesOptions: Accessor<{
|
||||
queries:
|
||||
| readonly [...QueriesOptions<T>]
|
||||
| readonly [...{ [K in keyof T]: GetOptions<T[K]> }]
|
||||
combine?: (result: QueriesResults<T>) => TCombinedResult
|
||||
}>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): TCombinedResult {
|
||||
const resolveClient = useQueryClientResolver(queryClient)
|
||||
const client = createMemo(() => resolveClient())
|
||||
const isRestoring = useIsRestoring()
|
||||
|
||||
const defaultedQueries = createMemo(() =>
|
||||
queriesOptions().queries.map((options) =>
|
||||
mergeProps(
|
||||
client().defaultQueryOptions(options as QueryObserverOptions),
|
||||
{
|
||||
get _optimisticResults() {
|
||||
return isRestoring() ? 'isRestoring' : 'optimistic'
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const observer = new QueriesObserver(
|
||||
client(),
|
||||
defaultedQueries(),
|
||||
queriesOptions().combine
|
||||
? ({
|
||||
combine: queriesOptions().combine,
|
||||
} as QueriesObserverOptions<TCombinedResult>)
|
||||
: undefined,
|
||||
)
|
||||
|
||||
const [state, setState] = createStore<TCombinedResult>(
|
||||
observer.getOptimisticResult(
|
||||
defaultedQueries(),
|
||||
(queriesOptions() as QueriesObserverOptions<TCombinedResult>).combine,
|
||||
)[1](),
|
||||
)
|
||||
|
||||
createRenderEffect(
|
||||
on(
|
||||
() => queriesOptions().queries.length,
|
||||
() =>
|
||||
setState(
|
||||
observer.getOptimisticResult(
|
||||
defaultedQueries(),
|
||||
(queriesOptions() as QueriesObserverOptions<TCombinedResult>)
|
||||
.combine,
|
||||
)[1](),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const dataResources = createMemo(
|
||||
on(
|
||||
() => state.length,
|
||||
() =>
|
||||
state.map((queryRes) => {
|
||||
const dataPromise = () =>
|
||||
new Promise((resolve) => {
|
||||
if (queryRes.isFetching && queryRes.isLoading) return
|
||||
resolve(unwrap(queryRes.data))
|
||||
})
|
||||
return createResource(dataPromise)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
batch(() => {
|
||||
const dataResources_ = dataResources()
|
||||
for (let index = 0; index < dataResources_.length; index++) {
|
||||
const dataResource = dataResources_[index]!
|
||||
dataResource[1].mutate(() => unwrap(state[index]!.data))
|
||||
dataResource[1].refetch()
|
||||
}
|
||||
})
|
||||
|
||||
let taskQueue: Array<() => void> = []
|
||||
const subscribeToObserver = () =>
|
||||
observer.subscribe((result) => {
|
||||
taskQueue.push(() => {
|
||||
batch(() => {
|
||||
const dataResources_ = dataResources()
|
||||
for (let index = 0; index < dataResources_.length; index++) {
|
||||
const dataResource = dataResources_[index]!
|
||||
const unwrappedResult = { ...unwrap(result[index]) }
|
||||
// @ts-expect-error typescript pedantry regarding the possible range of index
|
||||
setState(index, unwrap(unwrappedResult))
|
||||
dataResource[1].mutate(() => unwrap(state[index]!.data))
|
||||
dataResource[1].refetch()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
queueMicrotask(() => {
|
||||
const taskToRun = taskQueue.pop()
|
||||
if (taskToRun) taskToRun()
|
||||
taskQueue = []
|
||||
})
|
||||
})
|
||||
|
||||
let unsubscribe: () => void = noop
|
||||
createComputed<() => void>((cleanup) => {
|
||||
cleanup?.()
|
||||
unsubscribe = isRestoring() ? noop : subscribeToObserver()
|
||||
// cleanup needs to be scheduled after synchronous effects take place
|
||||
return () => queueMicrotask(unsubscribe)
|
||||
})
|
||||
onCleanup(unsubscribe)
|
||||
|
||||
onMount(() => {
|
||||
observer.setQueries(
|
||||
defaultedQueries(),
|
||||
queriesOptions().combine
|
||||
? ({
|
||||
combine: queriesOptions().combine,
|
||||
} as QueriesObserverOptions<TCombinedResult>)
|
||||
: undefined,
|
||||
)
|
||||
})
|
||||
|
||||
createComputed(() => {
|
||||
observer.setQueries(
|
||||
defaultedQueries(),
|
||||
queriesOptions().combine
|
||||
? ({
|
||||
combine: queriesOptions().combine,
|
||||
} as QueriesObserverOptions<TCombinedResult>)
|
||||
: undefined,
|
||||
)
|
||||
})
|
||||
|
||||
const handler = (index: number) => ({
|
||||
get(target: QueryObserverResult, prop: keyof QueryObserverResult): any {
|
||||
if (prop === 'data') {
|
||||
return dataResources()[index]![0]()
|
||||
}
|
||||
return Reflect.get(target, prop)
|
||||
},
|
||||
})
|
||||
|
||||
const getProxies = () =>
|
||||
state.map((s, index) => {
|
||||
return new Proxy(s, handler(index))
|
||||
})
|
||||
|
||||
const [proxyState, setProxyState] = createStore(getProxies())
|
||||
createRenderEffect(() => setProxyState(getProxies()))
|
||||
|
||||
return proxyState as TCombinedResult
|
||||
}
|
||||
249
packages/solid-query/src/useQuery.ts
Normal file
249
packages/solid-query/src/useQuery.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { QueryObserver } from '@tanstack/query-core'
|
||||
import { createMemo } from 'solid-js'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import type { DefaultError, QueryKey } from '@tanstack/query-core'
|
||||
import type { QueryClient } from './QueryClient'
|
||||
import type { Accessor } from 'solid-js'
|
||||
import type {
|
||||
DefinedUseQueryResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from './types'
|
||||
import type {
|
||||
DefinedInitialDataOptions,
|
||||
UndefinedInitialDataOptions,
|
||||
} from './queryOptions'
|
||||
|
||||
/**
|
||||
* @see {@link queryOptions} to share these options between `useQuery` and imperative APIs like `queryClient.query`.
|
||||
* @param options - An accessor returning the {@link UndefinedInitialDataOptions} to use — everything you can
|
||||
* pass to `useQuery`.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns The current query result, as a Solid store. `status` is `pending` if there is no cached data to
|
||||
* display, `error` if the last fetch attempt failed, or `success` if the query has data to display.
|
||||
* `isPending`/`isSuccess`/`isError` are derived booleans for convenience.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { For, Match, Switch } from 'solid-js'
|
||||
* import { useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Posts() {
|
||||
* const postsQuery = useQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch>
|
||||
* <Match when={postsQuery.isPending}>Loading...</Match>
|
||||
* <Match when={postsQuery.isError}>Error: {postsQuery.error.message}</Match>
|
||||
* <Match when={postsQuery.isSuccess}>
|
||||
* <ul>
|
||||
* <For each={postsQuery.data}>{(post) => <li>{post.title}</li>}</For>
|
||||
* </ul>
|
||||
* <div>{postsQuery.isFetching ? 'Background Updating...' : ' '}</div>
|
||||
* </Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* `select` derives whatever `data` a component needs from the cached value, without changing what's
|
||||
* actually stored in the cache — the cache still holds the full `Post[]`, but `data` here is a `number`:
|
||||
* ```tsx
|
||||
* import { Match, Switch } from 'solid-js'
|
||||
* import { useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function PostCount() {
|
||||
* const postsQuery = useQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* select: (posts) => posts.length,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch>
|
||||
* <Match when={postsQuery.isPending}>Loading...</Match>
|
||||
* <Match when={postsQuery.isError}>Error: {postsQuery.error.message}</Match>
|
||||
* <Match when={postsQuery.isSuccess}>{postsQuery.data} posts</Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A dependent query, only enabled once `postId` is set:
|
||||
* ```tsx
|
||||
* import { Match, Switch } from 'solid-js'
|
||||
* import { useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Post(props: { postId: number | undefined }) {
|
||||
* const postQuery = useQuery(() => ({
|
||||
* queryKey: ['post', props.postId],
|
||||
* queryFn: () => fetchPost(props.postId!),
|
||||
* enabled: props.postId != null,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch fallback={<h1>{postQuery.data?.title}</h1>}>
|
||||
* <Match when={props.postId == null}>Select a post</Match>
|
||||
* <Match when={postQuery.isLoading}>Loading...</Match>
|
||||
* <Match when={postQuery.isError}>Error: {postQuery.error.message}</Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* The same dependent query, using `skipToken` to disable it in a type-safe way instead of relying on
|
||||
* `enabled`. The non-null assertion is still needed — Solid's `props` narrowing doesn't survive into the
|
||||
* `queryFn` closure the way a local `const` would — but `skipToken` keeps `queryFn`'s return type accurate
|
||||
* without it. `refetch` doesn't work while `queryFn` is `skipToken` — use `enabled: false` instead if you
|
||||
* need to trigger the query manually:
|
||||
* ```tsx
|
||||
* import { Match, Switch } from 'solid-js'
|
||||
* import { skipToken, useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Post(props: { postId: number | undefined }) {
|
||||
* const postQuery = useQuery(() => ({
|
||||
* queryKey: ['post', props.postId],
|
||||
* queryFn: props.postId != null ? () => fetchPost(props.postId!) : skipToken,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <Switch fallback={<h1>{postQuery.data?.title}</h1>}>
|
||||
* <Match when={props.postId == null}>Select a post</Match>
|
||||
* <Match when={postQuery.isLoading}>Loading...</Match>
|
||||
* <Match when={postQuery.isError}>Error: {postQuery.error.message}</Match>
|
||||
* </Switch>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Seeding a detail query from an already-cached list, to skip the loading state:
|
||||
* ```tsx
|
||||
* import { useQuery, useQueryClient } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Post(props: { postId: number }) {
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const postQuery = useQuery(() => ({
|
||||
* queryKey: ['post', props.postId],
|
||||
* queryFn: () => fetchPost(props.postId),
|
||||
* initialData: () =>
|
||||
* queryClient
|
||||
* .getQueryData<Array<Post>>(['posts'])
|
||||
* ?.find((post) => post.id === props.postId),
|
||||
* }))
|
||||
*
|
||||
* return postQuery.isError ? <span>Error: {postQuery.error.message}</span> : <h1>{postQuery.data?.title}</h1>
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Paginated data, keeping the previous page's data visible while the next page loads:
|
||||
* ```tsx
|
||||
* import { For, createSignal } from 'solid-js'
|
||||
* import { keepPreviousData, useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Posts() {
|
||||
* const [page, setPage] = createSignal(0)
|
||||
*
|
||||
* const postsQuery = useQuery(() => ({
|
||||
* queryKey: ['posts', page()],
|
||||
* queryFn: () => fetchPosts(page()),
|
||||
* placeholderData: keepPreviousData,
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <ul>
|
||||
* <For each={postsQuery.data}>{(post) => <li>{post.title}</li>}</For>
|
||||
* </ul>
|
||||
* <button
|
||||
* disabled={postsQuery.isPlaceholderData}
|
||||
* onClick={() => setPage((old) => old + 1)}
|
||||
* >
|
||||
* Next Page
|
||||
* </button>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: () => QueryClient,
|
||||
): UseQueryResult<TData, TError>
|
||||
|
||||
/**
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* @see {@link queryOptions} to share these options between `useQuery` and imperative APIs like `queryClient.query`.
|
||||
* @param options - An accessor returning the {@link DefinedInitialDataOptions} to use — everything you can
|
||||
* pass to `useQuery`, with `initialData` set.
|
||||
* @param queryClient - An accessor for a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns The current query result, as a Solid store, typed so that `status` is `success` — or `error` if a
|
||||
* fetch attempt fails while keeping the existing data (`status` never resolves to `pending` in this overload's
|
||||
* type, since `initialData` guarantees data upfront). `isSuccess`/`isError` are derived booleans for
|
||||
* convenience.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { For } from 'solid-js'
|
||||
* import { useQuery } from '@tanstack/solid-query'
|
||||
*
|
||||
* function Posts() {
|
||||
* // `postsQuery.data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the
|
||||
* // list stays visible alongside the error.
|
||||
* const postsQuery = useQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* initialData: [],
|
||||
* }))
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {postsQuery.isError ? <span>Error: {postsQuery.error.message}</span> : null}
|
||||
* <ul>
|
||||
* <For each={postsQuery.data}>{(post) => <li>{post.title}</li>}</For>
|
||||
* </ul>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: () => QueryClient,
|
||||
): DefinedUseQueryResult<TData, TError>
|
||||
export function useQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
) {
|
||||
return useBaseQuery(
|
||||
createMemo(() => options()),
|
||||
QueryObserver,
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
6
packages/solid-query/test-setup.ts
Normal file
6
packages/solid-query/test-setup.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { cleanup } from '@solidjs/testing-library'
|
||||
import { afterEach } from 'vitest'
|
||||
|
||||
// https://github.com/solidjs/solid-testing-library
|
||||
afterEach(() => cleanup())
|
||||
16
packages/solid-query/tsconfig.json
Normal file
16
packages/solid-query/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist-ts",
|
||||
"rootDir": ".",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"test-setup.ts",
|
||||
"*.config.ts",
|
||||
"*.config.js",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
11
packages/solid-query/tsconfig.legacy.json
Normal file
11
packages/solid-query/tsconfig.legacy.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"outDir": "./dist-ts/legacy"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__tests__"],
|
||||
"references": [{ "path": "../query-core" }]
|
||||
}
|
||||
10
packages/solid-query/tsconfig.prod.json
Normal file
10
packages/solid-query/tsconfig.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"composite": false,
|
||||
"rootDir": "../../"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__tests__"]
|
||||
}
|
||||
42
packages/solid-query/tsdown.config.ts
Normal file
42
packages/solid-query/tsdown.config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
import solid from 'unplugin-solid/rolldown'
|
||||
|
||||
const baseConfig = {
|
||||
target: 'esnext',
|
||||
platform: 'browser' as const,
|
||||
format: ['esm', 'cjs'] as ['esm', 'cjs'],
|
||||
outDir: 'build',
|
||||
fixedExtension: false,
|
||||
}
|
||||
|
||||
const environment = (development: boolean) => ({
|
||||
NODE_ENV: development ? 'development' : 'production',
|
||||
PROD: !development,
|
||||
DEV: development,
|
||||
SSR: false,
|
||||
})
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
...baseConfig,
|
||||
entry: { index: 'src/index.ts' },
|
||||
clean: true,
|
||||
dts: true,
|
||||
env: environment(false),
|
||||
minify: {
|
||||
compress: {
|
||||
dropConsole: true,
|
||||
dropDebugger: true,
|
||||
},
|
||||
},
|
||||
plugins: [solid()],
|
||||
},
|
||||
{
|
||||
...baseConfig,
|
||||
entry: { dev: 'src/index.ts' },
|
||||
clean: false,
|
||||
dts: false,
|
||||
env: environment(true),
|
||||
plugins: [solid()],
|
||||
},
|
||||
])
|
||||
34
packages/solid-query/vite.config.ts
Normal file
34
packages/solid-query/vite.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import solid from 'vite-plugin-solid'
|
||||
|
||||
import packageJson from './package.json'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
// fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
environments: {
|
||||
ssr: {
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
name: packageJson.name,
|
||||
dir: './src',
|
||||
watch: false,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['test-setup.ts'],
|
||||
coverage: {
|
||||
enabled: !!process.env.CI,
|
||||
provider: 'istanbul',
|
||||
include: ['src/**/*'],
|
||||
exclude: ['src/__tests__/**'],
|
||||
},
|
||||
typecheck: { enabled: true },
|
||||
restoreMocks: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user