Repository: TanStack/query Branch: main Commit: e79176a72b57 Files: 2057 Total size: 21.4 MB Directory structure: gitextract_d__inhuu/ ├── .changeset/ │ └── config.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── config.yml │ ├── pull_request_template.md │ ├── renovate.json │ └── workflows/ │ ├── autofix.yml │ ├── detect-agent.yml │ ├── labeler.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .nx/ │ └── workflows/ │ └── dynamic-changesets.yaml ├── .prettierignore ├── .size-limit.json ├── CONTRIBUTING.md ├── FUNDING.json ├── LICENSE ├── README.md ├── docs/ │ ├── community-resources.md │ ├── config.json │ ├── eslint/ │ │ ├── eslint-plugin-query.md │ │ ├── exhaustive-deps.md │ │ ├── infinite-query-property-order.md │ │ ├── mutation-property-order.md │ │ ├── no-rest-destructuring.md │ │ ├── no-unstable-deps.md │ │ ├── no-void-query-fn.md │ │ └── stable-query-client.md │ ├── framework/ │ │ ├── angular/ │ │ │ ├── angular-httpclient-and-other-data-fetching-clients.md │ │ │ ├── devtools.md │ │ │ ├── guides/ │ │ │ │ ├── background-fetching-indicators.md │ │ │ │ ├── caching.md │ │ │ │ ├── default-query-function.md │ │ │ │ ├── dependent-queries.md │ │ │ │ ├── disabling-queries.md │ │ │ │ ├── does-this-replace-client-state.md │ │ │ │ ├── filters.md │ │ │ │ ├── important-defaults.md │ │ │ │ ├── infinite-queries.md │ │ │ │ ├── initial-query-data.md │ │ │ │ ├── invalidations-from-mutations.md │ │ │ │ ├── mutation-options.md │ │ │ │ ├── mutations.md │ │ │ │ ├── network-mode.md │ │ │ │ ├── optimistic-updates.md │ │ │ │ ├── paginated-queries.md │ │ │ │ ├── parallel-queries.md │ │ │ │ ├── placeholder-query-data.md │ │ │ │ ├── queries.md │ │ │ │ ├── query-cancellation.md │ │ │ │ ├── query-functions.md │ │ │ │ ├── query-invalidation.md │ │ │ │ ├── query-keys.md │ │ │ │ ├── query-options.md │ │ │ │ ├── query-retries.md │ │ │ │ ├── scroll-restoration.md │ │ │ │ ├── testing.md │ │ │ │ └── window-focus-refetching.md │ │ │ ├── installation.md │ │ │ ├── overview.md │ │ │ ├── quick-start.md │ │ │ ├── reference/ │ │ │ │ ├── functions/ │ │ │ │ │ ├── infiniteQueryOptions.md │ │ │ │ │ ├── injectInfiniteQuery.md │ │ │ │ │ ├── injectIsFetching.md │ │ │ │ │ ├── injectIsMutating.md │ │ │ │ │ ├── injectIsRestoring.md │ │ │ │ │ ├── injectMutation.md │ │ │ │ │ ├── injectMutationState.md │ │ │ │ │ ├── injectQuery.md │ │ │ │ │ ├── injectQueryClient.md │ │ │ │ │ ├── mutationOptions.md │ │ │ │ │ ├── provideAngularQuery.md │ │ │ │ │ ├── provideIsRestoring.md │ │ │ │ │ ├── provideQueryClient.md │ │ │ │ │ ├── provideTanStackQuery.md │ │ │ │ │ ├── queryFeature.md │ │ │ │ │ └── queryOptions.md │ │ │ │ ├── index.md │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── BaseMutationNarrowing.md │ │ │ │ │ ├── BaseQueryNarrowing.md │ │ │ │ │ ├── CreateBaseQueryOptions.md │ │ │ │ │ ├── CreateInfiniteQueryOptions.md │ │ │ │ │ ├── CreateMutationOptions.md │ │ │ │ │ ├── CreateQueryOptions.md │ │ │ │ │ ├── InjectInfiniteQueryOptions.md │ │ │ │ │ ├── InjectIsFetchingOptions.md │ │ │ │ │ ├── InjectIsMutatingOptions.md │ │ │ │ │ ├── InjectMutationOptions.md │ │ │ │ │ ├── InjectMutationStateOptions.md │ │ │ │ │ ├── InjectQueryOptions.md │ │ │ │ │ └── QueryFeature.md │ │ │ │ └── type-aliases/ │ │ │ │ ├── CreateBaseMutationResult.md │ │ │ │ ├── CreateBaseQueryResult.md │ │ │ │ ├── CreateInfiniteQueryResult.md │ │ │ │ ├── CreateMutateAsyncFunction.md │ │ │ │ ├── CreateMutateFunction.md │ │ │ │ ├── CreateMutationResult.md │ │ │ │ ├── CreateQueryResult.md │ │ │ │ ├── DefinedCreateInfiniteQueryResult.md │ │ │ │ ├── DefinedCreateQueryResult.md │ │ │ │ ├── DefinedInitialDataInfiniteOptions.md │ │ │ │ ├── DefinedInitialDataOptions.md │ │ │ │ ├── DevtoolsFeature.md │ │ │ │ ├── PersistQueryClientFeature.md │ │ │ │ ├── QueriesOptions.md │ │ │ │ ├── QueriesResults.md │ │ │ │ ├── QueryFeatures.md │ │ │ │ ├── UndefinedInitialDataInfiniteOptions.md │ │ │ │ ├── UndefinedInitialDataOptions.md │ │ │ │ ├── UnusedSkipTokenInfiniteOptions.md │ │ │ │ └── UnusedSkipTokenOptions.md │ │ │ ├── typescript.md │ │ │ └── zoneless.md │ │ ├── preact/ │ │ │ ├── devtools.md │ │ │ ├── graphql.md │ │ │ ├── guides/ │ │ │ │ ├── background-fetching-indicators.md │ │ │ │ ├── caching.md │ │ │ │ ├── default-query-function.md │ │ │ │ ├── dependent-queries.md │ │ │ │ ├── disabling-queries.md │ │ │ │ ├── does-this-replace-client-state.md │ │ │ │ ├── filters.md │ │ │ │ ├── important-defaults.md │ │ │ │ ├── infinite-queries.md │ │ │ │ ├── initial-query-data.md │ │ │ │ ├── invalidations-from-mutations.md │ │ │ │ ├── mutations.md │ │ │ │ ├── network-mode.md │ │ │ │ ├── optimistic-updates.md │ │ │ │ ├── paginated-queries.md │ │ │ │ ├── parallel-queries.md │ │ │ │ ├── placeholder-query-data.md │ │ │ │ ├── prefetching.md │ │ │ │ ├── queries.md │ │ │ │ ├── query-cancellation.md │ │ │ │ ├── query-functions.md │ │ │ │ ├── query-invalidation.md │ │ │ │ ├── query-keys.md │ │ │ │ ├── query-options.md │ │ │ │ ├── query-retries.md │ │ │ │ ├── render-optimizations.md │ │ │ │ ├── request-waterfalls.md │ │ │ │ ├── scroll-restoration.md │ │ │ │ ├── updates-from-mutation-responses.md │ │ │ │ └── window-focus-refetching.md │ │ │ ├── installation.md │ │ │ ├── overview.md │ │ │ ├── plugins/ │ │ │ │ ├── broadcastQueryClient.md │ │ │ │ ├── createAsyncStoragePersister.md │ │ │ │ ├── createPersister.md │ │ │ │ ├── createSyncStoragePersister.md │ │ │ │ └── persistQueryClient.md │ │ │ ├── quick-start.md │ │ │ ├── reference/ │ │ │ │ ├── functions/ │ │ │ │ │ ├── HydrationBoundary.md │ │ │ │ │ ├── QueryClientProvider.md │ │ │ │ │ ├── QueryErrorResetBoundary.md │ │ │ │ │ ├── infiniteQueryOptions.md │ │ │ │ │ ├── mutationOptions.md │ │ │ │ │ ├── queryOptions.md │ │ │ │ │ ├── useInfiniteQuery.md │ │ │ │ │ ├── useIsFetching.md │ │ │ │ │ ├── useIsMutating.md │ │ │ │ │ ├── useIsRestoring.md │ │ │ │ │ ├── useMutation.md │ │ │ │ │ ├── useMutationState.md │ │ │ │ │ ├── usePrefetchInfiniteQuery.md │ │ │ │ │ ├── usePrefetchQuery.md │ │ │ │ │ ├── useQueries.md │ │ │ │ │ ├── useQuery.md │ │ │ │ │ ├── useQueryClient.md │ │ │ │ │ ├── useQueryErrorResetBoundary.md │ │ │ │ │ ├── useSuspenseInfiniteQuery.md │ │ │ │ │ ├── useSuspenseQueries.md │ │ │ │ │ └── useSuspenseQuery.md │ │ │ │ ├── index.md │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── HydrationBoundaryProps.md │ │ │ │ │ ├── QueryErrorResetBoundaryProps.md │ │ │ │ │ ├── UseBaseQueryOptions.md │ │ │ │ │ ├── UseInfiniteQueryOptions.md │ │ │ │ │ ├── UseMutationOptions.md │ │ │ │ │ ├── UsePrefetchQueryOptions.md │ │ │ │ │ ├── UseQueryOptions.md │ │ │ │ │ ├── UseSuspenseInfiniteQueryOptions.md │ │ │ │ │ └── UseSuspenseQueryOptions.md │ │ │ │ ├── type-aliases/ │ │ │ │ │ ├── AnyUseBaseQueryOptions.md │ │ │ │ │ ├── AnyUseInfiniteQueryOptions.md │ │ │ │ │ ├── AnyUseMutationOptions.md │ │ │ │ │ ├── AnyUseQueryOptions.md │ │ │ │ │ ├── AnyUseSuspenseInfiniteQueryOptions.md │ │ │ │ │ ├── AnyUseSuspenseQueryOptions.md │ │ │ │ │ ├── DefinedInitialDataInfiniteOptions.md │ │ │ │ │ ├── DefinedInitialDataOptions.md │ │ │ │ │ ├── DefinedUseInfiniteQueryResult.md │ │ │ │ │ ├── DefinedUseQueryResult.md │ │ │ │ │ ├── QueriesOptions.md │ │ │ │ │ ├── QueriesResults.md │ │ │ │ │ ├── QueryClientProviderProps.md │ │ │ │ │ ├── QueryErrorClearResetFunction.md │ │ │ │ │ ├── QueryErrorIsResetFunction.md │ │ │ │ │ ├── QueryErrorResetBoundaryFunction.md │ │ │ │ │ ├── QueryErrorResetFunction.md │ │ │ │ │ ├── SuspenseQueriesOptions.md │ │ │ │ │ ├── SuspenseQueriesResults.md │ │ │ │ │ ├── UndefinedInitialDataInfiniteOptions.md │ │ │ │ │ ├── UndefinedInitialDataOptions.md │ │ │ │ │ ├── UnusedSkipTokenInfiniteOptions.md │ │ │ │ │ ├── UnusedSkipTokenOptions.md │ │ │ │ │ ├── UseBaseMutationResult.md │ │ │ │ │ ├── UseBaseQueryResult.md │ │ │ │ │ ├── UseInfiniteQueryResult.md │ │ │ │ │ ├── UseMutateAsyncFunction.md │ │ │ │ │ ├── UseMutateFunction.md │ │ │ │ │ ├── UseMutationResult.md │ │ │ │ │ ├── UseQueryResult.md │ │ │ │ │ ├── UseSuspenseInfiniteQueryResult.md │ │ │ │ │ └── UseSuspenseQueryResult.md │ │ │ │ └── variables/ │ │ │ │ ├── IsRestoringProvider.md │ │ │ │ └── QueryClientContext.md │ │ │ └── typescript.md │ │ ├── react/ │ │ │ ├── comparison.md │ │ │ ├── devtools.md │ │ │ ├── graphql.md │ │ │ ├── guides/ │ │ │ │ ├── advanced-ssr.md │ │ │ │ ├── background-fetching-indicators.md │ │ │ │ ├── caching.md │ │ │ │ ├── default-query-function.md │ │ │ │ ├── dependent-queries.md │ │ │ │ ├── disabling-queries.md │ │ │ │ ├── does-this-replace-client-state.md │ │ │ │ ├── filters.md │ │ │ │ ├── important-defaults.md │ │ │ │ ├── infinite-queries.md │ │ │ │ ├── initial-query-data.md │ │ │ │ ├── invalidations-from-mutations.md │ │ │ │ ├── migrating-to-react-query-3.md │ │ │ │ ├── migrating-to-react-query-4.md │ │ │ │ ├── migrating-to-v5.md │ │ │ │ ├── mutations.md │ │ │ │ ├── network-mode.md │ │ │ │ ├── optimistic-updates.md │ │ │ │ ├── paginated-queries.md │ │ │ │ ├── parallel-queries.md │ │ │ │ ├── placeholder-query-data.md │ │ │ │ ├── prefetching.md │ │ │ │ ├── queries.md │ │ │ │ ├── query-cancellation.md │ │ │ │ ├── query-functions.md │ │ │ │ ├── query-invalidation.md │ │ │ │ ├── query-keys.md │ │ │ │ ├── query-options.md │ │ │ │ ├── query-retries.md │ │ │ │ ├── render-optimizations.md │ │ │ │ ├── request-waterfalls.md │ │ │ │ ├── scroll-restoration.md │ │ │ │ ├── ssr.md │ │ │ │ ├── suspense.md │ │ │ │ ├── testing.md │ │ │ │ ├── updates-from-mutation-responses.md │ │ │ │ └── window-focus-refetching.md │ │ │ ├── installation.md │ │ │ ├── overview.md │ │ │ ├── plugins/ │ │ │ │ ├── broadcastQueryClient.md │ │ │ │ ├── createAsyncStoragePersister.md │ │ │ │ ├── createPersister.md │ │ │ │ ├── createSyncStoragePersister.md │ │ │ │ └── persistQueryClient.md │ │ │ ├── quick-start.md │ │ │ ├── react-native.md │ │ │ ├── reference/ │ │ │ │ ├── QueryClientProvider.md │ │ │ │ ├── QueryErrorResetBoundary.md │ │ │ │ ├── hydration.md │ │ │ │ ├── infiniteQueryOptions.md │ │ │ │ ├── mutationOptions.md │ │ │ │ ├── queryOptions.md │ │ │ │ ├── useInfiniteQuery.md │ │ │ │ ├── useIsFetching.md │ │ │ │ ├── useIsMutating.md │ │ │ │ ├── useMutation.md │ │ │ │ ├── useMutationState.md │ │ │ │ ├── usePrefetchInfiniteQuery.md │ │ │ │ ├── usePrefetchQuery.md │ │ │ │ ├── useQueries.md │ │ │ │ ├── useQuery.md │ │ │ │ ├── useQueryClient.md │ │ │ │ ├── useQueryErrorResetBoundary.md │ │ │ │ ├── useSuspenseInfiniteQuery.md │ │ │ │ ├── useSuspenseQueries.md │ │ │ │ └── useSuspenseQuery.md │ │ │ └── typescript.md │ │ ├── solid/ │ │ │ ├── devtools.md │ │ │ ├── guides/ │ │ │ │ ├── advanced-ssr.md │ │ │ │ ├── background-fetching-indicators.md │ │ │ │ ├── caching.md │ │ │ │ ├── default-query-function.md │ │ │ │ ├── dependent-queries.md │ │ │ │ ├── disabling-queries.md │ │ │ │ ├── does-this-replace-client-state.md │ │ │ │ ├── filters.md │ │ │ │ ├── important-defaults.md │ │ │ │ ├── infinite-queries.md │ │ │ │ ├── initial-query-data.md │ │ │ │ ├── invalidations-from-mutations.md │ │ │ │ ├── mutations.md │ │ │ │ ├── network-mode.md │ │ │ │ ├── optimistic-updates.md │ │ │ │ ├── paginated-queries.md │ │ │ │ ├── parallel-queries.md │ │ │ │ ├── placeholder-query-data.md │ │ │ │ ├── prefetching.md │ │ │ │ ├── queries.md │ │ │ │ ├── query-cancellation.md │ │ │ │ ├── query-functions.md │ │ │ │ ├── query-invalidation.md │ │ │ │ ├── query-keys.md │ │ │ │ ├── query-options.md │ │ │ │ ├── query-retries.md │ │ │ │ ├── request-waterfalls.md │ │ │ │ ├── scroll-restoration.md │ │ │ │ ├── ssr.md │ │ │ │ ├── suspense.md │ │ │ │ ├── testing.md │ │ │ │ ├── updates-from-mutation-responses.md │ │ │ │ └── window-focus-refetching.md │ │ │ ├── installation.md │ │ │ ├── overview.md │ │ │ ├── plugins/ │ │ │ │ ├── broadcastQueryClient.md │ │ │ │ └── createPersister.md │ │ │ ├── quick-start.md │ │ │ ├── reference/ │ │ │ │ ├── hydration.md │ │ │ │ ├── infiniteQueryOptions.md │ │ │ │ ├── mutationOptions.md │ │ │ │ ├── queryOptions.md │ │ │ │ ├── useInfiniteQuery.md │ │ │ │ ├── useIsFetching.md │ │ │ │ ├── useIsMutating.md │ │ │ │ ├── useMutation.md │ │ │ │ ├── useMutationState.md │ │ │ │ ├── useQueries.md │ │ │ │ └── useQuery.md │ │ │ └── typescript.md │ │ ├── svelte/ │ │ │ ├── devtools.md │ │ │ ├── installation.md │ │ │ ├── migrate-from-v5-to-v6.md │ │ │ ├── overview.md │ │ │ ├── reference/ │ │ │ │ ├── functions/ │ │ │ │ │ ├── createInfiniteQuery.md │ │ │ │ │ ├── createMutation.md │ │ │ │ │ ├── createQueries.md │ │ │ │ │ ├── createQuery.md │ │ │ │ │ ├── getIsRestoringContext.md │ │ │ │ │ ├── getQueryClientContext.md │ │ │ │ │ ├── infiniteQueryOptions.md │ │ │ │ │ ├── mutationOptions.md │ │ │ │ │ ├── queryOptions.md │ │ │ │ │ ├── setIsRestoringContext.md │ │ │ │ │ ├── setQueryClientContext.md │ │ │ │ │ ├── useHydrate.md │ │ │ │ │ ├── useIsFetching.md │ │ │ │ │ ├── useIsMutating.md │ │ │ │ │ ├── useIsRestoring.md │ │ │ │ │ ├── useMutationState.md │ │ │ │ │ └── useQueryClient.md │ │ │ │ ├── index.md │ │ │ │ ├── type-aliases/ │ │ │ │ │ ├── Accessor.md │ │ │ │ │ ├── CreateBaseMutationResult.md │ │ │ │ │ ├── CreateBaseQueryOptions.md │ │ │ │ │ ├── CreateBaseQueryResult.md │ │ │ │ │ ├── CreateInfiniteQueryOptions.md │ │ │ │ │ ├── CreateInfiniteQueryResult.md │ │ │ │ │ ├── CreateMutateAsyncFunction.md │ │ │ │ │ ├── CreateMutateFunction.md │ │ │ │ │ ├── CreateMutationOptions.md │ │ │ │ │ ├── CreateMutationResult.md │ │ │ │ │ ├── CreateQueryOptions.md │ │ │ │ │ ├── CreateQueryResult.md │ │ │ │ │ ├── DefinedCreateBaseQueryResult.md │ │ │ │ │ ├── DefinedCreateQueryResult.md │ │ │ │ │ ├── DefinedInitialDataOptions.md │ │ │ │ │ ├── HydrationBoundary.md │ │ │ │ │ ├── MutationStateOptions.md │ │ │ │ │ ├── QueriesOptions.md │ │ │ │ │ ├── QueriesResults.md │ │ │ │ │ ├── QueryClientProviderProps.md │ │ │ │ │ └── UndefinedInitialDataOptions.md │ │ │ │ └── variables/ │ │ │ │ └── HydrationBoundary.md │ │ │ └── ssr.md │ │ └── vue/ │ │ ├── devtools.md │ │ ├── graphql.md │ │ ├── guides/ │ │ │ ├── background-fetching-indicators.md │ │ │ ├── caching.md │ │ │ ├── custom-client.md │ │ │ ├── default-query-function.md │ │ │ ├── dependent-queries.md │ │ │ ├── disabling-queries.md │ │ │ ├── does-this-replace-client-state.md │ │ │ ├── filters.md │ │ │ ├── important-defaults.md │ │ │ ├── infinite-queries.md │ │ │ ├── initial-query-data.md │ │ │ ├── invalidations-from-mutations.md │ │ │ ├── migrating-to-v5.md │ │ │ ├── mutations.md │ │ │ ├── network-mode.md │ │ │ ├── optimistic-updates.md │ │ │ ├── paginated-queries.md │ │ │ ├── parallel-queries.md │ │ │ ├── placeholder-query-data.md │ │ │ ├── prefetching.md │ │ │ ├── queries.md │ │ │ ├── query-cancellation.md │ │ │ ├── query-functions.md │ │ │ ├── query-invalidation.md │ │ │ ├── query-keys.md │ │ │ ├── query-options.md │ │ │ ├── query-retries.md │ │ │ ├── scroll-restoration.md │ │ │ ├── ssr.md │ │ │ ├── suspense.md │ │ │ ├── testing.md │ │ │ ├── updates-from-mutation-responses.md │ │ │ └── window-focus-refetching.md │ │ ├── installation.md │ │ ├── overview.md │ │ ├── plugins/ │ │ │ ├── broadcastQueryClient.md │ │ │ └── createPersister.md │ │ ├── quick-start.md │ │ ├── reactivity.md │ │ ├── reference/ │ │ │ ├── hydration.md │ │ │ ├── infiniteQueryOptions.md │ │ │ ├── queryOptions.md │ │ │ ├── useInfiniteQuery.md │ │ │ ├── useIsFetching.md │ │ │ ├── useIsMutating.md │ │ │ ├── useMutation.md │ │ │ ├── useMutationState.md │ │ │ ├── useQueries.md │ │ │ ├── useQuery.md │ │ │ └── useQueryClient.md │ │ └── typescript.md │ └── reference/ │ ├── InfiniteQueryObserver.md │ ├── MutationCache.md │ ├── QueriesObserver.md │ ├── QueryCache.md │ ├── QueryClient.md │ ├── QueryObserver.md │ ├── environmentManager.md │ ├── focusManager.md │ ├── notifyManager.md │ ├── onlineManager.md │ ├── streamedQuery.md │ └── timeoutManager.md ├── eslint.config.js ├── examples/ │ ├── angular/ │ │ ├── auto-refetching/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── auto-refetching.component.html │ │ │ │ │ │ └── auto-refetching.component.ts │ │ │ │ │ ├── interceptor/ │ │ │ │ │ │ └── mock-api.interceptor.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── tasks.service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── basic/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.html │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── post.component.html │ │ │ │ │ │ ├── post.component.ts │ │ │ │ │ │ ├── posts.component.html │ │ │ │ │ │ └── posts.component.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── posts-service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── basic-persister/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.html │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── post.component.html │ │ │ │ │ │ ├── post.component.ts │ │ │ │ │ │ ├── posts.component.html │ │ │ │ │ │ └── posts.component.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── posts-service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── devtools-panel/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ └── components/ │ │ │ │ │ ├── basic-devtools-panel-example.component.ts │ │ │ │ │ ├── example-query.component.ts │ │ │ │ │ └── lazy-load-devtools-panel-example.component.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── infinite-query-with-max-pages/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── projects-mock.interceptor.ts │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── example.component.html │ │ │ │ │ │ └── example.component.ts │ │ │ │ │ ├── directives/ │ │ │ │ │ │ └── project-style.directive.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── projects.service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── optimistic-updates/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── optimistic-updates.component.ts │ │ │ │ │ ├── interceptor/ │ │ │ │ │ │ └── mock-api.interceptor.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── tasks.service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── pagination/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── projects-mock.interceptor.ts │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── example.component.html │ │ │ │ │ │ └── example.component.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── projects.service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── query-options-from-a-service/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.html │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── post.component.html │ │ │ │ │ │ ├── post.component.ts │ │ │ │ │ │ ├── posts.component.html │ │ │ │ │ │ └── posts.component.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── queries-service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── router/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.html │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── post.component.html │ │ │ │ │ │ ├── post.component.ts │ │ │ │ │ │ ├── posts.component.html │ │ │ │ │ │ └── posts.component.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── posts-service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── rxjs/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── autocomplete-mock.interceptor.ts │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── example.component.html │ │ │ │ │ │ └── example.component.ts │ │ │ │ │ └── services/ │ │ │ │ │ └── autocomplete-service.ts │ │ │ │ ├── index.html │ │ │ │ └── main.ts │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ └── simple/ │ │ ├── .devcontainer/ │ │ │ └── devcontainer.json │ │ ├── .eslintrc.cjs │ │ ├── README.md │ │ ├── angular.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.config.ts │ │ │ │ └── components/ │ │ │ │ ├── simple-example.component.html │ │ │ │ └── simple-example.component.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ └── styles.css │ │ ├── tsconfig.app.json │ │ └── tsconfig.json │ ├── preact/ │ │ └── simple/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── react/ │ │ ├── algolia/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── eslint.config.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── SearchResults.tsx │ │ │ │ ├── algolia.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── styles.css │ │ │ │ └── useAlgolia.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── auto-refetching/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── api/ │ │ │ │ │ └── data.ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── basic/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── eslint.config.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── basic-graphql-request/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── eslint.config.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── chat/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── chat.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── message.tsx │ │ │ │ └── style.css │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── default-query-function/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── devtools-panel/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── eslint-legacy/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── infinite-query-with-max-pages/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── api/ │ │ │ │ │ └── projects.ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── load-more-infinite-scroll/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── about.tsx │ │ │ │ ├── api/ │ │ │ │ │ └── projects.ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── nextjs/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── InfoBox.tsx │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ ├── PostList.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── hooks/ │ │ │ │ │ └── usePosts.ts │ │ │ │ └── pages/ │ │ │ │ ├── _app.tsx │ │ │ │ ├── client-only.tsx │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── nextjs-app-prefetching/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── app/ │ │ │ │ ├── get-query-client.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── pokemon-info.tsx │ │ │ │ ├── pokemon.ts │ │ │ │ └── providers.tsx │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── nextjs-suspense-streaming/ │ │ │ ├── .gitignore │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── app/ │ │ │ │ ├── api/ │ │ │ │ │ └── wait/ │ │ │ │ │ └── route.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── providers.tsx │ │ │ └── tsconfig.json │ │ ├── offline/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── public/ │ │ │ │ └── mockServiceWorker.js │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── api.ts │ │ │ │ ├── index.tsx │ │ │ │ └── movies.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── optimistic-updates-cache/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── api/ │ │ │ │ │ └── data.ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── optimistic-updates-ui/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── api/ │ │ │ │ │ └── data.ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── pagination/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── api/ │ │ │ │ │ └── projects.ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── playground/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── prefetching/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── pages/ │ │ │ │ ├── [user]/ │ │ │ │ │ └── [repo].tsx │ │ │ │ ├── _app.tsx │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── react-native/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── App.tsx │ │ │ ├── README.md │ │ │ ├── app.json │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Divider.tsx │ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ │ ├── ListItem.tsx │ │ │ │ │ └── LoadingIndicator.tsx │ │ │ │ ├── data/ │ │ │ │ │ └── movies.json │ │ │ │ ├── hooks/ │ │ │ │ │ ├── useAppState.ts │ │ │ │ │ ├── useOnlineManager.ts │ │ │ │ │ ├── useRefreshByUser.ts │ │ │ │ │ └── useRefreshOnFocus.ts │ │ │ │ ├── lib/ │ │ │ │ │ └── api.ts │ │ │ │ ├── navigation/ │ │ │ │ │ ├── MoviesStack.tsx │ │ │ │ │ └── types.ts │ │ │ │ └── screens/ │ │ │ │ ├── MovieDetailsScreen.tsx │ │ │ │ └── MoviesListScreen.tsx │ │ │ └── tsconfig.json │ │ ├── react-router/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── contacts.ts │ │ │ │ ├── error-page.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── routes/ │ │ │ │ ├── contact.tsx │ │ │ │ ├── destroy.tsx │ │ │ │ ├── edit.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── new.tsx │ │ │ │ └── root.tsx │ │ │ └── tsconfig.json │ │ ├── rick-morty/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── Character.tsx │ │ │ │ ├── Characters.tsx │ │ │ │ ├── Episode.tsx │ │ │ │ ├── Episodes.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ ├── api.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── shadow-dom/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── eslint.config.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── DogList.tsx │ │ │ │ ├── index.css │ │ │ │ ├── main.tsx │ │ │ │ └── vite-env.d.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── simple/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── star-wars/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── Character.tsx │ │ │ │ ├── Characters.tsx │ │ │ │ ├── Film.tsx │ │ │ │ ├── Films.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ ├── api.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ └── suspense/ │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── Button.tsx │ │ │ │ ├── Project.tsx │ │ │ │ ├── Projects.tsx │ │ │ │ └── Spinner.tsx │ │ │ ├── index.tsx │ │ │ └── queries.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── solid/ │ │ ├── astro/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── astro.config.mjs │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Link.tsx │ │ │ │ │ └── SolidApp.tsx │ │ │ │ ├── env.d.ts │ │ │ │ ├── layouts/ │ │ │ │ │ └── MainLayout.astro │ │ │ │ ├── pages/ │ │ │ │ │ └── index.astro │ │ │ │ └── utils/ │ │ │ │ └── index.ts │ │ │ ├── tailwind.config.mjs │ │ │ └── tsconfig.json │ │ ├── basic/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── basic-graphql-request/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── default-query-function/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── offline/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── api.ts │ │ │ │ ├── index.tsx │ │ │ │ └── movies.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── simple/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ └── solid-start-streaming/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.config.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.tsx │ │ │ ├── components/ │ │ │ │ ├── example.tsx │ │ │ │ ├── post-viewer.tsx │ │ │ │ ├── query-boundary.tsx │ │ │ │ └── user-info.tsx │ │ │ ├── entry-client.tsx │ │ │ ├── entry-server.tsx │ │ │ ├── global.d.ts │ │ │ ├── routes/ │ │ │ │ ├── [...404].tsx │ │ │ │ ├── batch-methods.tsx │ │ │ │ ├── deferred.tsx │ │ │ │ ├── hydration.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── mixed.tsx │ │ │ │ ├── prefetch.tsx │ │ │ │ ├── streamed.tsx │ │ │ │ └── with-error.tsx │ │ │ └── utils/ │ │ │ └── api.ts │ │ └── tsconfig.json │ ├── svelte/ │ │ ├── auto-refetching/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.d.ts │ │ │ │ ├── app.html │ │ │ │ └── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── api/ │ │ │ │ └── data/ │ │ │ │ └── +server.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── basic/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.d.ts │ │ │ │ ├── app.html │ │ │ │ ├── lib/ │ │ │ │ │ ├── Post.svelte │ │ │ │ │ ├── Posts.svelte │ │ │ │ │ ├── data.ts │ │ │ │ │ └── types.ts │ │ │ │ └── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── [postId]/ │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── load-more-infinite-scroll/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.d.ts │ │ │ │ ├── app.html │ │ │ │ ├── lib/ │ │ │ │ │ └── LoadMore.svelte │ │ │ │ └── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ └── +page.svelte │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── optimistic-updates/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.d.ts │ │ │ │ ├── app.html │ │ │ │ └── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── api/ │ │ │ │ └── data/ │ │ │ │ └── +server.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── playground/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.d.ts │ │ │ │ ├── app.html │ │ │ │ ├── lib/ │ │ │ │ │ └── stores.svelte.ts │ │ │ │ └── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.svelte │ │ │ │ ├── AddTodo.svelte │ │ │ │ ├── App.svelte │ │ │ │ ├── EditTodo.svelte │ │ │ │ └── TodosList.svelte │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── simple/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.svelte │ │ │ │ ├── app.css │ │ │ │ ├── lib/ │ │ │ │ │ └── Simple.svelte │ │ │ │ ├── main.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.d.ts │ │ │ │ ├── app.html │ │ │ │ ├── lib/ │ │ │ │ │ ├── Post.svelte │ │ │ │ │ ├── Posts.svelte │ │ │ │ │ ├── api.ts │ │ │ │ │ └── types.ts │ │ │ │ └── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +layout.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── +page.ts │ │ │ │ └── [postId]/ │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ └── star-wars/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.d.ts │ │ │ ├── app.html │ │ │ ├── lib/ │ │ │ │ └── api.ts │ │ │ └── routes/ │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── characters/ │ │ │ │ ├── +page.svelte │ │ │ │ └── [characterId]/ │ │ │ │ ├── +page.svelte │ │ │ │ ├── Film.svelte │ │ │ │ └── Homeworld.svelte │ │ │ └── films/ │ │ │ ├── +page.svelte │ │ │ └── [filmId]/ │ │ │ ├── +page.svelte │ │ │ └── Character.svelte │ │ ├── svelte.config.js │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── vue/ │ ├── 2.6-basic/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── Post.vue │ │ │ ├── Posts.vue │ │ │ ├── main.ts │ │ │ ├── shims-vue.d.ts │ │ │ └── types.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── 2.7-basic/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── Post.vue │ │ │ ├── Posts.vue │ │ │ ├── main.ts │ │ │ ├── shims-vue.d.ts │ │ │ └── types.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── basic/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── Post.vue │ │ │ ├── Posts.vue │ │ │ ├── main.ts │ │ │ ├── shims-vue.d.ts │ │ │ └── types.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── dependent-queries/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── Post.vue │ │ │ ├── Posts.vue │ │ │ ├── main.ts │ │ │ ├── shims-vue.d.ts │ │ │ └── types.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── nuxt3/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── plugins/ │ │ │ └── vue-query.ts │ │ └── tsconfig.json │ ├── persister/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── Post.vue │ │ │ ├── Posts.vue │ │ │ ├── main.ts │ │ │ ├── shims-vue.d.ts │ │ │ └── types.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── simple/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── main.ts │ │ ├── shims-vue.d.ts │ │ └── types.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── integrations/ │ ├── angular-cli-20/ │ │ ├── .gitignore │ │ ├── .vscode/ │ │ │ ├── extensions.json │ │ │ ├── launch.json │ │ │ └── tasks.json │ │ ├── README.md │ │ ├── angular.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app.config.ts │ │ │ │ └── app.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ └── styles.css │ │ ├── tsconfig.app.json │ │ └── tsconfig.json │ ├── react-next-14/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── client-component.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── providers.tsx │ │ ├── next.config.js │ │ ├── package.json │ │ └── tsconfig.json │ ├── react-next-15/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── _action.ts │ │ │ ├── client-component.tsx │ │ │ ├── count/ │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ ├── make-query-client.ts │ │ │ ├── page.tsx │ │ │ └── providers.tsx │ │ ├── next.config.js │ │ ├── package.json │ │ └── tsconfig.json │ ├── react-next-16/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── _action.ts │ │ │ ├── client-component.tsx │ │ │ ├── count/ │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ ├── make-query-client.ts │ │ │ ├── page.tsx │ │ │ └── providers.tsx │ │ ├── next.config.js │ │ ├── package.json │ │ └── tsconfig.json │ ├── react-vite/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ └── main.jsx │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── react-webpack-4/ │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.js │ │ │ └── index.js │ │ └── webpack.config.js │ ├── react-webpack-5/ │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.js │ │ │ └── index.js │ │ └── webpack.config.js │ ├── solid-vite/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── svelte-vite/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.svelte │ │ │ ├── Simple.svelte │ │ │ ├── main.js │ │ │ └── vite-env.d.ts │ │ ├── svelte.config.js │ │ └── vite.config.js │ └── vue-vite/ │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── knip.json ├── labeler-config.yml ├── media/ │ └── logo.sketch ├── nx.json ├── package.json ├── packages/ │ ├── angular-query-experimental/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── prepack.js │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── infinite-query-options.test-d.ts │ │ │ │ ├── infinite-query-options.test.ts │ │ │ │ ├── inject-devtools-panel.test.ts │ │ │ │ ├── inject-infinite-query.test-d.ts │ │ │ │ ├── inject-infinite-query.test.ts │ │ │ │ ├── inject-is-fetching.test.ts │ │ │ │ ├── inject-is-mutating.test.ts │ │ │ │ ├── inject-is-restoring.test.ts │ │ │ │ ├── inject-mutation-state.test-d.ts │ │ │ │ ├── inject-mutation-state.test.ts │ │ │ │ ├── inject-mutation.test-d.ts │ │ │ │ ├── inject-mutation.test.ts │ │ │ │ ├── inject-queries.test-d.ts │ │ │ │ ├── inject-queries.test.ts │ │ │ │ ├── inject-query.test-d.ts │ │ │ │ ├── inject-query.test.ts │ │ │ │ ├── mutation-options.test-d.ts │ │ │ │ ├── mutation-options.test.ts │ │ │ │ ├── pending-tasks.test.ts │ │ │ │ ├── provide-query-client.test.ts │ │ │ │ ├── provide-tanstack-query.test.ts │ │ │ │ ├── query-options.test-d.ts │ │ │ │ ├── query-options.test.ts │ │ │ │ ├── signal-proxy.test.ts │ │ │ │ ├── test-utils.ts │ │ │ │ └── with-devtools.test.ts │ │ │ ├── create-base-query.ts │ │ │ ├── devtools/ │ │ │ │ ├── index.ts │ │ │ │ ├── production/ │ │ │ │ │ └── index.ts │ │ │ │ ├── stub.ts │ │ │ │ ├── types.ts │ │ │ │ └── with-devtools.ts │ │ │ ├── devtools-panel/ │ │ │ │ ├── index.ts │ │ │ │ ├── inject-devtools-panel.ts │ │ │ │ ├── production/ │ │ │ │ │ └── index.ts │ │ │ │ ├── stub.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── infinite-query-options.ts │ │ │ ├── inject-infinite-query.ts │ │ │ ├── inject-is-fetching.ts │ │ │ ├── inject-is-mutating.ts │ │ │ ├── inject-is-restoring.ts │ │ │ ├── inject-mutation-state.ts │ │ │ ├── inject-mutation.ts │ │ │ ├── inject-queries-experimental/ │ │ │ │ └── index.ts │ │ │ ├── inject-queries.ts │ │ │ ├── inject-query-client.ts │ │ │ ├── inject-query.ts │ │ │ ├── mutation-options.ts │ │ │ ├── pending-tasks-compat.ts │ │ │ ├── providers.ts │ │ │ ├── query-options.ts │ │ │ ├── signal-proxy.ts │ │ │ └── types.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ └── vite.config.ts │ ├── angular-query-persist-client/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── with-persist-query-client.test.ts │ │ │ ├── index.ts │ │ │ └── with-persist-query-client.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── eslint-plugin-query/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── root.tsup.config.js │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── exhaustive-deps.test.ts │ │ │ │ ├── infinite-query-property-order.rule.test.ts │ │ │ │ ├── mutation-property-order.rule.test.ts │ │ │ │ ├── no-rest-destructuring.test.ts │ │ │ │ ├── no-unstable-deps.test.ts │ │ │ │ ├── no-void-query-fn.test.ts │ │ │ │ ├── sort-data-by-order.utils.test.ts │ │ │ │ ├── stable-query-client.test.ts │ │ │ │ ├── test-utils.test.ts │ │ │ │ ├── test-utils.ts │ │ │ │ └── ts-fixture/ │ │ │ │ ├── file.ts │ │ │ │ └── tsconfig.json │ │ │ ├── index.ts │ │ │ ├── rules/ │ │ │ │ ├── exhaustive-deps/ │ │ │ │ │ ├── exhaustive-deps.rule.ts │ │ │ │ │ └── exhaustive-deps.utils.ts │ │ │ │ ├── infinite-query-property-order/ │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── infinite-query-property-order.rule.ts │ │ │ │ ├── mutation-property-order/ │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── mutation-property-order.rule.ts │ │ │ │ ├── no-rest-destructuring/ │ │ │ │ │ ├── no-rest-destructuring.rule.ts │ │ │ │ │ └── no-rest-destructuring.utils.ts │ │ │ │ ├── no-unstable-deps/ │ │ │ │ │ └── no-unstable-deps.rule.ts │ │ │ │ ├── no-void-query-fn/ │ │ │ │ │ └── no-void-query-fn.rule.ts │ │ │ │ └── stable-query-client/ │ │ │ │ └── stable-query-client.rule.ts │ │ │ ├── rules.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── ast-utils.ts │ │ │ ├── create-property-order-rule.ts │ │ │ ├── detect-react-query-imports.ts │ │ │ ├── get-docs-url.ts │ │ │ ├── sort-data-by-order.ts │ │ │ └── unique-by.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── preact-query/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── HydrationBoundary.tsx │ │ │ ├── IsRestoringProvider.ts │ │ │ ├── QueryClientProvider.tsx │ │ │ ├── QueryErrorResetBoundary.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── ErrorBoundary/ │ │ │ │ │ ├── ErrorBoundary.ts │ │ │ │ │ ├── ErrorBoundaryContext.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── HydrationBoundary.test.tsx │ │ │ │ ├── QueryClientProvider.test.tsx │ │ │ │ ├── QueryResetErrorBoundary.test.tsx │ │ │ │ ├── fine-grained-persister.test.tsx │ │ │ │ ├── infiniteQueryOptions.test-d.tsx │ │ │ │ ├── infiniteQueryOptions.test.tsx │ │ │ │ ├── mutationOptions.test-d.tsx │ │ │ │ ├── mutationOptions.test.tsx │ │ │ │ ├── queryOptions.test-d.tsx │ │ │ │ ├── queryOptions.test.tsx │ │ │ │ ├── ssr-hydration.test.tsx │ │ │ │ ├── ssr.test.tsx │ │ │ │ ├── suspense.test.tsx │ │ │ │ ├── useInfiniteQuery.test-d.tsx │ │ │ │ ├── useInfiniteQuery.test.tsx │ │ │ │ ├── useIsFetching.test.tsx │ │ │ │ ├── useMutation.test.tsx │ │ │ │ ├── useMutationState.test-d.tsx │ │ │ │ ├── useMutationState.test.tsx │ │ │ │ ├── usePrefetchInfiniteQuery.test-d.tsx │ │ │ │ ├── usePrefetchInfiniteQuery.test.tsx │ │ │ │ ├── usePrefetchQuery.test-d.tsx │ │ │ │ ├── usePrefetchQuery.test.tsx │ │ │ │ ├── useQueries.test-d.tsx │ │ │ │ ├── useQueries.test.tsx │ │ │ │ ├── useQuery.test-d.tsx │ │ │ │ ├── useQuery.test.tsx │ │ │ │ ├── useSuspenseInfiniteQuery.test-d.tsx │ │ │ │ ├── useSuspenseInfiniteQuery.test.tsx │ │ │ │ ├── useSuspenseQueries.test-d.tsx │ │ │ │ ├── useSuspenseQueries.test.tsx │ │ │ │ ├── useSuspenseQuery.test-d.tsx │ │ │ │ ├── useSuspenseQuery.test.tsx │ │ │ │ └── utils.tsx │ │ │ ├── errorBoundaryUtils.ts │ │ │ ├── index.ts │ │ │ ├── infiniteQueryOptions.ts │ │ │ ├── mutationOptions.ts │ │ │ ├── queryOptions.ts │ │ │ ├── suspense.ts │ │ │ ├── types.ts │ │ │ ├── useBaseQuery.ts │ │ │ ├── useInfiniteQuery.ts │ │ │ ├── useIsFetching.ts │ │ │ ├── useMutation.ts │ │ │ ├── useMutationState.ts │ │ │ ├── usePrefetchInfiniteQuery.tsx │ │ │ ├── usePrefetchQuery.tsx │ │ │ ├── useQueries.ts │ │ │ ├── useQuery.ts │ │ │ ├── useSuspenseInfiniteQuery.ts │ │ │ ├── useSuspenseQueries.ts │ │ │ ├── useSuspenseQuery.ts │ │ │ └── utils.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── preact-query-devtools/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── root.eslint.config.js │ │ ├── root.tsup.config.js │ │ ├── src/ │ │ │ ├── PreactQueryDevtools.tsx │ │ │ ├── PreactQueryDevtoolsPanel.tsx │ │ │ ├── index.ts │ │ │ └── production.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── preact-query-persist-client/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PersistQueryClientProvider.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── PersistQueryClientProvider.test.tsx │ │ │ │ ├── testPersistProvider.tsx │ │ │ │ └── use-queries-with-persist.test.tsx │ │ │ └── index.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-async-storage-persister/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── asyncThrottle.test.ts │ │ │ ├── asyncThrottle.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-broadcast-client-experimental/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-codemods/ │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── utils/ │ │ │ │ ├── index.cjs │ │ │ │ └── transformers/ │ │ │ │ ├── query-cache-transformer.cjs │ │ │ │ ├── query-client-transformer.cjs │ │ │ │ └── use-query-like-transformer.cjs │ │ │ ├── v4/ │ │ │ │ ├── __testfixtures__/ │ │ │ │ │ ├── default-import.input.tsx │ │ │ │ │ ├── default-import.output.tsx │ │ │ │ │ ├── named-import.input.tsx │ │ │ │ │ ├── named-import.output.tsx │ │ │ │ │ ├── namespaced-import.input.tsx │ │ │ │ │ ├── namespaced-import.output.tsx │ │ │ │ │ ├── parameter-is-identifier.input.tsx │ │ │ │ │ ├── parameter-is-identifier.output.tsx │ │ │ │ │ ├── parameter-is-object-expression.input.tsx │ │ │ │ │ ├── parameter-is-object-expression.output.tsx │ │ │ │ │ ├── replace-import-specifier.input.tsx │ │ │ │ │ ├── replace-import-specifier.output.tsx │ │ │ │ │ ├── type-arguments.input.tsx │ │ │ │ │ └── type-arguments.output.tsx │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── key-transformation.test.cjs │ │ │ │ │ └── replace-import-specifier.test.cjs │ │ │ │ ├── key-transformation.cjs │ │ │ │ ├── replace-import-specifier.cjs │ │ │ │ └── utils/ │ │ │ │ └── replacers/ │ │ │ │ └── key-replacer.cjs │ │ │ └── v5/ │ │ │ ├── is-loading/ │ │ │ │ ├── __testfixtures__/ │ │ │ │ │ ├── default-import.input.tsx │ │ │ │ │ ├── default-import.output.tsx │ │ │ │ │ ├── named-import.input.tsx │ │ │ │ │ └── named-import.output.tsx │ │ │ │ ├── __tests__/ │ │ │ │ │ └── is-loading.test.cjs │ │ │ │ └── is-loading.cjs │ │ │ ├── keep-previous-data/ │ │ │ │ ├── README.md │ │ │ │ ├── __testfixtures__/ │ │ │ │ │ ├── default.input.tsx │ │ │ │ │ ├── default.output.tsx │ │ │ │ │ ├── named.input.tsx │ │ │ │ │ └── named.output.tsx │ │ │ │ ├── __tests__/ │ │ │ │ │ └── keep-previous-data.test.cjs │ │ │ │ ├── keep-previous-data.cjs │ │ │ │ └── utils/ │ │ │ │ └── already-has-placeholder-data-property.cjs │ │ │ ├── remove-overloads/ │ │ │ │ ├── __testfixtures__/ │ │ │ │ │ ├── bug-reports.input.tsx │ │ │ │ │ ├── bug-reports.output.tsx │ │ │ │ │ ├── default-import.input.tsx │ │ │ │ │ └── default-import.output.tsx │ │ │ │ ├── __tests__/ │ │ │ │ │ └── remove-overloads.test.cjs │ │ │ │ ├── remove-overloads.cjs │ │ │ │ ├── transformers/ │ │ │ │ │ ├── filter-aware-usage-transformer.cjs │ │ │ │ │ └── query-fn-aware-usage-transformer.cjs │ │ │ │ └── utils/ │ │ │ │ ├── index.cjs │ │ │ │ └── unknown-usage-error.cjs │ │ │ ├── rename-hydrate/ │ │ │ │ ├── __testfixtures__/ │ │ │ │ │ ├── default-import.input.tsx │ │ │ │ │ ├── default-import.output.tsx │ │ │ │ │ ├── named-import.input.tsx │ │ │ │ │ └── named-import.output.tsx │ │ │ │ ├── __tests__/ │ │ │ │ │ └── rename-hydrate.test.cjs │ │ │ │ └── rename-hydrate.cjs │ │ │ └── rename-properties/ │ │ │ ├── __testfixtures__/ │ │ │ │ ├── rename-cache-time.input.tsx │ │ │ │ ├── rename-cache-time.output.tsx │ │ │ │ ├── rename-use-error-boundary.input.tsx │ │ │ │ └── rename-use-error-boundary.output.tsx │ │ │ ├── __tests__/ │ │ │ │ └── rename-properties.test.cjs │ │ │ └── rename-properties.cjs │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── query-core/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── OmitKeyof.test-d.ts │ │ │ │ ├── environmentManager.test.tsx │ │ │ │ ├── focusManager.test.tsx │ │ │ │ ├── hydration.test.tsx │ │ │ │ ├── infiniteQueryBehavior.test.tsx │ │ │ │ ├── infiniteQueryObserver.test-d.tsx │ │ │ │ ├── infiniteQueryObserver.test.tsx │ │ │ │ ├── mutation.test-d.tsx │ │ │ │ ├── mutationCache.test.tsx │ │ │ │ ├── mutationObserver.test.tsx │ │ │ │ ├── mutations.test.tsx │ │ │ │ ├── notifyManager.test.tsx │ │ │ │ ├── onlineManager.test.tsx │ │ │ │ ├── queriesObserver.test.tsx │ │ │ │ ├── query.test.tsx │ │ │ │ ├── queryCache.test.tsx │ │ │ │ ├── queryClient.test-d.tsx │ │ │ │ ├── queryClient.test.tsx │ │ │ │ ├── queryObserver.test-d.tsx │ │ │ │ ├── queryObserver.test.tsx │ │ │ │ ├── streamedQuery.test.tsx │ │ │ │ ├── timeoutManager.test.tsx │ │ │ │ ├── utils.test-d.tsx │ │ │ │ ├── utils.test.tsx │ │ │ │ └── utils.ts │ │ │ ├── environmentManager.ts │ │ │ ├── focusManager.ts │ │ │ ├── hydration.ts │ │ │ ├── index.ts │ │ │ ├── infiniteQueryBehavior.ts │ │ │ ├── infiniteQueryObserver.ts │ │ │ ├── mutation.ts │ │ │ ├── mutationCache.ts │ │ │ ├── mutationObserver.ts │ │ │ ├── notifyManager.ts │ │ │ ├── onlineManager.ts │ │ │ ├── queriesObserver.ts │ │ │ ├── query.ts │ │ │ ├── queryCache.ts │ │ │ ├── queryClient.ts │ │ │ ├── queryObserver.ts │ │ │ ├── removable.ts │ │ │ ├── retryer.ts │ │ │ ├── streamedQuery.ts │ │ │ ├── subscribable.ts │ │ │ ├── thenable.ts │ │ │ ├── timeoutManager.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-devtools/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── Devtools.tsx │ │ │ ├── DevtoolsComponent.tsx │ │ │ ├── DevtoolsPanelComponent.tsx │ │ │ ├── Explorer.tsx │ │ │ ├── TanstackQueryDevtools.tsx │ │ │ ├── TanstackQueryDevtoolsPanel.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── devtools.test.tsx │ │ │ │ └── utils.test.ts │ │ │ ├── constants.ts │ │ │ ├── contexts/ │ │ │ │ ├── PiPContext.tsx │ │ │ │ ├── QueryDevtoolsContext.ts │ │ │ │ ├── ThemeContext.ts │ │ │ │ └── index.ts │ │ │ ├── icons/ │ │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ ├── theme.ts │ │ │ └── utils.tsx │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-persist-client-core/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── createPersister.test.ts │ │ │ │ ├── persist.test.ts │ │ │ │ └── utils.ts │ │ │ ├── createPersister.ts │ │ │ ├── index.ts │ │ │ ├── persist.ts │ │ │ └── retryStrategies.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-sync-storage-persister/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── storageIsFull.test.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── query-test-utils/ │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ ├── queryKey.test.ts │ │ │ │ └── sleep.test.ts │ │ │ ├── index.ts │ │ │ ├── mockVisibilityState.ts │ │ │ ├── queryKey.ts │ │ │ └── sleep.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── react-query/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── HydrationBoundary.tsx │ │ │ ├── IsRestoringProvider.ts │ │ │ ├── QueryClientProvider.tsx │ │ │ ├── QueryErrorResetBoundary.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── HydrationBoundary.test.tsx │ │ │ │ ├── QueryClientProvider.test.tsx │ │ │ │ ├── QueryResetErrorBoundary.test.tsx │ │ │ │ ├── fine-grained-persister.test.tsx │ │ │ │ ├── infiniteQueryOptions.test-d.tsx │ │ │ │ ├── infiniteQueryOptions.test.tsx │ │ │ │ ├── mutationOptions.test-d.tsx │ │ │ │ ├── mutationOptions.test.tsx │ │ │ │ ├── queryOptions.test-d.tsx │ │ │ │ ├── queryOptions.test.tsx │ │ │ │ ├── ssr-hydration.test.tsx │ │ │ │ ├── ssr.test.tsx │ │ │ │ ├── suspense.test.tsx │ │ │ │ ├── useInfiniteQuery.test-d.tsx │ │ │ │ ├── useInfiniteQuery.test.tsx │ │ │ │ ├── useIsFetching.test.tsx │ │ │ │ ├── useMutation.test.tsx │ │ │ │ ├── useMutationState.test-d.tsx │ │ │ │ ├── useMutationState.test.tsx │ │ │ │ ├── usePrefetchInfiniteQuery.test-d.tsx │ │ │ │ ├── usePrefetchInfiniteQuery.test.tsx │ │ │ │ ├── usePrefetchQuery.test-d.tsx │ │ │ │ ├── usePrefetchQuery.test.tsx │ │ │ │ ├── useQueries.test-d.tsx │ │ │ │ ├── useQueries.test.tsx │ │ │ │ ├── useQuery.promise.test.tsx │ │ │ │ ├── useQuery.test-d.tsx │ │ │ │ ├── useQuery.test.tsx │ │ │ │ ├── useSuspenseInfiniteQuery.test-d.tsx │ │ │ │ ├── useSuspenseInfiniteQuery.test.tsx │ │ │ │ ├── useSuspenseQueries.test-d.tsx │ │ │ │ ├── useSuspenseQueries.test.tsx │ │ │ │ ├── useSuspenseQuery.test-d.tsx │ │ │ │ ├── useSuspenseQuery.test.tsx │ │ │ │ └── utils.tsx │ │ │ ├── errorBoundaryUtils.ts │ │ │ ├── index.ts │ │ │ ├── infiniteQueryOptions.ts │ │ │ ├── mutationOptions.ts │ │ │ ├── queryOptions.ts │ │ │ ├── suspense.ts │ │ │ ├── types.ts │ │ │ ├── useBaseQuery.ts │ │ │ ├── useInfiniteQuery.ts │ │ │ ├── useIsFetching.ts │ │ │ ├── useMutation.ts │ │ │ ├── useMutationState.ts │ │ │ ├── usePrefetchInfiniteQuery.tsx │ │ │ ├── usePrefetchQuery.tsx │ │ │ ├── useQueries.ts │ │ │ ├── useQuery.ts │ │ │ ├── useSuspenseInfiniteQuery.ts │ │ │ ├── useSuspenseQueries.ts │ │ │ └── useSuspenseQuery.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── react-query-devtools/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── ReactQueryDevtools.tsx │ │ │ ├── ReactQueryDevtoolsPanel.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── devtools.test.tsx │ │ │ │ └── not-development.test.tsx │ │ │ ├── index.ts │ │ │ └── production.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── react-query-next-experimental/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── HydrationStreamProvider.tsx │ │ │ ├── ReactQueryStreamedHydration.tsx │ │ │ ├── htmlescape.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── react-query-persist-client/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PersistQueryClientProvider.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── PersistQueryClientProvider.test.tsx │ │ │ │ └── use-queries-with-persist.test.tsx │ │ │ └── index.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── solid-query/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── QueryClient.ts │ │ │ ├── QueryClientProvider.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── QueryClientProvider.test.tsx │ │ │ │ ├── createQueries.test-d.tsx │ │ │ │ ├── mutationOptions.test-d.tsx │ │ │ │ ├── mutationOptions.test.tsx │ │ │ │ ├── queryOptions.test-d.tsx │ │ │ │ ├── queryOptions.test.tsx │ │ │ │ ├── suspense.test.tsx │ │ │ │ ├── transition.test.tsx │ │ │ │ ├── useInfiniteQuery.test.tsx │ │ │ │ ├── useIsFetching.test.tsx │ │ │ │ ├── useIsMutating.test.tsx │ │ │ │ ├── useMutation.test.tsx │ │ │ │ ├── useMutationState.test-d.tsx │ │ │ │ ├── useMutationState.test.tsx │ │ │ │ ├── useQueries.test.tsx │ │ │ │ ├── useQuery.test-d.tsx │ │ │ │ ├── useQuery.test.tsx │ │ │ │ ├── useQueryOptions.test-d.tsx │ │ │ │ └── utils.tsx │ │ │ ├── index.ts │ │ │ ├── infiniteQueryOptions.ts │ │ │ ├── isRestoring.ts │ │ │ ├── mutationOptions.ts │ │ │ ├── queryOptions.ts │ │ │ ├── types.ts │ │ │ ├── useBaseQuery.ts │ │ │ ├── useInfiniteQuery.ts │ │ │ ├── useIsFetching.ts │ │ │ ├── useIsMutating.ts │ │ │ ├── useMutation.ts │ │ │ ├── useMutationState.ts │ │ │ ├── useQueries.ts │ │ │ └── useQuery.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── solid-query-devtools/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── devtools.test.tsx │ │ │ │ └── devtoolsPanel.test.tsx │ │ │ ├── clientOnly.tsx │ │ │ ├── devtools.tsx │ │ │ ├── devtoolsPanel.tsx │ │ │ └── index.tsx │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── solid-query-persist-client/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PersistQueryClientProvider.tsx │ │ │ ├── __tests__/ │ │ │ │ └── PersistQueryClientProvider.test.tsx │ │ │ └── index.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ ├── svelte-query/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── HydrationBoundary.svelte │ │ │ ├── QueryClientProvider.svelte │ │ │ ├── containers.svelte.ts │ │ │ ├── context.ts │ │ │ ├── createBaseQuery.svelte.ts │ │ │ ├── createInfiniteQuery.ts │ │ │ ├── createMutation.svelte.ts │ │ │ ├── createQueries.svelte.ts │ │ │ ├── createQuery.ts │ │ │ ├── index.ts │ │ │ ├── infiniteQueryOptions.ts │ │ │ ├── mutationOptions.ts │ │ │ ├── queryOptions.ts │ │ │ ├── types.ts │ │ │ ├── useHydrate.ts │ │ │ ├── useIsFetching.svelte.ts │ │ │ ├── useIsMutating.svelte.ts │ │ │ ├── useIsRestoring.ts │ │ │ ├── useMutationState.svelte.ts │ │ │ ├── useQueryClient.ts │ │ │ └── utils.svelte.ts │ │ ├── svelte.config.js │ │ ├── tests/ │ │ │ ├── HydrationBoundary/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ └── HydrationBoundary.svelte.test.ts │ │ │ ├── ProviderWrapper.svelte │ │ │ ├── QueryClientProvider/ │ │ │ │ ├── ChildComponent.svelte │ │ │ │ ├── ParentComponent.svelte │ │ │ │ └── QueryClientProvider.svelte.test.ts │ │ │ ├── containers.svelte.test.ts │ │ │ ├── context/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ └── context.svelte.test.ts │ │ │ ├── createInfiniteQuery/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ ├── ChangeClient.svelte │ │ │ │ ├── SelectExample.svelte │ │ │ │ └── createInfiniteQuery.svelte.test.ts │ │ │ ├── createMutation/ │ │ │ │ ├── FailureExample.svelte │ │ │ │ ├── OnSuccessExample.svelte │ │ │ │ ├── ResetExample.svelte │ │ │ │ └── createMutation.svelte.test.ts │ │ │ ├── createQueries.svelte.test.ts │ │ │ ├── createQueries.test-d.ts │ │ │ ├── createQuery.svelte.test.ts │ │ │ ├── createQuery.test-d.ts │ │ │ ├── infiniteQueryOptions.svelte.test.ts │ │ │ ├── infiniteQueryOptions.test-d.ts │ │ │ ├── mutationOptions/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ ├── MultiExample.svelte │ │ │ │ ├── mutationOptions.svelte.test.ts │ │ │ │ └── mutationOptions.test-d.ts │ │ │ ├── queryOptions.svelte.test.ts │ │ │ ├── queryOptions.test-d.ts │ │ │ ├── test-setup.ts │ │ │ ├── useIsFetching/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ ├── FetchStatus.svelte │ │ │ │ ├── Query.svelte │ │ │ │ └── useIsFetching.svelte.test.ts │ │ │ ├── useIsMutating/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ ├── MutatingStatus.svelte │ │ │ │ ├── Query.svelte │ │ │ │ └── useIsMutating.svelte.test.ts │ │ │ ├── useMutationState/ │ │ │ │ ├── BaseExample.svelte │ │ │ │ ├── SelectExample.svelte │ │ │ │ └── useMutationState.svelte.test.ts │ │ │ └── utils.svelte.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── svelte-query-devtools/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── Devtools.svelte │ │ │ └── index.ts │ │ ├── svelte.config.js │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── svelte-query-persist-client/ │ │ ├── .attw.json │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PersistQueryClientProvider.svelte │ │ │ ├── index.ts │ │ │ └── utils.svelte.ts │ │ ├── svelte.config.js │ │ ├── tests/ │ │ │ ├── AwaitOnSuccess/ │ │ │ │ ├── AwaitOnSuccess.svelte │ │ │ │ └── Provider.svelte │ │ │ ├── FreshData/ │ │ │ │ ├── FreshData.svelte │ │ │ │ └── Provider.svelte │ │ │ ├── InitialData/ │ │ │ │ ├── InitialData.svelte │ │ │ │ └── Provider.svelte │ │ │ ├── OnSuccess/ │ │ │ │ ├── OnSuccess.svelte │ │ │ │ └── Provider.svelte │ │ │ ├── PersistQueryClientProvider.svelte.test.ts │ │ │ ├── RemoveCache/ │ │ │ │ ├── Provider.svelte │ │ │ │ └── RemoveCache.svelte │ │ │ ├── RestoreCache/ │ │ │ │ ├── Provider.svelte │ │ │ │ └── RestoreCache.svelte │ │ │ ├── UseQueries/ │ │ │ │ ├── Provider.svelte │ │ │ │ └── UseQueries.svelte │ │ │ ├── test-setup.ts │ │ │ └── utils.svelte.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── vue-query/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __mocks__/ │ │ │ │ ├── useBaseQuery.ts │ │ │ │ └── useQueryClient.ts │ │ │ ├── __tests__/ │ │ │ │ ├── infiniteQueryOptions.test-d.ts │ │ │ │ ├── mutationCache.test.ts │ │ │ │ ├── queryCache.test.ts │ │ │ │ ├── queryClient.test-d.ts │ │ │ │ ├── queryClient.test.ts │ │ │ │ ├── queryOptions.test-d.ts │ │ │ │ ├── queryOptions.test.ts │ │ │ │ ├── useInfiniteQuery.test-d.tsx │ │ │ │ ├── useInfiniteQuery.test.ts │ │ │ │ ├── useIsFetching.test.ts │ │ │ │ ├── useIsMutating.test.ts │ │ │ │ ├── useMutation.test-d.tsx │ │ │ │ ├── useMutation.test.ts │ │ │ │ ├── useMutationState.test.ts │ │ │ │ ├── useQueries.test-d.ts │ │ │ │ ├── useQueries.test.ts │ │ │ │ ├── useQuery.test-d.ts │ │ │ │ ├── useQuery.test.ts │ │ │ │ ├── useQueryClient.test.ts │ │ │ │ ├── utils.test.ts │ │ │ │ └── vueQueryPlugin.test.ts │ │ │ ├── devtools/ │ │ │ │ ├── devtools.ts │ │ │ │ └── utils.ts │ │ │ ├── index.ts │ │ │ ├── infiniteQueryOptions.ts │ │ │ ├── mutationCache.ts │ │ │ ├── queryCache.ts │ │ │ ├── queryClient.ts │ │ │ ├── queryOptions.ts │ │ │ ├── types.ts │ │ │ ├── useBaseQuery.ts │ │ │ ├── useInfiniteQuery.ts │ │ │ ├── useIsFetching.ts │ │ │ ├── useMutation.ts │ │ │ ├── useMutationState.ts │ │ │ ├── useQueries.ts │ │ │ ├── useQuery.ts │ │ │ ├── useQueryClient.ts │ │ │ ├── utils.ts │ │ │ └── vueQueryPlugin.ts │ │ ├── test-setup.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.legacy.json │ │ ├── tsconfig.prod.json │ │ ├── tsup.config.ts │ │ └── vite.config.ts │ └── vue-query-devtools/ │ ├── .attw.json │ ├── CHANGELOG.md │ ├── eslint.config.js │ ├── package.json │ ├── src/ │ │ ├── devtools.vue │ │ ├── devtoolsPanel.vue │ │ ├── index.ts │ │ ├── production.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-workspace.yaml ├── prettier.config.js ├── scripts/ │ ├── create-github-release.mjs │ ├── generate-docs.ts │ ├── generate-labeler-config.ts │ ├── getTsupConfig.js │ ├── getViteAliases.js │ ├── tsconfig.json │ └── verify-links.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", "changelog": [ "@svitejs/changesets-changelog-github-compact", { "repo": "TanStack/query" } ], "commit": false, "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "fixed": [], "linked": [], "ignore": [], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } } ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ github: [tannerlinsley, tkdodo] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: '🐛 Bug report' description: Report a reproducible bug or regression body: - type: markdown attributes: value: | Thank you for reporting an issue :pray:. This issue tracker is for reporting reproducible bugs or regression's found in [TanStack Query](https://github.com/TanStack/query) If you have a question about how to achieve something and are struggling, please post a question inside of TanStack Query's [Discussions tab](https://github.com/TanStack/query/discussions) Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: - TanStack Query's [Discussions tab](https://github.com/TanStack/query/discussions) - TanStack Query's [Open Issues](https://github.com/TanStack/query/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) - TanStack Query's [Closed Issues](https://github.com/TanStack/query/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) The more information you fill in, the better the community can help you. - type: textarea id: description attributes: label: Describe the bug description: Provide a clear and concise description of the challenge you are running into. validations: required: true - type: input id: link attributes: label: Your minimal, reproducible example description: | Please add a link to a minimal reproduction. Note: - Your bug may get fixed much faster if we can run your code and it doesn't have dependencies other than React/Solid/Vue/Svelte. - To create a shareable code example for web, you can use CodeSandbox (https://codesandbox.io/s/new) or Stackblitz (https://stackblitz.com/). - Please make sure the example is complete and runnable - e.g. avoid localhost URLs. - To stub out real api requests - Promise.resolve and Promise.reject are good options for easy reproduction - Feel free to fork any of the official examples to reproduce your issue: https://tanstack.com/query/latest/docs/framework/react/examples/simple - For React Native, you can use: https://snack.expo.dev/ - For TypeScript related issues only, a TypeScript Playground link might be sufficient: https://www.typescriptlang.org/play - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. placeholder: | e.g. Code Sandbox, Stackblitz, Expo Snack or TypeScript playground validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Describe the steps we have to take to reproduce the behavior. placeholder: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: Provide a clear and concise description of what you expected to happen. placeholder: | As a user, I expected ___ behavior but i am seeing ___ validations: required: true - type: dropdown attributes: label: How often does this bug happen? description: | Following the repro steps above, how easily are you able to reproduce this bug? options: - Every time - Often - Sometimes - Only once - type: textarea id: screenshots_or_videos attributes: label: Screenshots or Videos description: | If applicable, add screenshots or a video to help explain your problem. For more information on the supported file image/file types and the file size limits, please refer to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files placeholder: | You can drag your video or image files inside of this editor ↓ - type: textarea id: platform attributes: label: Platform description: | Please let us know which Operting System, Browser and Browser version you were using when the issue occurred. placeholder: | - OS: [e.g. macOS, Windows, Linux, iOS, Android] - Browser: [e.g. Chrome, Safari, Firefox, React Native] - Version: [e.g. 91.1] validations: required: true - type: dropdown id: adapter attributes: label: Tanstack Query adapter description: | Please let us know which adapter of TanStack Query you were using when the issue occurred. options: - react-query - solid-query - svelte-query - vue-query - angular-query - vanilla - type: input id: rq-version attributes: label: TanStack Query version description: | Please let us know the exact version of TanStack Query you were using when the issue occurred. Please don't just put in "latest", as this is subject to change. placeholder: | e.g. v5.51.9 validations: required: true - type: input id: ts-version attributes: label: TypeScript version description: | If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred. placeholder: | e.g. v5.5.4 - type: textarea id: additional attributes: label: Additional context description: Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 🤔 Feature Requests & Questions url: https://github.com/TanStack/query/discussions about: Please ask and answer questions here. - name: 💬 Community Chat url: https://discord.gg/mQd7egN about: A dedicated discord server hosted by TanStack - name: 🦋 TanStack Bluesky url: https://bsky.app/profile/tanstack.com about: Stay up to date with new releases of our libraries ================================================ FILE: .github/pull_request_template.md ================================================ ## 🎯 Changes ## ✅ Checklist - [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/query/blob/main/CONTRIBUTING.md). - [ ] I have tested this code locally with `pnpm run test:pr`. ## 🚀 Release Impact - [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). - [ ] This change is docs/CI/dev-only (no release). ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "configMigration": true, "extends": [ "config:recommended", "group:allNonMajor", "schedule:weekly", ":approveMajorUpdates", ":automergeMinor", ":disablePeerDependencies", ":maintainLockFilesMonthly", ":semanticCommits", ":semanticCommitTypeAll(chore)" ], "ignorePresets": [":ignoreModulesAndTests"], "labels": ["dependencies"], "rangeStrategy": "bump", "postUpdateOptions": ["pnpmDedupe"], "ignoreDeps": [ "@types/node", "@types/react", "@types/react-dom", "node", "react", "react-dom", "tsup", "typescript", "typescript54", "typescript55", "typescript56", "typescript57", "typescript58", "typescript59", "typescript60", "vue", "vue-tsc", "vue2", "vue2.7", "webpack" ] } ================================================ FILE: .github/workflows/autofix.yml ================================================ name: autofix.ci # needed to securely identify the workflow on: pull_request: push: branches: [main, 'v[0-9]', '*-pre', '*-maint'] concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} cancel-in-progress: true permissions: contents: read jobs: autofix: name: autofix runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Fix formatting run: pnpm run format - name: Apply fixes uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 with: commit-message: 'ci: apply automated fixes' ================================================ FILE: .github/workflows/detect-agent.yml ================================================ name: Detect Agent on: pull_request_target: types: [opened] workflow_dispatch: {} permissions: issues: write pull-requests: write jobs: detect: if: github.event_name != 'workflow_dispatch' uses: bombshell-dev/automation/.github/workflows/detect-agent.yml@a1553cebd9318d416f6a8e9f38f363b6aaa19c72 backfill: if: github.event_name == 'workflow_dispatch' uses: bombshell-dev/automation/.github/workflows/detect-agent-backfill.yml@a1553cebd9318d416f6a8e9f38f363b6aaa19c72 ================================================ FILE: .github/workflows/labeler.yml ================================================ name: Labeler on: pull_request_target: permissions: contents: read pull-requests: write jobs: labeler: name: Labeler runs-on: ubuntu-latest steps: - name: Labeler uses: actions/labeler@v6.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: labeler-config.yml ================================================ FILE: .github/workflows/pr.yml ================================================ name: PR on: pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} cancel-in-progress: true env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} permissions: contents: read pull-requests: write issues: write jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Start Nx Agents run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml" - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Get base and head commits for `nx affected` uses: nrwl/nx-set-shas@v4.4.0 with: main-branch-name: main - name: Run Checks run: pnpm run test:pr - name: Stop Nx Agents if: ${{ always() }} run: npx nx-cloud stop-all-agents preview: name: Preview runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Build Packages run: pnpm run build:all - name: Publish Previews run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' - name: Determine commit SHA id: determine-sha run: | echo "COMMIT_SHA=${{ github.event.pull_request.head.sha || github.sha }}" >> $GITHUB_ENV - name: Size Limit uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d with: github_token: ${{ secrets.GITHUB_TOKEN }} skip_step: install build_script: build:all provenance: name: Provenance runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Check Provenance uses: danielroe/provenance-action@v0.1.1 with: fail-on-downgrade: true version-preview: name: Version Preview runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Changeset Preview uses: TanStack/config/.github/changeset-preview@main ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: [main, 'v[0-9]', '*-pre', '*-maint'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} permissions: contents: write id-token: write pull-requests: write jobs: release: name: Release if: "!contains(github.event.head_commit.message, 'ci: changeset release')" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Check for changesets id: changesets run: | CHANGESET_FILES=$(ls .changeset/*.md 2>/dev/null | grep -v README.md || true) if [ -z "$CHANGESET_FILES" ]; then echo "has_changesets=false" >> "$GITHUB_OUTPUT" else echo "has_changesets=true" >> "$GITHUB_OUTPUT" fi - name: Start Nx Agents if: steps.changesets.outputs.has_changesets == 'true' run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml" - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Run Tests if: steps.changesets.outputs.has_changesets == 'true' run: pnpm run test:ci - name: Stop Nx Agents if: ${{ always() && steps.changesets.outputs.has_changesets == 'true' }} run: npx nx-cloud stop-all-agents - name: Enter Pre-Release Mode if: "contains(github.ref_name, '-pre') && !hashFiles('.changeset/pre.json')" run: pnpm changeset pre enter pre - name: Version Packages run: pnpm run changeset:version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Commit and Push Version Changes id: commit run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add . if git commit -m "ci: changeset release"; then git push echo "committed=true" >> "$GITHUB_OUTPUT" fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Determine dist-tag if: steps.commit.outputs.committed == 'true' id: dist-tag run: | BRANCH="${GITHUB_REF_NAME}" if [[ "$BRANCH" == *-pre ]]; then echo "prerelease=true" >> "$GITHUB_OUTPUT" elif [[ "$BRANCH" == *-maint ]]; then echo "tag=maint" >> "$GITHUB_OUTPUT" elif [[ "$BRANCH" =~ ^v[0-9]+$ ]]; then echo "tag=$BRANCH" >> "$GITHUB_OUTPUT" else echo "latest=true" >> "$GITHUB_OUTPUT" fi - name: Publish Packages if: steps.commit.outputs.committed == 'true' run: pnpm run changeset:publish ${{ steps.dist-tag.outputs.tag && format('--tag {0}', steps.dist-tag.outputs.tag) }} - name: Create GitHub Release if: steps.commit.outputs.committed == 'true' run: node scripts/create-github-release.mjs ${{ steps.dist-tag.outputs.prerelease == 'true' && '--prerelease' }} ${{ steps.dist-tag.outputs.latest == 'true' && '--latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules package-lock.json yarn.lock # builds build coverage dist dist-ts # misc .DS_Store .env .env.local .env.development.local .env.test.local .env.production.local .next next-env.d.ts npm-debug.log* yarn-debug.log* yarn-error.log* .history size-plugin.json stats-hydration.json stats.json stats.html .vscode/settings.json .vscode/mcp.json .cursor/rules .github/instructions/nx.instructions.md *.log *.tsbuildinfo .angular .cache .idea .nx/cache .nx/workspace-data .pnpm-store .svelte-kit .tsup .vinxi temp vite.config.js.timestamp-* vite.config.ts.timestamp-* ================================================ FILE: .npmrc ================================================ provenance=true ================================================ FILE: .nvmrc ================================================ 24.8.0 ================================================ FILE: .nx/workflows/dynamic-changesets.yaml ================================================ distribute-on: small-changeset: 3 linux-medium-js medium-changeset: 4 linux-medium-js large-changeset: 5 linux-medium-js ================================================ FILE: .prettierignore ================================================ **/.next **/.nx/cache **/.svelte-kit **/build **/coverage **/dist **/query-codemods/**/__testfixtures__ .changeset/*.md pnpm-lock.yaml packages/**/tsup.config.bundled*.mjs **/tsconfig.vitest-temp.json docs/framework/*/reference ================================================ FILE: .size-limit.json ================================================ [ { "name": "react full", "path": "packages/react-query/build/modern/index.js", "limit": "13.00 kB", "ignore": ["react", "react-dom"] }, { "name": "react minimal", "path": "packages/react-query/build/modern/index.js", "limit": "9.99 kB", "import": "{ useQuery, QueryClient, QueryClientProvider }", "ignore": ["react", "react-dom"] } ] ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## Questions If you have questions about implementation details, help or support, then please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/query/discussions) **PLEASE NOTE:** If you choose to instead open an issue for your question, your issue will be immediately closed and redirected to the forum. ## Reporting Issues If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/query/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/query/discussions) ## Suggesting new features If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented. ## Development _TanStack/query uses **symlink-based** configuration files. For smooth development in a local environment, we recommend developing in an environment that supports symlinks(ex: Linux, macOS, Windows Subsystem for Linux / WSL)._ If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started: - Fork this repository. - Install dependencies ```bash pnpm install ``` - We use [pnpm](https://pnpm.io/) v10 for package management (run in case of pnpm-related issues). ```bash corepack enable && corepack prepare ``` - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc` ```bash nvm use ``` - Build all packages. ```bash pnpm build:all ``` - Run development server. ```bash pnpm run watch ``` - Implement your changes and tests to files in the `src/` directory and corresponding test files. - Document your changes in the appropriate doc page. - Git stage your required changes and commit (see below commit guidelines). - Submit PR for review. ### Editing the docs locally and previewing the changes The documentations for all the TanStack projects are hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/query` docs. > [!NOTE] > The website fetches the doc pages from GitHub in production, and searches for them at `../query/docs` in development. Your local clone of `TanStack/query` needs to be in the same directory as the local clone of `TanStack/tanstack.com`. You can follow these steps to set up the docs for local development: 1. Make a new directory called `tanstack`. ```sh mkdir tanstack ``` 2. Enter that directory and clone the [`TanStack/query`](https://github.com/TanStack/query) and [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com) repos. ```sh cd tanstack git clone git@github.com:TanStack/query.git # We probably don't need all the branches and commit history # from the `tanstack.com` repo, so let's just create a shallow # clone of the latest version of the `main` branch. # Read more about shallow clones here: # https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-shallow-clones git clone git@github.com:TanStack/tanstack.com.git --depth=1 --single-branch --branch=main ``` > [!NOTE] > Your `tanstack` directory should look like this: > > ``` > tanstack/ > | > +-- query/ (<-- this directory cannot be called anything else!) > | > +-- tanstack.com/ > ``` 3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: ```sh cd tanstack.com pnpm i # The app will run on https://localhost:3000 by default pnpm dev ``` 4. Now you can visit http://localhost:3000/query/latest/docs/framework/react/overview in the browser and see the changes you make in `tanstack/query/docs` there. > [!WARNING] > You will need to update the `docs/config.json` file (in `TanStack/query`) if you add a new documentation page! You can see the whole process in the screen capture below: https://github.com/fulopkovacs/form/assets/43729152/9d35a3c3-8153-4e74-9cb2-af275f7a269b ### Running examples - Make sure you've installed the dependencies in the repo's root directory. ```bash pnpm install ``` - If you want to run the example against your local changes, run below in the repo's root directory. Otherwise, it will be run against the latest TanStack Query release. ```bash pnpm run watch ``` - Run below in the selected examples' directory. ```bash pnpm run dev ``` #### Note on standalone execution If you want to run an example without installing dependencies for the whole repo, just follow the instructions from the example's README.md file. It will then be run against the latest TanStack Query release. ## Online one-click setup You can use Gitpod (An Online open-source VS Code-like IDE that is free for Open Source) for developing online. With a single click it will start a workspace and automatically: - clone the `TanStack/query` repo. - install all the dependencies in `/` and `/docs`. - run below in the root(`/`) to Auto-build files. ```bash npm start ``` - run below in `/docs`. ```bash npm run dev ``` [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TanStack/query) ## Changesets This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run `pnpm changeset` and commit the file. If needed, changeset descriptions can be more descriptive, and will be included in the changelog. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset. ## Pull requests Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface. Use an appropriate commit type. Be especially careful with breaking changes. ## Releases For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to NPM. ## 🧪 Test TanStack Query uses [Nx](https://nx.dev/) as its monorepo tool. To run tests in a local environment, you should use `nx` commands from the root directory. ### ✅ Run all tests To run tests for **all packages**, run: ```bash npm run test ``` ### ✅ Run tests for a specific package To run tests for a specific package, use the following command: ```bash npx nx run @tanstack/{package-name}:test:lib ``` For example: ```bash npx nx run @tanstack/react-query:test:lib ``` ### ⚠️ Caution Do not run `pnpm run test:lib` inside individual package folders. This can cause test failures due to dependencies between packages. Always run tests from the **root folder** using `nx` commands. ================================================ FILE: FUNDING.json ================================================ { "drips": { "ethereum": { "ownedBy": "0xD5371B61b35E13F2ae354BE95081aD63FB383452" } } } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021-present Tanner Linsley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
TanStack Query

semantic-release Best of JS Follow @TanStack
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
# TanStack Query An async state management library built to simplify fetching, caching, synchronizing, and updating server state. - Protocol‑agnostic fetching (REST, GraphQL, promises, etc.) - Caching, refetching, pagination & infinite scroll - Mutations, dependent queries & background updates - Prefetching, cancellation & React Suspense support ### Read the docs → ## Get Involved - We welcome issues and pull requests! - Participate in [GitHub discussions](https://github.com/TanStack/query/discussions) - Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) - See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions ## Partners
CodeRabbit Cloudflare
Query & you?

We're looking for TanStack Query Partners to join our mission! Partner with us to push the boundaries of TanStack Query and build amazing things together.

LET'S CHAT
## Explore the TanStack Ecosystem - TanStack Config – Tooling for JS/TS packages - TanStack DB – Reactive sync client store - TanStack DevTools – Unified devtools panel - TanStack Form – Type‑safe form state - TanStack Pacer – Debouncing, throttling, batching
- TanStack Query – Async state & caching - TanStack Ranger – Range & slider primitives - TanStack Router – Type‑safe routing, caching & URL state - TanStack Start – Full‑stack SSR & streaming - TanStack Store – Reactive data store - TanStack Table – Headless datagrids - TanStack Virtual – Virtualized rendering … and more at TanStack.com » ================================================ FILE: docs/community-resources.md ================================================ --- title: Community Resources articles: [ { title: "TkDodo's Blog Posts", url: 'https://tkdodo.eu/blog/practical-react-query', description: 'TkDodo, a maintainer of TanStack Query, writes a series of blog posts about the library. These articles offer general best practices and often present opinionated perspectives on using TanStack Query.', }, ] media: [ { title: 'React Query: It’s Time to Break up with your Global State! – Tanner Linsley', url: 'https://www.youtube.com/watch?v=seU46c6Jz7E', description: 'Get the lowdown on “global state” and how React Query can help you fetch, cache and manage your asynchronous data with a fraction of the effort and code that you’re used to', }, { title: 'All About React Query (with Tanner Linsley) — Learn With Jason', url: 'https://www.youtube.com/watch?v=DocXo3gqGdI', description: 'Learn all about React Query with its creator Tanner Linsley.', }, { title: 'Hooks for Fetching with ReactQuery Creator Tanner Linsley aka @tannerlinsley', url: 'https://www.youtube.com/watch?v=PPvWXbSCtBU', description: 'Learn how React Query simplifies asynchronous data fetching in React applications.', }, { title: 'React Query - Open Source Friday stream with Tanner Linsley from', url: 'https://www.youtube.com/watch?v=B3cJDT3j19I', description: 'An Open Source Friday stream featuring Tanner Linsley.', }, { title: 'React Query Presentation - Tanner Linsley', url: 'https://www.youtube.com/watch?v=_ehibado6rU', description: 'Tanner Linsley gives a talk to Lambda School students about the React Query.', }, { title: 'TanStack Query v4 (with Dominik Dorfmeister) — Learn With Jason', url: 'https://www.youtube.com/watch?v=SPPQm0dvEes', description: 'Dominik Dorfmeister covering TanStack Query v4.', }, { title: 'React Query Exposed by Its Maintainer', url: 'https://www.youtube.com/watch?v=8-RTNnn9GR8', description: 'Dominik Dorfmeister explores the less favorable aspects of React Query and situations where it may not be the best fit.', }, { title: 'React Query API Design: Lessons Learned - Dominik Dorfmeister', url: 'https://www.youtube.com/watch?v=l3PxErcKeAI', description: 'Dominik Dorfmeister walks through some of the API design choices that were made in React Query to get to the DX.', }, ] utilities: [ { title: 'batshit', url: 'https://github.com/yornaath/batshit', description: 'A batch manager that will deduplicate and batch requests for a certain data type made within a window', }, { title: 'GraphQL Code Generator', url: 'https://the-guild.dev/graphql/codegen', description: 'Generate React Query hooks from your GraphQL schema', }, { title: 'Http-wizard', url: 'https://http-wizard.com', description: 'End-to-end type-safe Fastify API with typeScript magic ✨', }, { title: 'Normy', url: 'https://github.com/klis87/normy', description: 'Automatic normalization and data updates for data fetching libraries', }, { title: 'Orval', url: 'https://orval.dev/', description: 'Generate TypeScript client from OpenAPI specifications.', }, { title: 'Query Key Factory', url: 'https://github.com/lukemorales/query-key-factory', description: 'A library for creating typesafe standardized query keys, useful for cache management in @tanstack/query', }, { title: 'React Query Kit', url: 'https://github.com/liaoliao666/react-query-kit', description: '🕊️ A toolkit for ReactQuery that makes ReactQuery hooks reusable and typesafe', }, { title: 'React Query Rewind', url: 'https://reactqueryrewind.com/', description: 'Time travel and visualize state during development', }, { title: 'React Query Swagger', url: 'https://github.com/Shaddix/react-query-swagger', description: 'Generate React Query hooks based on Swagger API definitions', }, { title: 'Suspensive React Query', url: 'https://suspensive.org/docs/react-query/motivation', description: 'Enhances React Query with Suspense support, allowing for simpler and more declarative data fetching', }, { title: 'tRPC', url: 'https://trpc.io/', description: 'End-to-end typesafe APIs made easy', }, ] others: [ { title: 'Atomic CRM', url: 'https://marmelab.com/atomic-crm/', description: 'A full-featured CRM built with React, react-admin, and Supabase.', }, { title: 'Blitz', url: 'https://blitzjs.com/', description: 'The Missing Fullstack Toolkit for Next.js', }, { title: 'Connect', url: 'https://connectrpc.com/docs', description: 'A family of libraries for building browser and gRPC-compatible HTTP APIs.', }, { title: 'Hey API', url: 'https://heyapi.dev/openapi-ts/plugins/tanstack-query', description: 'OpenAPI to TypeScript codegen. Production-ready SDKs, Zod schemas, TanStack Query hooks, and 20+ plugins. Used by Vercel, OpenCode, and PayPal.', }, { title: 'Kubb', url: 'https://www.kubb.dev/', description: 'Generate SDKs for all your APIs', }, { title: 'OpenAPI codegen', url: 'https://github.com/fabien0102/openapi-codegen', description: 'A tool for generating code based on an OpenAPI schema.', }, { title: 'OpenAPI Qraft React', url: 'https://github.com/OpenAPI-Qraft/openapi-qraft', description: 'Generate type-safe API clients and Hooks for TanStack Query directly from OpenAPI Documents.Zero-runtime overhead, Proxy-based design, seamless SSR support, and full TanStack Query functionality.', }, { title: 'OpenAPI React Query codegen', url: 'https://github.com/7nohe/openapi-react-query-codegen', description: 'Generate TanStack Query hooks based on an OpenAPI specification file.', }, { title: 'OpenAPI zod client', url: 'https://github.com/astahmer/openapi-zod-client', description: 'Generate a zodios client from an OpenAPI specification', }, { title: 'openapi-fetch', url: 'https://openapi-ts.dev/openapi-react-query/', description: 'A 2KB min, typesafe fetch wrapper that uses static TypeScript type inference and no runtime checks.', }, { title: 'oRPC', url: 'https://orpc.unnoq.com/docs/integrations/tanstack-query', description: 'Easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards.', }, { title: 'Rapini', url: 'https://github.com/rametta/rapini', description: '🥬 OpenAPI to React Query (or SWR) & Axios', }, { title: 'Tanstack Query Visualizer', url: 'https://tanstack-query-visualizer.sofi.coop/', description: 'An interactive sandbox that visualizes the relationship between mutations and query keys.', }, { title: 'ts-rest', url: 'https://ts-rest.com/', description: 'Incrementally adoptable type-safety for your new and existing APIs', }, { title: 'wagmi', url: 'https://wagmi.sh/', description: 'React Hooks for Ethereum based on @tanstack/react-query', }, { title: 'zodios', url: 'https://www.zodios.org/', description: 'End-to-end typesafe REST API toolbox', }, ] --- ================================================ FILE: docs/config.json ================================================ { "$schema": "https://raw.githubusercontent.com/TanStack/tanstack.com/main/tanstack-docs-config.schema.json", "docSearch": { "appId": "20TOVD6LOE", "apiKey": "35bc6c51aa322700d1383e17c4f669f4", "indexName": "tanstackquery" }, "sections": [ { "label": "Community Resources", "children": [], "frameworks": [] }, { "label": "Getting Started", "children": [], "frameworks": [ { "label": "react", "children": [ { "label": "Overview", "to": "framework/react/overview" }, { "label": "Installation", "to": "framework/react/installation" }, { "label": "Quick Start", "to": "framework/react/quick-start" }, { "label": "Devtools", "to": "framework/react/devtools" }, { "label": "Comparison", "to": "framework/react/comparison" }, { "label": "TypeScript", "to": "framework/react/typescript" }, { "label": "GraphQL", "to": "framework/react/graphql" }, { "label": "React Native", "to": "framework/react/react-native" } ] }, { "label": "solid", "children": [ { "label": "Overview", "to": "framework/solid/overview" }, { "label": "Quick Start", "to": "framework/solid/quick-start" }, { "label": "Installation", "to": "framework/solid/installation" }, { "label": "Devtools", "to": "framework/solid/devtools" }, { "label": "TypeScript", "to": "framework/solid/typescript" } ] }, { "label": "vue", "children": [ { "label": "Overview", "to": "framework/vue/overview" }, { "label": "Installation", "to": "framework/vue/installation" }, { "label": "Quick Start", "to": "framework/vue/quick-start" }, { "label": "Devtools", "to": "framework/vue/devtools" }, { "label": "TypeScript", "to": "framework/vue/typescript" }, { "label": "Reactivity", "to": "framework/vue/reactivity" }, { "label": "GraphQL", "to": "framework/vue/graphql" } ] }, { "label": "svelte", "children": [ { "label": "Overview", "to": "framework/svelte/overview" }, { "label": "Installation", "to": "framework/svelte/installation" }, { "label": "Devtools", "to": "framework/svelte/devtools" }, { "label": "SSR & SvelteKit", "to": "framework/svelte/ssr" }, { "label": "Migrate from v5 to v6", "to": "framework/svelte/migrate-from-v5-to-v6" } ] }, { "label": "angular", "children": [ { "label": "Overview", "to": "framework/angular/overview" }, { "label": "Installation", "to": "framework/angular/installation" }, { "label": "Quick Start", "to": "framework/angular/quick-start" }, { "label": "Angular HttpClient and other data fetching clients", "to": "framework/angular/angular-httpclient-and-other-data-fetching-clients" }, { "label": "Devtools", "to": "framework/angular/devtools" }, { "label": "TypeScript", "to": "framework/angular/typescript" }, { "label": "Zoneless", "to": "framework/angular/zoneless" } ] }, { "label": "preact", "children": [ { "label": "Overview", "to": "framework/preact/overview" }, { "label": "Installation", "to": "framework/preact/installation" }, { "label": "Quick Start", "to": "framework/preact/quick-start" }, { "label": "Devtools", "to": "framework/preact/devtools" }, { "label": "TypeScript", "to": "framework/preact/typescript" }, { "label": "GraphQL", "to": "framework/preact/graphql" } ] } ] }, { "label": "Guides & Concepts", "children": [], "frameworks": [ { "label": "react", "children": [ { "label": "Important Defaults", "to": "framework/react/guides/important-defaults" }, { "label": "Queries", "to": "framework/react/guides/queries" }, { "label": "Query Keys", "to": "framework/react/guides/query-keys" }, { "label": "Query Functions", "to": "framework/react/guides/query-functions" }, { "label": "Query Options", "to": "framework/react/guides/query-options" }, { "label": "Network Mode", "to": "framework/react/guides/network-mode" }, { "label": "Parallel Queries", "to": "framework/react/guides/parallel-queries" }, { "label": "Dependent Queries", "to": "framework/react/guides/dependent-queries" }, { "label": "Background Fetching Indicators", "to": "framework/react/guides/background-fetching-indicators" }, { "label": "Window Focus Refetching", "to": "framework/react/guides/window-focus-refetching" }, { "label": "Disabling/Pausing Queries", "to": "framework/react/guides/disabling-queries" }, { "label": "Query Retries", "to": "framework/react/guides/query-retries" }, { "label": "Paginated Queries", "to": "framework/react/guides/paginated-queries" }, { "label": "Infinite Queries", "to": "framework/react/guides/infinite-queries" }, { "label": "Initial Query Data", "to": "framework/react/guides/initial-query-data" }, { "label": "Placeholder Query Data", "to": "framework/react/guides/placeholder-query-data" }, { "label": "Mutations", "to": "framework/react/guides/mutations" }, { "label": "Query Invalidation", "to": "framework/react/guides/query-invalidation" }, { "label": "Invalidation from Mutations", "to": "framework/react/guides/invalidations-from-mutations" }, { "label": "Updates from Mutation Responses", "to": "framework/react/guides/updates-from-mutation-responses" }, { "label": "Optimistic Updates", "to": "framework/react/guides/optimistic-updates" }, { "label": "Query Cancellation", "to": "framework/react/guides/query-cancellation" }, { "label": "Scroll Restoration", "to": "framework/react/guides/scroll-restoration" }, { "label": "Filters", "to": "framework/react/guides/filters" }, { "label": "Performance & Request Waterfalls", "to": "framework/react/guides/request-waterfalls" }, { "label": "Prefetching & Router Integration", "to": "framework/react/guides/prefetching" }, { "label": "Server Rendering & Hydration", "to": "framework/react/guides/ssr" }, { "label": "Advanced Server Rendering", "to": "framework/react/guides/advanced-ssr" }, { "label": "Caching", "to": "framework/react/guides/caching" }, { "label": "Render Optimizations", "to": "framework/react/guides/render-optimizations" }, { "label": "Default Query Fn", "to": "framework/react/guides/default-query-function" }, { "label": "Suspense", "to": "framework/react/guides/suspense" }, { "label": "Testing", "to": "framework/react/guides/testing" }, { "label": "Does this replace [Redux, MobX, etc]?", "to": "framework/react/guides/does-this-replace-client-state" }, { "label": "Migrating to v3", "to": "framework/react/guides/migrating-to-react-query-3" }, { "label": "Migrating to v4", "to": "framework/react/guides/migrating-to-react-query-4" }, { "label": "Migrating to v5", "to": "framework/react/guides/migrating-to-v5" } ] }, { "label": "solid", "children": [ { "label": "Important Defaults", "to": "framework/solid/guides/important-defaults" }, { "label": "Queries", "to": "framework/solid/guides/queries" }, { "label": "Query Keys", "to": "framework/solid/guides/query-keys" }, { "label": "Query Functions", "to": "framework/solid/guides/query-functions" }, { "label": "Query Options", "to": "framework/solid/guides/query-options" }, { "label": "Network Mode", "to": "framework/solid/guides/network-mode" }, { "label": "Parallel Queries", "to": "framework/solid/guides/parallel-queries" }, { "label": "Dependent Queries", "to": "framework/solid/guides/dependent-queries" }, { "label": "Background Fetching Indicators", "to": "framework/solid/guides/background-fetching-indicators" }, { "label": "Window Focus Refetching", "to": "framework/solid/guides/window-focus-refetching" }, { "label": "Disabling/Pausing Queries", "to": "framework/solid/guides/disabling-queries" }, { "label": "Query Retries", "to": "framework/solid/guides/query-retries" }, { "label": "Paginated Queries", "to": "framework/solid/guides/paginated-queries" }, { "label": "Infinite Queries", "to": "framework/solid/guides/infinite-queries" }, { "label": "Initial Query Data", "to": "framework/solid/guides/initial-query-data" }, { "label": "Placeholder Query Data", "to": "framework/solid/guides/placeholder-query-data" }, { "label": "Mutations", "to": "framework/solid/guides/mutations" }, { "label": "Query Invalidation", "to": "framework/solid/guides/query-invalidation" }, { "label": "Invalidation from Mutations", "to": "framework/solid/guides/invalidations-from-mutations" }, { "label": "Updates from Mutation Responses", "to": "framework/solid/guides/updates-from-mutation-responses" }, { "label": "Optimistic Updates", "to": "framework/solid/guides/optimistic-updates" }, { "label": "Query Cancellation", "to": "framework/solid/guides/query-cancellation" }, { "label": "Scroll Restoration", "to": "framework/solid/guides/scroll-restoration" }, { "label": "Filters", "to": "framework/solid/guides/filters" }, { "label": "Request Waterfalls", "to": "framework/solid/guides/request-waterfalls" }, { "label": "Prefetching", "to": "framework/solid/guides/prefetching" }, { "label": "SSR", "to": "framework/solid/guides/ssr" }, { "label": "Advanced SSR", "to": "framework/solid/guides/advanced-ssr" }, { "label": "Caching", "to": "framework/solid/guides/caching" }, { "label": "Default Query Fn", "to": "framework/solid/guides/default-query-function" }, { "label": "Suspense", "to": "framework/solid/guides/suspense" }, { "label": "Testing", "to": "framework/solid/guides/testing" }, { "label": "Does this replace state managers?", "to": "framework/solid/guides/does-this-replace-client-state" } ] }, { "label": "vue", "children": [ { "label": "Important Defaults", "to": "framework/vue/guides/important-defaults" }, { "label": "Queries", "to": "framework/vue/guides/queries" }, { "label": "Query Keys", "to": "framework/vue/guides/query-keys" }, { "label": "Query Functions", "to": "framework/vue/guides/query-functions" }, { "label": "Query Options", "to": "framework/vue/guides/query-options" }, { "label": "Network Mode", "to": "framework/vue/guides/network-mode" }, { "label": "Parallel Queries", "to": "framework/vue/guides/parallel-queries" }, { "label": "Dependent Queries", "to": "framework/vue/guides/dependent-queries" }, { "label": "Background Fetching Indicators", "to": "framework/vue/guides/background-fetching-indicators" }, { "label": "Window Focus Refetching", "to": "framework/vue/guides/window-focus-refetching" }, { "label": "Disabling/Pausing Queries", "to": "framework/vue/guides/disabling-queries" }, { "label": "Query Retries", "to": "framework/vue/guides/query-retries" }, { "label": "Paginated Queries", "to": "framework/vue/guides/paginated-queries" }, { "label": "Infinite Queries", "to": "framework/vue/guides/infinite-queries" }, { "label": "Initial Query Data", "to": "framework/vue/guides/initial-query-data" }, { "label": "Placeholder Query Data", "to": "framework/vue/guides/placeholder-query-data" }, { "label": "Mutations", "to": "framework/vue/guides/mutations" }, { "label": "Query Invalidation", "to": "framework/vue/guides/query-invalidation" }, { "label": "Invalidation from Mutations", "to": "framework/vue/guides/invalidations-from-mutations" }, { "label": "Updates from Mutation Responses", "to": "framework/vue/guides/updates-from-mutation-responses" }, { "label": "Optimistic Updates", "to": "framework/vue/guides/optimistic-updates" }, { "label": "Query Cancellation", "to": "framework/vue/guides/query-cancellation" }, { "label": "Scroll Restoration", "to": "framework/vue/guides/scroll-restoration" }, { "label": "Filters", "to": "framework/vue/guides/filters" }, { "label": "Prefetching", "to": "framework/vue/guides/prefetching" }, { "label": "SSR & Nuxt", "to": "framework/vue/guides/ssr" }, { "label": "Caching", "to": "framework/vue/guides/caching" }, { "label": "Default Query Fn", "to": "framework/vue/guides/default-query-function" }, { "label": "Suspense", "to": "framework/vue/guides/suspense" }, { "label": "Testing", "to": "framework/vue/guides/testing" }, { "label": "Custom Client", "to": "framework/vue/guides/custom-client" }, { "label": "Does this replace [Vuex, Pinia]?", "to": "framework/vue/guides/does-this-replace-client-state" }, { "label": "Migrating to v5", "to": "framework/vue/guides/migrating-to-v5" } ] }, { "label": "angular", "children": [ { "label": "Important Defaults", "to": "framework/angular/guides/important-defaults" }, { "label": "Queries", "to": "framework/angular/guides/queries" }, { "label": "Query Keys", "to": "framework/angular/guides/query-keys" }, { "label": "Query Functions", "to": "framework/angular/guides/query-functions" }, { "label": "Query Options", "to": "framework/angular/guides/query-options" }, { "label": "Network Mode", "to": "framework/angular/guides/network-mode" }, { "label": "Parallel Queries", "to": "framework/angular/guides/parallel-queries" }, { "label": "Dependent Queries", "to": "framework/angular/guides/dependent-queries" }, { "label": "Background Fetching Indicators", "to": "framework/angular/guides/background-fetching-indicators" }, { "label": "Window Focus Refetching", "to": "framework/angular/guides/window-focus-refetching" }, { "label": "Disabling/Pausing Queries", "to": "framework/angular/guides/disabling-queries" }, { "label": "Query Retries", "to": "framework/angular/guides/query-retries" }, { "label": "Paginated Queries", "to": "framework/angular/guides/paginated-queries" }, { "label": "Infinite Queries", "to": "framework/angular/guides/infinite-queries" }, { "label": "Initial Query Data", "to": "framework/angular/guides/initial-query-data" }, { "label": "Placeholder Query Data", "to": "framework/angular/guides/placeholder-query-data" }, { "label": "Mutations", "to": "framework/angular/guides/mutations" }, { "label": "Mutation Options", "to": "framework/angular/guides/mutation-options" }, { "label": "Query Invalidation", "to": "framework/angular/guides/query-invalidation" }, { "label": "Invalidation from Mutations", "to": "framework/angular/guides/invalidations-from-mutations" }, { "label": "Optimistic Updates", "to": "framework/angular/guides/optimistic-updates" }, { "label": "Query Cancellation", "to": "framework/angular/guides/query-cancellation" }, { "label": "Scroll Restoration", "to": "framework/angular/guides/scroll-restoration" }, { "label": "Filters", "to": "framework/angular/guides/filters" }, { "label": "Caching", "to": "framework/angular/guides/caching" }, { "label": "Default Query Fn", "to": "framework/angular/guides/default-query-function" }, { "label": "Testing", "to": "framework/angular/guides/testing" }, { "label": "Does this replace state managers?", "to": "framework/angular/guides/does-this-replace-client-state" } ] }, { "label": "preact", "children": [ { "label": "Important Defaults", "to": "framework/preact/guides/important-defaults" }, { "label": "Queries", "to": "framework/preact/guides/queries" }, { "label": "Query Keys", "to": "framework/preact/guides/query-keys" }, { "label": "Query Functions", "to": "framework/preact/guides/query-functions" }, { "label": "Query Options", "to": "framework/preact/guides/query-options" }, { "label": "Network Mode", "to": "framework/preact/guides/network-mode" }, { "label": "Parallel Queries", "to": "framework/preact/guides/parallel-queries" }, { "label": "Dependent Queries", "to": "framework/preact/guides/dependent-queries" }, { "label": "Background Fetching Indicators", "to": "framework/preact/guides/background-fetching-indicators" }, { "label": "Window Focus Refetching", "to": "framework/preact/guides/window-focus-refetching" }, { "label": "Disabling/Pausing Queries", "to": "framework/preact/guides/disabling-queries" }, { "label": "Query Retries", "to": "framework/preact/guides/query-retries" }, { "label": "Paginated Queries", "to": "framework/preact/guides/paginated-queries" }, { "label": "Infinite Queries", "to": "framework/preact/guides/infinite-queries" }, { "label": "Initial Query Data", "to": "framework/preact/guides/initial-query-data" }, { "label": "Placeholder Query Data", "to": "framework/preact/guides/placeholder-query-data" }, { "label": "Mutations", "to": "framework/preact/guides/mutations" }, { "label": "Query Invalidation", "to": "framework/preact/guides/query-invalidation" }, { "label": "Invalidation from Mutations", "to": "framework/preact/guides/invalidations-from-mutations" }, { "label": "Updates from Mutation Responses", "to": "framework/preact/guides/updates-from-mutation-responses" }, { "label": "Optimistic Updates", "to": "framework/preact/guides/optimistic-updates" }, { "label": "Query Cancellation", "to": "framework/preact/guides/query-cancellation" }, { "label": "Scroll Restoration", "to": "framework/preact/guides/scroll-restoration" }, { "label": "Filters", "to": "framework/preact/guides/filters" }, { "label": "Performance & Request Waterfalls", "to": "framework/preact/guides/request-waterfalls" }, { "label": "Prefetching & Router Integration", "to": "framework/preact/guides/prefetching" }, { "label": "Caching", "to": "framework/preact/guides/caching" }, { "label": "Render Optimizations", "to": "framework/preact/guides/render-optimizations" }, { "label": "Default Query Fn", "to": "framework/preact/guides/default-query-function" }, { "label": "Does this replace [Redux, MobX, etc]?", "to": "framework/preact/guides/does-this-replace-client-state" } ] } ] }, { "label": "API Reference", "children": [ { "label": "QueryClient", "to": "reference/QueryClient" }, { "label": "QueryCache", "to": "reference/QueryCache" }, { "label": "MutationCache", "to": "reference/MutationCache" }, { "label": "QueryObserver", "to": "reference/QueryObserver" }, { "label": "InfiniteQueryObserver", "to": "reference/InfiniteQueryObserver" }, { "label": "QueriesObserver", "to": "reference/QueriesObserver" }, { "label": "streamedQuery", "to": "reference/streamedQuery" }, { "label": "focusManager", "to": "reference/focusManager" }, { "label": "onlineManager", "to": "reference/onlineManager" }, { "label": "environmentManager", "to": "reference/environmentManager" }, { "label": "notifyManager", "to": "reference/notifyManager" }, { "label": "timeoutManager", "to": "reference/timeoutManager" } ], "frameworks": [ { "label": "react", "children": [ { "label": "useQuery", "to": "framework/react/reference/useQuery" }, { "label": "useQueries", "to": "framework/react/reference/useQueries" }, { "label": "useInfiniteQuery", "to": "framework/react/reference/useInfiniteQuery" }, { "label": "useMutation", "to": "framework/react/reference/useMutation" }, { "label": "useIsFetching", "to": "framework/react/reference/useIsFetching" }, { "label": "useIsMutating", "to": "framework/react/reference/useIsMutating" }, { "label": "useMutationState", "to": "framework/react/reference/useMutationState" }, { "label": "useSuspenseQuery", "to": "framework/react/reference/useSuspenseQuery" }, { "label": "useSuspenseInfiniteQuery", "to": "framework/react/reference/useSuspenseInfiniteQuery" }, { "label": "useSuspenseQueries", "to": "framework/react/reference/useSuspenseQueries" }, { "label": "QueryClientProvider", "to": "framework/react/reference/QueryClientProvider" }, { "label": "useQueryClient", "to": "framework/react/reference/useQueryClient" }, { "label": "queryOptions", "to": "framework/react/reference/queryOptions" }, { "label": "infiniteQueryOptions", "to": "framework/react/reference/infiniteQueryOptions" }, { "label": "mutationOptions", "to": "framework/react/reference/mutationOptions" }, { "label": "usePrefetchQuery", "to": "framework/react/reference/usePrefetchQuery" }, { "label": "usePrefetchInfiniteQuery", "to": "framework/react/reference/usePrefetchInfiniteQuery" }, { "label": "QueryErrorResetBoundary", "to": "framework/react/reference/QueryErrorResetBoundary" }, { "label": "useQueryErrorResetBoundary", "to": "framework/react/reference/useQueryErrorResetBoundary" }, { "label": "hydration", "to": "framework/react/reference/hydration" } ] }, { "label": "vue", "children": [ { "label": "useQuery", "to": "framework/vue/reference/useQuery" }, { "label": "useQueries", "to": "framework/vue/reference/useQueries" }, { "label": "useInfiniteQuery", "to": "framework/vue/reference/useInfiniteQuery" }, { "label": "useMutation", "to": "framework/vue/reference/useMutation" }, { "label": "useIsFetching", "to": "framework/vue/reference/useIsFetching" }, { "label": "useIsMutating", "to": "framework/vue/reference/useIsMutating" }, { "label": "useMutationState", "to": "framework/vue/reference/useMutationState" }, { "label": "useQueryClient", "to": "framework/vue/reference/useQueryClient" }, { "label": "queryOptions", "to": "framework/vue/reference/queryOptions" }, { "label": "infiniteQueryOptions", "to": "framework/vue/reference/infiniteQueryOptions" }, { "label": "hydration", "to": "framework/vue/reference/hydration" } ] }, { "label": "solid", "children": [ { "label": "useQuery", "to": "framework/solid/reference/useQuery" }, { "label": "useQueries", "to": "framework/solid/reference/useQueries" }, { "label": "useInfiniteQuery", "to": "framework/solid/reference/useInfiniteQuery" }, { "label": "useMutation", "to": "framework/solid/reference/useMutation" }, { "label": "useIsFetching", "to": "framework/solid/reference/useIsFetching" }, { "label": "useIsMutating", "to": "framework/solid/reference/useIsMutating" }, { "label": "useMutationState", "to": "framework/solid/reference/useMutationState" }, { "label": "queryOptions", "to": "framework/solid/reference/queryOptions" }, { "label": "infiniteQueryOptions", "to": "framework/solid/reference/infiniteQueryOptions" }, { "label": "hydration", "to": "framework/solid/reference/hydration" } ] }, { "label": "svelte", "children": [ { "label": "Svelte Reference", "to": "framework/svelte/reference/index" }, { "label": "Functions / createQuery", "to": "framework/svelte/reference/functions/createQuery" }, { "label": "Functions / createQueries", "to": "framework/svelte/reference/functions/createQueries" }, { "label": "Functions / createInfiniteQuery", "to": "framework/svelte/reference/functions/createInfiniteQuery" }, { "label": "Functions / createMutation", "to": "framework/svelte/reference/functions/createMutation" }, { "label": "Functions / useIsFetching", "to": "framework/svelte/reference/functions/useIsFetching" }, { "label": "Functions / useIsMutating", "to": "framework/svelte/reference/functions/useIsMutating" }, { "label": "Functions / useMutationState", "to": "framework/svelte/reference/functions/useMutationState" }, { "label": "Functions / queryOptions", "to": "framework/svelte/reference/functions/queryOptions" }, { "label": "Functions / infiniteQueryOptions", "to": "framework/svelte/reference/functions/infiniteQueryOptions" }, { "label": "Functions / mutationOptions", "to": "framework/svelte/reference/functions/mutationOptions" } ] }, { "label": "angular", "children": [ { "label": "Angular Reference", "to": "framework/angular/reference/index" }, { "label": "Functions / injectQuery", "to": "framework/angular/reference/functions/injectQuery" }, { "label": "Functions / injectMutation", "to": "framework/angular/reference/functions/injectMutation" } ] }, { "label": "preact", "children": [ { "label": "useQuery", "to": "framework/preact/reference/functions/useQuery" }, { "label": "useQueries", "to": "framework/preact/reference/functions/useQueries" }, { "label": "useInfiniteQuery", "to": "framework/preact/reference/functions/useInfiniteQuery" }, { "label": "useMutation", "to": "framework/preact/reference/functions/useMutation" }, { "label": "useIsFetching", "to": "framework/preact/reference/functions/useIsFetching" }, { "label": "useIsMutating", "to": "framework/preact/reference/functions/useIsMutating" }, { "label": "useMutationState", "to": "framework/preact/reference/functions/useMutationState" }, { "label": "useSuspenseQuery", "to": "framework/preact/reference/functions/useSuspenseQuery" }, { "label": "useSuspenseInfiniteQuery", "to": "framework/preact/reference/functions/useSuspenseInfiniteQuery" }, { "label": "useSuspenseQueries", "to": "framework/preact/reference/functions/useSuspenseQueries" }, { "label": "QueryClientProvider", "to": "framework/preact/reference/functions/QueryClientProvider" }, { "label": "useQueryClient", "to": "framework/preact/reference/functions/useQueryClient" }, { "label": "queryOptions", "to": "framework/preact/reference/functions/queryOptions" }, { "label": "infiniteQueryOptions", "to": "framework/preact/reference/functions/infiniteQueryOptions" }, { "label": "mutationOptions", "to": "framework/preact/reference/functions/mutationOptions" }, { "label": "usePrefetchQuery", "to": "framework/preact/reference/functions/usePrefetchQuery" }, { "label": "usePrefetchInfiniteQuery", "to": "framework/preact/reference/functions/usePrefetchInfiniteQuery" }, { "label": "QueryErrorResetBoundary", "to": "framework/preact/reference/functions/QueryErrorResetBoundary" }, { "label": "useQueryErrorResetBoundary", "to": "framework/preact/reference/functions/useQueryErrorResetBoundary" }, { "label": "hydration", "to": "framework/preact/reference/functions/HydrationBoundary" } ] } ] }, { "label": "ESLint", "children": [ { "label": "ESLint Plugin Query", "to": "eslint/eslint-plugin-query" }, { "label": "Exhaustive Deps", "to": "eslint/exhaustive-deps" }, { "label": "Stable Query Client", "to": "eslint/stable-query-client" }, { "label": "No Rest Destructuring", "to": "eslint/no-rest-destructuring" }, { "label": "No Unstable Deps", "to": "eslint/no-unstable-deps" }, { "label": "Infinite Query Property Order", "to": "eslint/infinite-query-property-order" }, { "label": "No void Query Functions", "to": "eslint/no-void-query-fn" }, { "label": "Mutation Property Order", "to": "eslint/mutation-property-order" } ] }, { "label": "Examples", "children": [], "frameworks": [ { "label": "react", "children": [ { "label": "Simple", "to": "framework/react/examples/simple" }, { "label": "Basic", "to": "framework/react/examples/basic" }, { "label": "Basic w/ GraphQL-Request", "to": "framework/react/examples/basic-graphql-request" }, { "label": "Auto Refetching / Polling / Realtime", "to": "framework/react/examples/auto-refetching" }, { "label": "Optimistic Updates (UI)", "to": "framework/react/examples/optimistic-updates-ui" }, { "label": "Optimistic Updates (Cache)", "to": "framework/react/examples/optimistic-updates-cache" }, { "label": "Pagination", "to": "framework/react/examples/pagination" }, { "label": "Load-More & Infinite Scroll", "to": "framework/react/examples/load-more-infinite-scroll" }, { "label": "Infinite query with Max pages", "to": "framework/react/examples/infinite-query-with-max-pages" }, { "label": "Suspense", "to": "framework/react/examples/suspense" }, { "label": "Default Query Function", "to": "framework/react/examples/default-query-function" }, { "label": "Playground", "to": "framework/react/examples/playground" }, { "label": "Prefetching", "to": "framework/react/examples/prefetching" }, { "label": "Star Wars", "to": "framework/react/examples/star-wars" }, { "label": "Rick And Morty", "to": "framework/react/examples/rick-morty" }, { "label": "Next.js Pages", "to": "framework/react/examples/nextjs" }, { "label": "Next.js app with prefetching", "to": "framework/react/examples/nextjs-app-prefetching" }, { "label": "Next.js app with streaming", "to": "framework/react/examples/nextjs-suspense-streaming" }, { "label": "React Native", "to": "framework/react/examples/react-native" }, { "label": "React Router", "to": "framework/react/examples/react-router" }, { "label": "Offline Queries and Mutations", "to": "framework/react/examples/offline" }, { "label": "Algolia", "to": "framework/react/examples/algolia" }, { "label": "Shadow DOM", "to": "framework/react/examples/shadow-dom" }, { "label": "Devtools Embedded Panel", "to": "framework/react/examples/devtools-panel" }, { "label": "Chat example (streaming)", "to": "framework/react/examples/chat" } ] }, { "label": "solid", "children": [ { "label": "Simple", "to": "framework/solid/examples/simple" }, { "label": "Basic", "to": "framework/solid/examples/basic" }, { "label": "Basic w/ GraphQL-Request", "to": "framework/solid/examples/basic-graphql-request" }, { "label": "Default Query Function", "to": "framework/solid/examples/default-query-function" }, { "label": "Solid Start", "to": "framework/solid/examples/solid-start-streaming" }, { "label": "Astro", "to": "framework/solid/examples/astro" }, { "label": "Offline Queries and Mutations", "to": "framework/solid/examples/offline" } ] }, { "label": "vue", "children": [ { "label": "Basic", "to": "framework/vue/examples/basic" }, { "label": "Vue 2.6", "to": "framework/vue/examples/2.6-basic" }, { "label": "Vue 2.7", "to": "framework/vue/examples/2.7-basic" }, { "label": "Nuxt 3", "to": "framework/vue/examples/nuxt3" }, { "label": "Persister", "to": "framework/vue/examples/persister" } ] }, { "label": "svelte", "children": [ { "label": "Simple", "to": "framework/svelte/examples/simple" }, { "label": "Basic", "to": "framework/svelte/examples/basic" }, { "label": "Auto Refetching / Polling / Realtime", "to": "framework/svelte/examples/auto-refetching" }, { "label": "SSR", "to": "framework/svelte/examples/ssr" }, { "label": "Optimistic Updates", "to": "framework/svelte/examples/optimistic-updates" }, { "label": "Playground", "to": "framework/svelte/examples/playground" }, { "label": "Star Wars", "to": "framework/svelte/examples/star-wars" }, { "label": "Infinite Queries", "to": "framework/svelte/examples/load-more-infinite-scroll" } ] }, { "label": "angular", "children": [ { "label": "Simple", "to": "framework/angular/examples/simple" }, { "label": "Basic", "to": "framework/angular/examples/basic" }, { "label": "Auto Refetching / Polling / Realtime", "to": "framework/angular/examples/auto-refetching" }, { "label": "Optimistic Updates", "to": "framework/angular/examples/optimistic-updates" }, { "label": "Pagination", "to": "framework/angular/examples/pagination" }, { "label": "Infinite query with maxPages", "to": "framework/angular/examples/infinite-query-with-max-pages" }, { "label": "Angular Router", "to": "framework/angular/examples/router" }, { "label": "RxJS autocomplete", "to": "framework/angular/examples/rxjs" }, { "label": "Query options from a service", "to": "framework/angular/examples/query-options-from-a-service" }, { "label": "Devtools embedded panel", "to": "framework/angular/examples/devtools-panel" } ] } ] }, { "label": "Plugins", "children": [], "frameworks": [ { "label": "react", "children": [ { "label": "persistQueryClient", "to": "framework/react/plugins/persistQueryClient" }, { "label": "createSyncStoragePersister", "to": "framework/react/plugins/createSyncStoragePersister" }, { "label": "createAsyncStoragePersister", "to": "framework/react/plugins/createAsyncStoragePersister" }, { "label": "broadcastQueryClient (Experimental)", "to": "framework/react/plugins/broadcastQueryClient" }, { "label": "createPersister (Experimental)", "to": "framework/react/plugins/createPersister" } ] }, { "label": "solid", "children": [ { "label": "broadcastQueryClient (Experimental)", "to": "framework/solid/plugins/broadcastQueryClient" }, { "label": "createPersister (Experimental)", "to": "framework/solid/plugins/createPersister" } ] }, { "label": "vue", "children": [ { "label": "broadcastQueryClient (Experimental)", "to": "framework/vue/plugins/broadcastQueryClient" }, { "label": "createPersister (Experimental)", "to": "framework/vue/plugins/createPersister" } ] }, { "label": "preact", "children": [ { "label": "persistQueryClient", "to": "framework/preact/plugins/persistQueryClient" }, { "label": "createSyncStoragePersister", "to": "framework/preact/plugins/createSyncStoragePersister" }, { "label": "createAsyncStoragePersister", "to": "framework/preact/plugins/createAsyncStoragePersister" }, { "label": "broadcastQueryClient (Experimental)", "to": "framework/preact/plugins/broadcastQueryClient" }, { "label": "createPersister (Experimental)", "to": "framework/preact/plugins/createPersister" } ] } ] } ], "users": [ "Google", "Walmart", "Facebook", "PayPal", "Amazon", "American Express", "Microsoft", "Target", "Ebay", "Autodesk", "CarFAX", "Docusign", "HP", "MLB", "Volvo", "Ocado", "UPC.ch", "EFI.com", "ReactBricks", "Nozzle.io", "Uber" ] } ================================================ FILE: docs/eslint/eslint-plugin-query.md ================================================ --- id: eslint-plugin-query title: ESLint Plugin Query --- TanStack Query comes with its own ESLint plugin. This plugin is used to enforce best practices and to help you avoid common mistakes. ## Installation The plugin is a separate package that you need to install: ```bash npm i -D @tanstack/eslint-plugin-query ``` or ```bash pnpm add -D @tanstack/eslint-plugin-query ``` or ```bash yarn add -D @tanstack/eslint-plugin-query ``` or ```bash bun add -D @tanstack/eslint-plugin-query ``` ## Flat Config (`eslint.config.js`) ### Recommended setup To enable all of the recommended rules for our plugin, add the following config: ```js import pluginQuery from '@tanstack/eslint-plugin-query' export default [ ...pluginQuery.configs['flat/recommended'], // Any other config... ] ``` ### Custom setup Alternatively, you can load the plugin and configure only the rules you want to use: ```js import pluginQuery from '@tanstack/eslint-plugin-query' export default [ { plugins: { '@tanstack/query': pluginQuery, }, rules: { '@tanstack/query/exhaustive-deps': 'error', }, }, // Any other config... ] ``` ## Legacy Config (`.eslintrc`) ### Recommended setup To enable all of the recommended rules for our plugin, add `plugin:@tanstack/query/recommended` in extends: ```json { "extends": ["plugin:@tanstack/query/recommended"] } ``` ### Custom setup Alternatively, add `@tanstack/query` to the plugins section, and configure the rules you want to use: ```json { "plugins": ["@tanstack/query"], "rules": { "@tanstack/query/exhaustive-deps": "error" } } ``` ## Rules - [@tanstack/query/exhaustive-deps](./exhaustive-deps.md) - [@tanstack/query/no-rest-destructuring](./no-rest-destructuring.md) - [@tanstack/query/stable-query-client](./stable-query-client.md) - [@tanstack/query/no-unstable-deps](./no-unstable-deps.md) - [@tanstack/query/infinite-query-property-order](./infinite-query-property-order.md) - [@tanstack/query/no-void-query-fn](./no-void-query-fn.md) - [@tanstack/query/mutation-property-order](./mutation-property-order.md) ================================================ FILE: docs/eslint/exhaustive-deps.md ================================================ --- id: exhaustive-deps title: Exhaustive dependencies for query keys --- Query keys should be seen like a dependency array to your query function: Every variable that is used inside the queryFn should be added to the query key. This makes sure that queries are cached independently and that queries are refetched automatically when the variables changes. ## Rule Details Examples of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/exhaustive-deps": "error" */ useQuery({ queryKey: ['todo'], queryFn: () => api.getTodo(todoId), }) const todoQueries = { detail: (id) => ({ queryKey: ['todo'], queryFn: () => api.getTodo(id) }), } ``` Examples of **correct** code for this rule: ```tsx useQuery({ queryKey: ['todo', todoId], queryFn: () => api.getTodo(todoId), }) const todoQueries = { detail: (id) => ({ queryKey: ['todo', id], queryFn: () => api.getTodo(id) }), } ``` ## When Not To Use It If you don't care about the rules of the query keys, then you will not need this rule. ## Attributes - [x] ✅ Recommended - [x] 🔧 Fixable ================================================ FILE: docs/eslint/infinite-query-property-order.md ================================================ --- id: infinite-query-property-order title: Ensure correct order of inference sensitive properties for infinite queries --- For the following functions, the property order of the passed in object matters due to type inference: - `useInfiniteQuery` - `useSuspenseInfiniteQuery` - `infiniteQueryOptions` The correct property order is as follows: - `queryFn` - `getPreviousPageParam` - `getNextPageParam` All other properties are insensitive to the order as they do not depend on type inference. ## Rule Details Examples of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/infinite-query-property-order": "warn" */ import { useInfiniteQuery } from '@tanstack/react-query' const query = useInfiniteQuery({ queryKey: ['projects'], getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, queryFn: async ({ pageParam }) => { const response = await fetch(`/api/projects?cursor=${pageParam}`) return await response.json() }, initialPageParam: 0, getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, maxPages: 3, }) ``` Examples of **correct** code for this rule: ```tsx /* eslint "@tanstack/query/infinite-query-property-order": "warn" */ import { useInfiniteQuery } from '@tanstack/react-query' const query = useInfiniteQuery({ queryKey: ['projects'], queryFn: async ({ pageParam }) => { const response = await fetch(`/api/projects?cursor=${pageParam}`) return await response.json() }, initialPageParam: 0, getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, maxPages: 3, }) ``` ## Attributes - [x] ✅ Recommended - [x] 🔧 Fixable ================================================ FILE: docs/eslint/mutation-property-order.md ================================================ --- id: mutation-property-order title: Ensure correct order of inference-sensitive properties in useMutation() --- For the following functions, the property order of the passed in object matters due to type inference: - `useMutation()` The correct property order is as follows: - `onMutate` - `onError` - `onSettled` All other properties are insensitive to the order as they do not depend on type inference. ## Rule Details Examples of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/mutation-property-order": "warn" */ import { useMutation } from '@tanstack/react-query' const mutation = useMutation({ mutationFn: () => Promise.resolve('success'), onSettled: () => { results.push('onSettled-promise') return Promise.resolve('also-ignored') // Promise (should be ignored) }, onMutate: async () => { results.push('onMutate-async') await sleep(1) return { backup: 'async-data' } }, onError: async () => { results.push('onError-async-start') await sleep(1) results.push('onError-async-end') }, }) ``` Examples of **correct** code for this rule: ```tsx /* eslint "@tanstack/query/mutation-property-order": "warn" */ import { useMutation } from '@tanstack/react-query' const mutation = useMutation({ mutationFn: () => Promise.resolve('success'), onMutate: async () => { results.push('onMutate-async') await sleep(1) return { backup: 'async-data' } }, onError: async () => { results.push('onError-async-start') await sleep(1) results.push('onError-async-end') }, onSettled: () => { results.push('onSettled-promise') return Promise.resolve('also-ignored') // Promise (should be ignored) }, }) ``` ## Attributes - [x] ✅ Recommended - [x] 🔧 Fixable ================================================ FILE: docs/eslint/no-rest-destructuring.md ================================================ --- id: no-rest-destructuring title: Disallow object rest destructuring on query results --- Use object rest destructuring on query results automatically subscribes to every field of the query result, which may cause unnecessary re-renders. This makes sure that you only subscribe to the fields that you actually need. ## Rule Details Examples of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/no-rest-destructuring": "warn" */ const useTodos = () => { const { data: todos, ...rest } = useQuery({ queryKey: ['todos'], queryFn: () => api.getTodos(), }) return { todos, ...rest } } ``` Examples of **correct** code for this rule: ```tsx const todosQuery = useQuery({ queryKey: ['todos'], queryFn: () => api.getTodos(), }) // normal object destructuring is fine const { data: todos } = todosQuery ``` ## When Not To Use It If you set the `notifyOnChangeProps` options manually, you can disable this rule. Since you are not using tracked queries, you are responsible for specifying which props should trigger a re-render. ## Attributes - [x] ✅ Recommended - [ ] 🔧 Fixable ================================================ FILE: docs/eslint/no-unstable-deps.md ================================================ --- id: no-unstable-deps title: Disallow putting the result of query hooks directly in a React hook dependency array --- The object returned from the following query hooks is **not** referentially stable: - `useQuery` - `useSuspenseQuery` - `useQueries` - `useSuspenseQueries` - `useInfiniteQuery` - `useSuspenseInfiniteQuery` - `useMutation` The object returned from those hooks should **not** be put directly into the dependency array of a React hook (e.g. `useEffect`, `useMemo`, `useCallback`). Instead, destructure the return value of the query hook and pass the destructured values into the dependency array of the React hook. ## Rule Details Examples of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/no-unstable-deps": "warn" */ import { useCallback } from 'React' import { useMutation } from '@tanstack/react-query' function Component() { const mutation = useMutation({ mutationFn: (value: string) => value }) const callback = useCallback(() => { mutation.mutate('hello') }, [mutation]) return null } ``` Examples of **correct** code for this rule: ```tsx /* eslint "@tanstack/query/no-unstable-deps": "warn" */ import { useCallback } from 'React' import { useMutation } from '@tanstack/react-query' function Component() { const { mutate } = useMutation({ mutationFn: (value: string) => value }) const callback = useCallback(() => { mutate('hello') }, [mutate]) return null } ``` ## Attributes - [x] ✅ Recommended - [ ] 🔧 Fixable ================================================ FILE: docs/eslint/no-void-query-fn.md ================================================ --- id: no-void-query-fn title: Disallow returning void from query functions --- Query functions must return a value that will be cached by TanStack Query. Functions that don't return a value (void functions) can lead to unexpected behavior and might indicate a mistake in the implementation. ## Rule Details Example of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/no-void-query-fn": "error" */ useQuery({ queryKey: ['todos'], queryFn: async () => { await api.todos.fetch() // Function doesn't return the fetched data }, }) ``` Example of **correct** code for this rule: ```tsx /* eslint "@tanstack/query/no-void-query-fn": "error" */ useQuery({ queryKey: ['todos'], queryFn: async () => { const todos = await api.todos.fetch() return todos }, }) ``` ## Attributes - [x] ✅ Recommended - [ ] 🔧 Fixable ================================================ FILE: docs/eslint/stable-query-client.md ================================================ --- id: stable-query-client title: Stable Query Client --- The QueryClient contains the QueryCache, so you'd only want to create one instance of the QueryClient for the lifecycle of your application - _not_ a new instance on every render. > Exception: It's allowed to create a new QueryClient inside an async Server Component, because the async function is only called once on the server. ## Rule Details Examples of **incorrect** code for this rule: ```tsx /* eslint "@tanstack/query/stable-query-client": "error" */ function App() { const queryClient = new QueryClient() return ( ) } ``` Examples of **correct** code for this rule: ```tsx function App() { const [queryClient] = useState(() => new QueryClient()) return ( ) } ``` ```tsx const queryClient = new QueryClient() function App() { return ( ) } ``` ```tsx async function App() { const queryClient = new QueryClient() await queryClient.prefetchQuery(options) } ``` ## Attributes - [x] ✅ Recommended - [x] 🔧 Fixable ================================================ FILE: docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md ================================================ --- id: Angular-HttpClient-and-other-data-fetching-clients title: Angular HttpClient and other data fetching clients --- Because TanStack Query's fetching mechanisms are agnostically built on Promises, you can use literally any asynchronous data fetching client, including the browser native `fetch` API, `graphql-request`, and more. ## Using Angular's `HttpClient` for data fetching `HttpClient` is a powerful and integrated part of Angular, which gives the following benefits: - Mock responses in unit tests using [provideHttpClientTesting](https://angular.dev/guide/http/testing). - [Interceptors](https://angular.dev/guide/http/interceptors) can be used for a wide range of functionality including adding authentication headers, performing logging, etc. While some data fetching libraries have their own interceptor system, `HttpClient` interceptors are integrated with Angular's dependency injection system. - `HttpClient` automatically informs [`PendingTasks`](https://angular.dev/api/core/PendingTasks#), which enables Angular to be aware of pending requests. Unit tests and SSR can use the resulting application _stableness_ information to wait for pending requests to finish. This makes unit testing much easier for [Zoneless](https://angular.dev/guide/zoneless) applications. - When using SSR, `HttpClient` will [cache requests](https://angular.dev/guide/ssr#caching-data-when-using-HttpClient) performed on the server. This will prevent unneeded requests on the client. `HttpClient` SSR caching works out of the box. TanStack Query has its own hydration functionality which may be more powerful but requires some setup. Which one fits your needs best depends on your use case. ### Using observables in `queryFn` As TanStack Query is a promise based library, observables from `HttpClient` need to be converted to promises. This can be done with the `lastValueFrom` or `firstValueFrom` functions from `rxjs`. ```ts @Component({ // ... }) class ExampleComponent { private readonly http = inject(HttpClient) readonly query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => lastValueFrom( this.http.get('https://api.github.com/repos/tanstack/query'), ), })) } ``` > Since Angular is moving towards RxJS as an optional dependency, it's expected that `HttpClient` will also support promises in the future. > > Support for observables in TanStack Query for Angular is planned. ## Comparison table | Data fetching client | Pros | Cons | | --------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------- | | **Angular HttpClient** | Featureful and very well integrated with Angular. | Observables need to be converted to Promises. | | **Fetch** | Browser native API, so adds nothing to bundle size. | Barebones API which lacks many features. | | **Specialized libraries such as `graphql-request`** | Specialized features for specific use cases. | If it's not an Angular library it won't integrate well with the framework. | ================================================ FILE: docs/framework/angular/devtools.md ================================================ --- id: devtools title: Devtools --- > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > > - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) > - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) > - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) ## Enable devtools The devtools help you debug and inspect your queries and mutations. You can enable the devtools by adding `withDevtools` to `provideTanStackQuery`. By default, Angular Query Devtools are only included in development mode bundles, so you don't need to worry about excluding them during a production build. ```ts import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' export const appConfig: ApplicationConfig = { providers: [provideTanStackQuery(new QueryClient(), withDevtools())], } ``` ## Devtools in production Devtools are automatically excluded from production builds. However, it might be desirable to lazy load the devtools in production. To use `withDevtools` in production builds, import using the `production` sub-path. The function exported from the production subpath is identical to the main one, but won't be excluded from production builds. ```ts import { withDevtools } from '@tanstack/angular-query-experimental/devtools/production' ``` To control when devtools are loaded, you can use the `loadDevtools` option. When not setting the option or setting it to 'auto', the devtools will be loaded automatically only when Angular runs in development mode. ```ts import { withDevtools } from '@tanstack/angular-query-experimental/devtools' provideTanStackQuery(new QueryClient(), withDevtools()) // which is equivalent to provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: 'auto' })), ) ``` When setting the option to true, the devtools will be loaded in both development and production mode. This is useful if you want to load devtools based on [Angular environment configurations](https://angular.dev/tools/cli/environments). E.g. you could set this to true when the application is running on your production build staging environment. ```ts import { environment } from './environments/environment' // Make sure to use the production sub-path to load devtools in production builds import { withDevtools } from '@tanstack/angular-query-experimental/devtools/production' provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: environment.loadDevtools })), ) ``` When setting the option to false, the devtools will not be loaded. ```ts provideTanStackQuery( new QueryClient(), withDevtools(() => ({ loadDevtools: false })), ) ``` ## Derive options through reactivity Options are passed to `withDevtools` from a callback function to support reactivity through signals. In the following example a signal is created from a RxJS observable that emits on a keyboard shortcut. When the derived signal is set to true, the devtools are lazily loaded. The example below always loads devtools in development mode and loads on-demand in production mode when a keyboard shortcut is pressed. ```ts import { Injectable, isDevMode } from '@angular/core' import { fromEvent, map, scan } from 'rxjs' import { toSignal } from '@angular/core/rxjs-interop' @Injectable({ providedIn: 'root' }) export class DevtoolsOptionsManager { loadDevtools = toSignal( fromEvent(document, 'keydown').pipe( map( (event): boolean => event.metaKey && event.ctrlKey && event.shiftKey && event.key === 'D', ), scan((acc, curr) => acc || curr, isDevMode()), ), { initialValue: isDevMode(), }, ) } ``` If you want to use an injectable such as a service in the callback you can use `deps`. The injected value will be passed as parameter to the callback function. This is similar to `deps` in Angular's [`useFactory`](https://angular.dev/guide/di/dependency-injection-providers#factory-providers-usefactory) provider. ```ts // ... // 👇 Note we import from the production sub-path to enable devtools lazy loading in production builds import { withDevtools } from '@tanstack/angular-query-experimental/devtools/production' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), provideTanStackQuery( new QueryClient(), withDevtools( (devToolsOptionsManager: DevtoolsOptionsManager) => ({ loadDevtools: devToolsOptionsManager.loadDevtools(), }), { // `deps` is used to inject and pass `DevtoolsOptionsManager` to the `withDevtools` callback. deps: [DevtoolsOptionsManager], }, ), ), ], } ``` ### Options returned from the callback Of these options `loadDevtools`, `client`, `position`, `errorTypes`, `buttonPosition`, and `initialIsOpen` support reactivity through signals. - `loadDevtools?: 'auto' | boolean` - Defaults to `auto`: lazily loads devtools when in development mode. Skips loading in production mode. - Use this to control if the devtools are loaded. - `initialIsOpen?: Boolean` - Set this to `true` if you want the tools to default to being open - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative"` - Defaults to `bottom-right` - The position of the TanStack logo to open and close the devtools panel - If `relative`, the button is placed in the location that you render the devtools. - `position?: "top" | "bottom" | "left" | "right"` - Defaults to `bottom` - The position of the Angular Query devtools panel - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the QueryClient provided through `provideTanStackQuery` will be injected. - `errorTypes?: { name: string; initializer: (query: Query) => TError}[]` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. - `hideDisabledQueries?: boolean` - Set this to true to hide disabled queries from the devtools panel. ================================================ FILE: docs/framework/angular/guides/background-fetching-indicators.md ================================================ --- id: background-fetching-indicators title: Background Fetching Indicators ref: docs/framework/react/guides/background-fetching-indicators.md replace: { 'useIsFetching': 'injectIsFetching', 'hook': 'function', '@tanstack/react-query': '@tanstack/angular-query-experimental', } --- [//]: # 'Example' ```angular-ts @Component({ selector: 'todos', template: ` @if (todosQuery.isPending()) { Loading... } @else if (todosQuery.isError()) { An error has occurred: {{ todosQuery.error().message }} } @else if (todosQuery.isSuccess()) { @if (todosQuery.isFetching()) { Refreshing... } @for (todos of todosQuery.data(); track todo.id) { } } `, }) class TodosComponent { todosQuery = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, })) } ``` [//]: # 'Example' [//]: # 'Example2' ```angular-ts import { injectIsFetching } from '@tanstack/angular-query-experimental' @Component({ selector: 'global-loading-indicator', template: ` @if (isFetching()) {
Queries are fetching in the background...
} `, }) export class GlobalLoadingIndicatorComponent { isFetching = injectIsFetching() } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/angular/guides/caching.md ================================================ --- id: caching title: Caching Examples --- > Please thoroughly read the [Important Defaults](./important-defaults.md) before reading this guide ## Basic Example This caching example illustrates the story and lifecycle of: - Query Instances with and without cache data - Background Refetching - Inactive Queries - Garbage Collection Let's assume we are using the default `gcTime` of **5 minutes** and the default `staleTime` of `0`. - A new instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` initializes. - Since no other queries have been made with the `['todos']` query key, this query will show a hard loading state and make a network request to fetch the data. - When the network request has completed, the returned data will be cached under the `['todos']` key. - The data will be marked as stale after the configured `staleTime` (defaults to `0`, or immediately). - A second instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` initializes elsewhere. - Since the cache already has data for the `['todos']` key from the first query, that data is immediately returned from the cache. - The new instance triggers a new network request using its query function. - Note that regardless of whether both `fetchTodos` query functions are identical or not, both queries' [`status`](../reference/functions/injectQuery.md) are updated (including `isFetching`, `isPending`, and other related values) because they have the same query key. - When the request completes successfully, the cache's data under the `['todos']` key is updated with the new data, and both instances are updated with the new data. - Both instances of the `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` query are destroyed and no longer in use. - Since there are no more active instances of this query, a garbage collection timeout is set using `gcTime` to delete and garbage collect the query (defaults to **5 minutes**). - Before the cache timeout has completed, another instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` mounts. The query immediately returns the available cached data while the `fetchTodos` function is being run in the background. When it completes successfully, it will populate the cache with fresh data. - The final instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` gets destroyed. - No more instances of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` appear within **5 minutes**. - The cached data under the `['todos']` key is deleted and garbage collected. For more advanced use-cases, see [injectQuery](../reference/functions/injectQuery.md). ================================================ FILE: docs/framework/angular/guides/default-query-function.md ================================================ --- id: default-query-function title: Default Query Function ref: docs/framework/react/guides/default-query-function.md --- [//]: # 'Example' ```ts // Define a default query function that will receive the query key const defaultQueryFn: QueryFunction = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) bootstrapApplication(MyAppComponent, { providers: [provideTanStackQuery(queryClient)], }) export class PostsComponent { // All you have to do now is pass a key! postsQuery = injectQuery>(() => ({ queryKey: ['/posts'], })) // ... } export class PostComponent { // You can even leave out the queryFn and just go straight into options postQuery = injectQuery(() => ({ enabled: this.postIdSignal() > 0, queryKey: [`/posts/${this.postIdSignal()}`], })) // ... } ``` [//]: # 'Example' ================================================ FILE: docs/framework/angular/guides/dependent-queries.md ================================================ --- id: dependent-queries title: Dependent Queries ref: docs/framework/react/guides/dependent-queries.md replace: { 'useQuery': 'injectQuery', 'useQueries': 'injectQueries' } --- [//]: # 'Example' ```ts // Get the user userQuery = injectQuery(() => ({ queryKey: ['user', email], queryFn: getUserByEmail, })) // Then get the user's projects projectsQuery = injectQuery(() => ({ queryKey: ['projects', this.userQuery.data()?.id], queryFn: getProjectsByUser, // The query will not execute until the user id exists enabled: !!this.userQuery.data()?.id, })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts // injectQueries is under development for Angular Query ``` [//]: # 'Example2' ================================================ FILE: docs/framework/angular/guides/disabling-queries.md ================================================ --- id: disabling-queries title: Disabling/Pausing Queries ref: docs/framework/react/guides/disabling-queries.md replace: { 'useQuery': 'injectQuery' } --- [//]: # 'Example' ```angular-ts @Component({ selector: 'todos', template: `
@if (query.data()) {
    @for (todo of query.data(); track todo.id) {
  • {{ todo.title }}
  • }
} @else { @if (query.isError()) { Error: {{ query.error().message }} } @else if (query.isLoading()) { Loading... } @else if (!query.isLoading() && !query.isError()) { Not ready ... } }
{{ query.isLoading() ? 'Fetching...' : '' }}
`, }) export class TodosComponent { query = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, })) } ``` [//]: # 'Example' [//]: # 'Example2' ```angular-ts @Component({ selector: 'todos', template: `
// 🚀 applying the filter will enable and execute the query
`, }) export class TodosComponent { filter = signal('') todosQuery = injectQuery(() => ({ queryKey: ['todos', this.filter()], queryFn: () => fetchTodos(this.filter()), enabled: !!this.filter(), })) } ``` [//]: # 'Example2' [//]: # 'Example3' ```angular-ts import { skipToken, injectQuery } from '@tanstack/query-angular' @Component({ selector: 'todos', template: `
// 🚀 applying the filter will enable and execute the query
`, }) export class TodosComponent { filter = signal('') todosQuery = injectQuery(() => ({ queryKey: ['todos', this.filter()], queryFn: this.filter() ? () => fetchTodos(this.filter()) : skipToken, })) } ``` [//]: # 'Example3' ================================================ FILE: docs/framework/angular/guides/does-this-replace-client-state.md ================================================ --- id: does-this-replace-client-state title: Does TanStack Query replace global state managers? ref: docs/framework/react/guides/does-this-replace-client-state.md replace: { 'useQuery': 'injectQuery', 'useMutation': 'injectMutation', 'hook': 'function', } --- ================================================ FILE: docs/framework/angular/guides/filters.md ================================================ --- id: filters title: Filters ref: docs/framework/react/guides/filters.md --- ================================================ FILE: docs/framework/angular/guides/important-defaults.md ================================================ --- id: important-defaults title: Important Defaults ref: docs/framework/react/guides/important-defaults.md replace: { 'React': 'Angular', 'react-query': 'angular-query', 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery', 'useMemo and useCallback': 'setting signal values', } --- [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/guides/infinite-queries.md ================================================ --- id: infinite-queries title: Infinite Queries ref: docs/framework/react/guides/infinite-queries.md replace: { 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery' } --- [//]: # 'Example' ```angular-ts import { Component, computed, inject } from '@angular/core' import { injectInfiniteQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { ProjectsService } from './projects-service' @Component({ selector: 'example', templateUrl: './example.component.html', }) export class Example { projectsService = inject(ProjectsService) query = injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: async ({ pageParam }) => { return lastValueFrom(this.projectsService.getProjects(pageParam)) }, initialPageParam: 0, getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, maxPages: 3, })) nextButtonDisabled = computed( () => !this.#hasNextPage() || this.#isFetchingNextPage(), ) nextButtonText = computed(() => this.#isFetchingNextPage() ? 'Loading more...' : this.#hasNextPage() ? 'Load newer' : 'Nothing more to load', ) #hasNextPage = this.query.hasNextPage #isFetchingNextPage = this.query.isFetchingNextPage } ``` ```angular-html
@if (query.isPending()) {

Loading...

} @else if (query.isError()) { Error: {{ query?.error().message }} } @else { @for (page of query?.data().pages; track $index) { @for (project of page.data; track project.id) {

{{ project.name }} {{ project.id }}

} }
}
``` [//]: # 'Example' [//]: # 'Example1' ```angular-ts @Component({ template: ` `, }) export class Example { query = injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: async ({ pageParam }) => { return lastValueFrom(this.projectsService.getProjects(pageParam)) }, })) fetchNextPage() { // Do nothing if already fetching if (this.query.isFetching()) return this.query.fetchNextPage() } } ``` [//]: # 'Example1' [//]: # 'Example3' ```ts query = injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```ts query = injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), })) ``` [//]: # 'Example4' [//]: # 'Example8' ```ts injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, })) ``` [//]: # 'Example8' [//]: # 'Example9' ```ts injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, })) ``` [//]: # 'Example9' ================================================ FILE: docs/framework/angular/guides/initial-query-data.md ================================================ --- id: initial-query-data title: Initial Query Data ref: docs/framework/react/guides/initial-query-data.md replace: { 'render': 'service or component instance', ' when it mounts': '', 'after mount': 'after initialization', 'on mount': 'on initialization', } --- [//]: # 'Example' ```ts result = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts // Will show initialTodos immediately, but also immediately refetch todos // when an instance of the component or service is created result = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, })) ``` [//]: # 'Example2' [//]: # 'Example3' ```ts // Show initialTodos immediately, but won't refetch until // another interaction event is encountered after 1000 ms result = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```ts // Show initialTodos immediately, but won't refetch until // another interaction event is encountered after 1000 ms result = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 })) ``` [//]: # 'Example4' [//]: # 'Example5' ```ts result = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), })) ``` [//]: # 'Example5' [//]: # 'Example6' ```ts result = injectQuery(() => ({ queryKey: ['todo', this.todoId()], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return this.queryClient .getQueryData(['todos']) ?.find((d) => d.id === this.todoId()) }, })) ``` [//]: # 'Example6' [//]: # 'Example7' ```ts result = injectQuery(() => ({ queryKey: ['todos', this.todoId()], queryFn: () => fetch(`/todos/${this.todoId()}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, })) ``` [//]: # 'Example7' [//]: # 'Example8' ```ts result = injectQuery(() => ({ queryKey: ['todo', this.todoId()], queryFn: () => fetch(`/todos/${this.todoId()}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === this.todoId()) } // Otherwise, return undefined and let it fetch from a hard loading state! }, })) ``` [//]: # 'Example8' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/guides/invalidations-from-mutations.md ================================================ --- id: invalidations-from-mutations title: Invalidations from Mutations ref: docs/framework/react/guides/invalidations-from-mutations.md replace: { 'useMutation': 'injectMutation', 'hook': 'function' } --- [//]: # 'Example' ```ts mutation = injectMutation(() => ({ mutationFn: postTodo, })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts import { injectMutation, QueryClient, } from '@tanstack/angular-query-experimental' export class TodosComponent { queryClient = inject(QueryClient) // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key mutation = injectMutation(() => ({ mutationFn: addTodo, onSuccess: () => { this.queryClient.invalidateQueries({ queryKey: ['todos'] }) this.queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, })) } ``` [//]: # 'Example2' You can wire up your invalidations to happen using any of the callbacks available in the [`injectMutation` function](./mutations.md) ================================================ FILE: docs/framework/angular/guides/mutation-options.md ================================================ --- id: query-options title: Mutation Options --- One of the best ways to share mutation options between multiple places, is to use the `mutationOptions` helper. At runtime, this helper just returns whatever you pass into it, but it has a lot of advantages when using it [with TypeScript](../typescript.md#typing-query-options). You can define all possible options for a mutation in one place, and you'll also get type inference and type safety for all of them. ```ts export class QueriesService { private http = inject(HttpClient) updatePost(id: number) { return mutationOptions({ mutationFn: (post: Post) => Promise.resolve(post), mutationKey: ['updatePost', id], onSuccess: (newPost) => { // ^? newPost: Post this.queryClient.setQueryData(['posts', id], newPost) }, }) } } ``` ================================================ FILE: docs/framework/angular/guides/mutations.md ================================================ --- id: mutations title: Mutations ref: docs/framework/react/guides/mutations.md replace: { 'useMutation': 'injectMutation', 'hook': 'function', 'still mounted': 'still active', 'unmounts': 'gets destroyed', 'mounted': 'initialized', } --- [//]: # 'Example' ```angular-ts @Component({ template: `
@if (mutation.isPending()) { Adding todo... } @else if (mutation.isError()) {
An error occurred: {{ mutation.error()?.message }}
} @else if (mutation.isSuccess()) {
Todo added!
}
`, }) export class TodosComponent { todoService = inject(TodoService) mutation = injectMutation(() => ({ mutationFn: (todoId: number) => lastValueFrom(this.todoService.create(todoId)), })) } ``` [//]: # 'Example' [//]: # 'Info1' [//]: # 'Info1' [//]: # 'Example2' [//]: # 'Example2' [//]: # 'Example3' ```angular-ts @Component({ selector: 'todo-item', imports: [ReactiveFormsModule], template: `
@if (mutation.error()) {
{{ mutation.error() }}
}
`, }) export class TodosComponent { mutation = injectMutation(() => ({ mutationFn: createTodo, })) fb = inject(NonNullableFormBuilder) todoForm = this.fb.group({ title: this.fb.control('', { validators: [Validators.required], }), }) title = toSignal(this.todoForm.controls.title.valueChanges, { initialValue: '', }) onCreateTodo = () => { this.mutation.mutate(this.title()) } } ``` [//]: # 'Example3' [//]: # 'Example4' ```ts mutation = injectMutation(() => ({ mutationFn: addTodo, onMutate: (variables, context) => { // A mutation is about to happen! // Optionally return a result containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, onMutateResult, context) => { // An error happened! console.log(`rolling back optimistic update with id ${onMutateResult.id}`) }, onSuccess: (data, variables, onMutateResult, context) => { // Boom baby! }, onSettled: (data, error, variables, onMutateResult, context) => { // Error or success... doesn't matter! }, })) ``` [//]: # 'Example4' [//]: # 'Example5' ```ts mutation = injectMutation(() => ({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, })) ``` [//]: # 'Example5' [//]: # 'Example6' ```ts mutation = injectMutation(() => ({ mutationFn: addTodo, onSuccess: (data, variables, onMutateResult, context) => { // I will fire first }, onError: (error, variables, onMutateResult, context) => { // I will fire first }, onSettled: (data, error, variables, onMutateResult, context) => { // I will fire first }, })) mutation.mutate(todo, { onSuccess: (data, variables, onMutateResult, context) => { // I will fire second! }, onError: (error, variables, onMutateResult, context) => { // I will fire second! }, onSettled: (data, error, variables, onMutateResult, context) => { // I will fire second! }, }) ``` [//]: # 'Example6' [//]: # 'Example7' ```ts export class Example { mutation = injectMutation(() => ({ mutationFn: addTodo, onSuccess: (data, variables, onMutateResult, context) => { // Will be called 3 times }, })) doMutations() { ;['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => { this.mutation.mutate(todo, { onSuccess: (data, variables, onMutateResult, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) } } ``` [//]: # 'Example7' [//]: # 'Example8' ```ts mutation = injectMutation(() => ({ mutationFn: addTodo })) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } ``` [//]: # 'Example8' [//]: # 'Example9' ```ts mutation = injectMutation(() => ({ mutationFn: addTodo, retry: 3, })) ``` [//]: # 'Example9' [//]: # 'Example10' ```ts const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables, context) => { // Cancel current queries for the todos list await context.client.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return result with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, onMutateResult, context) => { // Replace optimistic todo in the todos list with the result context.client.setQueryData(['todos'], (old) => old.map((todo) => todo.id === onMutateResult.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, onMutateResult, context) => { // Remove optimistic todo from the todos list context.client.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id), ) }, retry: 3, }) class someComponent { // Start mutation in some component: mutation = injectMutation(() => ({ mutationKey: ['addTodo'] })) someMethod() { mutation.mutate({ title: 'title' }) } } // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() ``` [//]: # 'Example10' [//]: # 'Example11' [//]: # 'Example11' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/guides/network-mode.md ================================================ --- id: network-mode title: Network Mode ref: docs/framework/react/guides/network-mode.md --- ================================================ FILE: docs/framework/angular/guides/optimistic-updates.md ================================================ --- id: optimistic-updates title: Optimistic Updates ref: docs/framework/react/guides/optimistic-updates.md replace: { 'React': 'Angular', 'useMutation': 'injectMutation', 'hook': 'function', 'useMutationState': 'injectMutationState', 'addTodoMutation': 'addTodo', } --- [//]: # 'ExampleUI1' ```ts addTodo = injectMutation(() => ({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: async () => { return await queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })) ``` [//]: # 'ExampleUI1' [//]: # 'ExampleUI2' ```angular-ts @Component({ template: ` @for (todo of todos.data(); track todo.id) {
  • {{ todo.title }}
  • } @if (addTodo.isPending()) {
  • {{ addTodo.variables() }}
  • } `, }) class TodosComponent {} ``` [//]: # 'ExampleUI2' [//]: # 'ExampleUI3' ```angular-ts @Component({ template: ` @if (addTodo.isError()) {
  • {{ addTodo.variables() }}
  • } `, }) class TodosComponent {} ``` [//]: # 'ExampleUI3' [//]: # 'ExampleUI4' ```ts // somewhere in your app addTodo = injectMutation(() => ({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], })) // access variables somewhere else mutationState = injectMutationState(() => ({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, })) ``` [//]: # 'ExampleUI4' [//]: # 'Example' ```ts queryClient = inject(QueryClient) updateTodo = injectMutation(() => ({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await context.client.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = context.client.getQueryData(['todos']) // Optimistically update to the new value context.client.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a result object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the result returned from onMutate to roll back onError: (err, newTodo, onMutateResult, context) => { context.client.setQueryData(['todos'], onMutateResult.previousTodos) }, // Always refetch after error or success: onSettled: (data, error, variables, onMutateResult, context) => { context.client.invalidateQueries({ queryKey: ['todos'] }) }, })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts queryClient = inject(QueryClient) updateTodo = injectMutation(() => ({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = context.client.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value context.client.setQueryData(['todos', newTodo.id], newTodo) // Return a result with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the result we returned above onError: (err, newTodo, onMutateResult, context) => { context.client.setQueryData( ['todos', onMutateResult.newTodo.id], onMutateResult.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo, error, variables, onMutateResult, context) => { context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }) }, })) ``` [//]: # 'Example2' [//]: # 'Example3' ```ts injectMutation({ mutationFn: updateTodo, // ... onSettled: (newTodo, error, variables, onMutateResult, context) => { if (error) { // do something } }, }) ``` [//]: # 'Example3' ================================================ FILE: docs/framework/angular/guides/paginated-queries.md ================================================ --- id: paginated-queries title: Paginated / Lagged Queries ref: docs/framework/react/guides/paginated-queries.md replace: { 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery', 'hook': 'function', } --- [//]: # 'Example' ```ts const result = injectQuery(() => ({ queryKey: ['projects', page()], queryFn: fetchProjects, })) ``` [//]: # 'Example' [//]: # 'Example2' ```angular-ts @Component({ selector: 'pagination-example', template: `

    In this example, each page of data remains visible as the next page is fetched. The buttons and capability to proceed to the next page are also suppressed until the next page cursor is known. Each page is cached as a normal query too, so when going to previous pages, you'll see them instantaneously while they are also re-fetched invisibly in the background.

    @if (query.status() === 'pending') {
    Loading...
    } @else if (query.status() === 'error') {
    Error: {{ query.error().message }}
    } @else {
    @for (project of query.data().projects; track project.id) {

    {{ project.name }}

    }
    }
    Current Page: {{ page() + 1 }}
    @if (query.isFetching()) { Loading... }
    `, }) export class PaginationExampleComponent { page = signal(0) #queryClient = inject(QueryClient) query = injectQuery(() => ({ queryKey: ['projects', this.page()], queryFn: () => lastValueFrom(fetchProjects(this.page())), placeholderData: keepPreviousData, staleTime: 5000, })) constructor() { effect(() => { // Prefetch the next page! if (!this.query.isPlaceholderData() && this.query.data()?.hasMore) { this.#queryClient.prefetchQuery({ queryKey: ['projects', this.page() + 1], queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)), }) } }) } previousPage() { this.page.update((old) => Math.max(old - 1, 0)) } nextPage() { this.page.update((old) => (this.query.data()?.hasMore ? old + 1 : old)) } } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/angular/guides/parallel-queries.md ================================================ --- id: parallel-queries title: Parallel Queries ref: docs/framework/react/guides/parallel-queries.md replace: { 'If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, ': '', 'hook': 'function', 'React': 'Angular', 'hooks': 'functions', 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery', 'useQueries': 'injectQueries', } --- [//]: # 'Example' ```ts export class AppComponent { // The following queries will execute in parallel usersQuery = injectQuery(() => ({ queryKey: ['users'], queryFn: fetchUsers })) teamsQuery = injectQuery(() => ({ queryKey: ['teams'], queryFn: fetchTeams })) projectsQuery = injectQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, })) } ``` [//]: # 'Example' [//]: # 'Info' [//]: # 'Info' [//]: # 'DynamicParallelIntro' TanStack Query provides `injectQueries`, which you can use to dynamically execute as many queries in parallel as you'd like. [//]: # 'DynamicParallelIntro' [//]: # 'Example2' ```ts export class AppComponent { users = signal>([]) // Please note injectQueries is under development and this code does not work yet userQueries = injectQueries(() => ({ queries: users().map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), })) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/angular/guides/placeholder-query-data.md ================================================ --- id: placeholder-query-data title: Placeholder Query Data ref: docs/framework/react/guides/placeholder-query-data.md --- [//]: # 'ExampleValue' ```ts class TodosComponent { result = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, })) } ``` [//]: # 'ExampleValue' [//]: # 'Memoization' [//]: # 'Memoization' [//]: # 'ExampleFunction' ```ts class TodosComponent { result = injectQuery(() => ({ queryKey: ['todos', id()], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, })) } ``` [//]: # 'ExampleFunction' [//]: # 'ExampleCache' ```ts export class BlogPostComponent { postId = input.required() queryClient = inject(QueryClient) result = injectQuery(() => ({ queryKey: ['blogPost', this.postId()], queryFn: () => fetch(`/blogPosts/${this.postId()}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return this.queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === this.postId()) }, })) } ``` [//]: # 'ExampleCache' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/guides/queries.md ================================================ --- id: queries title: Queries ref: docs/framework/react/guides/queries.md replace: { 'React': 'Angular', 'react-query': 'angular-query', 'promise': 'promise or observable', 'custom hooks': 'services', 'the `useQuery` hook': '`injectQuery`', '`useQuery`': '`injectQuery`', "TypeScript will also narrow the type of data correctly if you've checked for pending and error before accessing it.": 'TypeScript will only narrow the type when checking boolean signals such as `isPending` and `isError`.', } --- [//]: # 'Example' ```ts import { injectQuery } from '@tanstack/angular-query-experimental' export class TodosComponent { info = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList })) } ``` [//]: # 'Example' [//]: # 'Example2' ```ts result = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList })) ``` [//]: # 'Example2' [//]: # 'Example3' ```angular-ts @Component({ selector: 'todos', template: ` @if (todos.isPending()) { Loading... } @else if (todos.isError()) { Error: {{ todos.error()?.message }} } @else { @for (todo of todos.data(); track todo.id) {
  • {{ todo.title }}
  • } @empty {
  • No todos found
  • } } `, }) export class PostsComponent { todos = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) } ``` [//]: # 'Example3' If booleans aren't your thing, you can always use the `status` state as well: [//]: # 'Example4' ```angular-ts @Component({ selector: 'todos', template: ` @switch (todos.status()) { @case ('pending') { Loading... } @case ('error') { Error: {{ todos.error()?.message }} } @default {
      @for (todo of todos.data(); track todo.id) {
    • {{ todo.title }}
    • } @empty {
    • No todos found
    • }
    } } `, }) class TodosComponent {} ``` [//]: # 'Example4' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/guides/query-cancellation.md ================================================ --- id: query-cancellation title: Query Cancellation --- TanStack Query provides each query function with an [`AbortSignal` instance](https://developer.mozilla.org/docs/Web/API/AbortSignal). When a query becomes out-of-date or inactive, this `signal` will become aborted. This means that all queries are cancellable, and you can respond to the cancellation inside your query function if desired. The best part about this is that it allows you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation. ## Default behavior By default, queries that unmount or become unused before their promises are resolved are _not_ cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available. However, if you consume the `AbortSignal`, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being _reverted_ to its previous state. ## Using `HttpClient` ```ts import { HttpClient } from '@angular/common/http' import { injectQuery } from '@tanstack/angular-query-experimental' postQuery = injectQuery(() => ({ enabled: this.postId() > 0, queryKey: ['post', this.postId()], queryFn: async (context): Promise => { const abort$ = fromEvent(context.signal, 'abort') return lastValueFrom(this.getPost$(this.postId()).pipe(takeUntil(abort$))) }, })) ``` ## Using `fetch` [//]: # 'Example2' ```ts query = injectQuery(() => ({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, })) ``` [//]: # 'Example2' ## Using `axios` [//]: # 'Example3' ```ts import axios from 'axios' const query = injectQuery(() => ({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), })) ``` [//]: # 'Example3' ## Manual Cancellation You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `queryClient.cancelQueries({ queryKey })`, which will cancel the query and revert it back to its previous state. If you have consumed the `signal` passed to the query function, TanStack Query will additionally also cancel the Promise. [//]: # 'Example7' ```angular-ts @Component({ template: ``, }) export class TodosComponent { query = injectQuery(() => ({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, })) queryClient = inject(QueryClient) onCancel() { this.queryClient.cancelQueries(['todos']) } } ``` [//]: # 'Example7' ================================================ FILE: docs/framework/angular/guides/query-functions.md ================================================ --- id: query-functions title: Query Functions ref: docs/framework/react/guides/query-functions.md --- [//]: # 'Example' ```ts injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, })) injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts todos = injectQuery(() => ({ queryKey: ['todos', todoId()], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, })) ``` [//]: # 'Example2' [//]: # 'Example3' ```ts todos = injectQuery(() => ({ queryKey: ['todos', todoId()], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```ts result = injectQuery(() => ({ queryKey: ['todos', { status: status(), page: page() }], queryFn: fetchTodoList, })) // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ``` [//]: # 'Example4' ================================================ FILE: docs/framework/angular/guides/query-invalidation.md ================================================ --- id: query-invalidation title: Query Invalidation ref: docs/framework/react/guides/query-invalidation.md replace: { 'useQuery': 'injectQuery', 'hooks': 'functions' } --- [//]: # 'Example2' ```ts import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental' class QueryInvalidationExample { queryClient = inject(QueryClient) invalidateQueries() { this.queryClient.invalidateQueries({ queryKey: ['todos'] }) } // Both queries below will be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) todoListQuery = injectQuery(() => ({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, })) } ``` [//]: # 'Example2' You can even invalidate queries with specific variables by passing a more specific query key to the `invalidateQueries` method: [//]: # 'Example3' ```ts queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, })) // However, the following query below will NOT be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) ``` [//]: # 'Example3' The `invalidateQueries` API is very flexible, so even if you want to **only** invalidate `todos` queries that don't have any more variables or subkeys, you can pass an `exact: true` option to the `invalidateQueries` method: [//]: # 'Example4' ```ts queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) // However, the following query below will NOT be invalidated const todoListQuery = injectQuery(() => ({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, })) ``` [//]: # 'Example4' If you find yourself wanting **even more** granularity, you can pass a predicate function to the `invalidateQueries` method. This function will receive each `Query` instance from the query cache and allow you to return `true` or `false` for whether you want to invalidate that query: [//]: # 'Example5' ```ts queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, })) // The query below will be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, })) // However, the following query below will NOT be invalidated todoListQuery = injectQuery(() => ({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, })) ``` [//]: # 'Example5' ================================================ FILE: docs/framework/angular/guides/query-keys.md ================================================ --- id: query-keys title: Query Keys ref: docs/framework/react/guides/query-keys.md #todo: exhaustive-deps is at least for now React-only --- [//]: # 'Example' ```ts // A list of todos injectQuery(() => ({ queryKey: ['todos'], ... })) // Something else, whatever! injectQuery(() => ({ queryKey: ['something', 'special'], ... })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts // An individual todo injectQuery(() => ({queryKey: ['todo', 5], ...})) // An individual todo in a "preview" format injectQuery(() => ({queryKey: ['todo', 5, {preview: true}], ...})) // A list of todos that are "done" injectQuery(() => ({queryKey: ['todos', {type: 'done'}], ...})) ``` [//]: # 'Example2' [//]: # 'Example3' ```ts injectQuery(() => ({ queryKey: ['todos', { status, page }], ... })) injectQuery(() => ({ queryKey: ['todos', { page, status }], ...})) injectQuery(() => ({ queryKey: ['todos', { page, status, other: undefined }], ... })) ``` [//]: # 'Example3' [//]: # 'Example4' ```ts injectQuery(() => ({ queryKey: ['todos', status, page], ... })) injectQuery(() => ({ queryKey: ['todos', page, status], ...})) injectQuery(() => ({ queryKey: ['todos', undefined, page, status], ...})) ``` [//]: # 'Example4' [//]: # 'Example5' ```ts todoId = signal(-1) injectQuery(() => ({ enabled: todoId() > 0, queryKey: ['todos', todoId()], queryFn: () => fetchTodoById(todoId()), })) ``` [//]: # 'Example5' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/guides/query-options.md ================================================ --- id: query-options title: Query Options ref: docs/framework/react/guides/query-options.md --- [//]: # 'Example1' ```ts import { queryOptions } from '@tanstack/angular-query-experimental' @Injectable({ providedIn: 'root', }) export class QueriesService { private http = inject(HttpClient) post(postId: number) { return queryOptions({ queryKey: ['post', postId], queryFn: () => { return lastValueFrom( this.http.get( `https://jsonplaceholder.typicode.com/posts/${postId}`, ), ) }, }) } } // usage: postId = input.required({ transform: numberAttribute, }) queries = inject(QueriesService) postQuery = injectQuery(() => this.queries.post(this.postId())) queryClient.prefetchQuery(this.queries.post(23)) queryClient.setQueryData(this.queries.post(42).queryKey, newPost) ``` [//]: # 'Example1' [//]: # 'Example2' ```ts // Type inference still works, so query.data will be the return type of select instead of queryFn queries = inject(QueriesService) query = injectQuery(() => ({ ...groupOptions(1), select: (data) => data.title, })) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/angular/guides/query-retries.md ================================================ --- id: query-retries title: Query Retries ref: docs/framework/react/guides/query-retries.md replace: { 'Provider': 'Plugin', 'useQuery': 'injectQuery', 'useMutation': 'injectMutation', } --- [//]: # 'Info' [//]: # 'Info' [//]: # 'Example' ```ts import { injectQuery } from '@tanstack/angular-query-experimental' // Make a specific query retry a certain number of times const result = injectQuery(() => ({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error })) ``` [//]: # 'Example' [//]: # 'Example2' ```ts // Configure for all queries import { QueryCache, QueryClient, QueryClientProvider, } from '@tanstack/angular-query-experimental' const queryClient = new QueryClient({ defaultOptions: { queries: { retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }, }, }) bootstrapApplication(AppComponent, { providers: [provideTanStackQuery(queryClient)], }) ``` [//]: # 'Example2' Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: [//]: # 'Example3' ```ts const result = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries })) ``` [//]: # 'Example3' ================================================ FILE: docs/framework/angular/guides/scroll-restoration.md ================================================ --- id: scroll-restoration title: Scroll Restoration ref: docs/framework/react/guides/scroll-restoration.md --- ================================================ FILE: docs/framework/angular/guides/testing.md ================================================ --- id: testing title: Testing --- Most Angular tests using TanStack Query will involve services or components that call `injectQuery`/`injectMutation`. TanStack Query's `inject*` functions integrate with [`PendingTasks`](https://angular.dev/api/core/PendingTasks) which ensures the framework is aware of in-progress queries and mutations. This means tests and SSR can wait until mutations and queries resolve. In unit tests you can use `ApplicationRef.whenStable()` or `fixture.whenStable()` to await query completion. This works for both Zone.js and Zoneless setups. > This integration requires Angular 19 or later. Earlier versions of Angular do not support `PendingTasks`. ## TestBed setup Create a fresh `QueryClient` for every spec and provide it with `provideTanStackQuery` or `provideQueryClient`. This keeps caches isolated and lets you change default options per test: ```ts const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, // ✅ faster failure tests }, }, }) TestBed.configureTestingModule({ providers: [provideTanStackQuery(queryClient)], }) ``` > If your applications actual TanStack Query config is used in unit tests, make sure `withDevtools` is not accidentally included in test providers. This can cause slow tests. It is best to keep test and production configs separate. If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another. ## First query test Query tests typically run inside `TestBed.runInInjectionContext`, then wait for stability: ```ts const appRef = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['greeting'], queryFn: () => 'Hello', })), ) TestBed.tick() // Trigger effect // Application is stable when queries are idle await appRef.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('Hello') ``` PendingTasks will have `whenStable()` resolve after the query settles. When using fake timers (Vitest), advance the clock and a microtask before awaiting stability: ```ts await vi.advanceTimersByTimeAsync(0) await Promise.resolve() await appRef.whenStable() ``` ## Testing components For components, bootstrap them through `TestBed.createComponent`, then await `fixture.whenStable()`: ```ts const fixture = TestBed.createComponent(ExampleComponent) await fixture.whenStable() expect(fixture.componentInstance.query.data()).toEqual({ value: 42 }) ``` ## Handling retries Retries slow failing tests because the default backoff runs three times. Set `retry: false` (or a specific number) through `defaultOptions` or per query to keep tests fast. If a query intentionally retries, assert on the final state rather than intermediate counts. ## HttpClient & network stubs Angular's `HttpClientTestingModule` plays nicely with PendingTasks. Register it alongside the Query provider and flush responses through `HttpTestingController`: ```ts TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [provideTanStackQuery(queryClient)], }) const httpCtrl = TestBed.inject(HttpTestingController) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ queryKey: ['todos'], queryFn: () => lastValueFrom(TestBed.inject(HttpClient).get('/api/todos')), })), ) const fixturePromise = TestBed.inject(ApplicationRef).whenStable() httpCtrl.expectOne('/api/todos').flush([{ id: 1 }]) await fixturePromise expect(query.data()).toEqual([{ id: 1 }]) httpCtrl.verify() ``` ## Infinite queries & pagination Use the same pattern for infinite queries: call `fetchNextPage()`, advance timers if you are faking time, then await stability and assert on `data().pages`. ```ts const infinite = TestBed.runInInjectionContext(() => injectInfiniteQuery(() => ({ queryKey: ['pages'], queryFn: ({ pageParam = 1 }) => fetchPage(pageParam), getNextPageParam: (last, all) => all.length + 1, })), ) await appRef.whenStable() expect(infinite.data().pages).toHaveLength(1) await infinite.fetchNextPage() await vi.advanceTimersByTimeAsync(0) await appRef.whenStable() expect(infinite.data().pages).toHaveLength(2) ``` ## Mutations and optimistic updates ```ts const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ mutationFn: async (input: string) => input.toUpperCase(), })), ) mutation.mutate('test') // Trigger effect TestBed.tick() await appRef.whenStable() expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('TEST') ``` ## Quick checklist - Fresh `QueryClient` per test (and clear it afterwards) - Disable or control retries to avoid timeouts - Advance timers + microtasks before `whenStable()` when using fake timers - Use `HttpClientTestingModule` or your preferred mock to assert network calls - Await `whenStable()` after every `refetch`, `fetchNextPage`, or mutation - Prefer `TestBed.runInInjectionContext` for service tests and `fixture.whenStable()` for component tests ================================================ FILE: docs/framework/angular/guides/window-focus-refetching.md ================================================ --- id: window-focus-refetching title: Window Focus Refetching ref: docs/framework/react/guides/window-focus-refetching.md replace: { '@tanstack/react-query': '@tanstack/angular-query-experimental' } --- [//]: # 'Example' ```ts export const appConfig: ApplicationConfig = { providers: [ provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }), ), ], } ``` [//]: # 'Example' [//]: # 'Example2' ```ts injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, })) ``` [//]: # 'Example2' [//]: # 'ReactNative' [//]: # 'ReactNative' ================================================ FILE: docs/framework/angular/installation.md ================================================ --- id: installation title: Installation --- > IMPORTANT: This library is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Upgrade carefully. If you use this in production while in experimental stage, please lock your version to a patch-level version to avoid unexpected breaking changes. ### NPM _Angular Query is compatible with Angular v16 and higher_ ```bash npm i @tanstack/angular-query-experimental ``` or ```bash pnpm add @tanstack/angular-query-experimental ``` or ```bash yarn add @tanstack/angular-query-experimental ``` or ```bash bun add @tanstack/angular-query-experimental ``` > Wanna give it a spin before you download? Try out the [simple](./examples/simple) or [basic](./examples/basic) examples! ================================================ FILE: docs/framework/angular/overview.md ================================================ --- id: overview title: Overview --- > IMPORTANT: This library is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Upgrade carefully. If you use this in production while in experimental stage, please lock your version to a patch-level version to avoid unexpected breaking changes. The `@tanstack/angular-query-experimental` package offers a 1st-class API for using TanStack Query via Angular. ## Feedback welcome! We are in the process of getting to a stable API for TanStack Query on Angular. If you have any feedback, please contact us at the [TanStack Discord](https://tlinz.com/discord) server or [visit this discussion](https://github.com/TanStack/query/discussions/6293) on Github. ## Supported Angular Versions TanStack Query is compatible with Angular v16 and higher. TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze. ## Motivation Most core web frameworks **do not** come with an opinionated way of fetching or updating data in a holistic way. Because of this developers end up building either meta-frameworks which encapsulate strict opinions about data-fetching, or they invent their own ways of fetching data. This usually means cobbling together component-based state and side-effects, or using more general purpose state management libraries to store and provide asynchronous data throughout their apps. While most traditional state management libraries are great for working with client state, they are **not so great at working with async or server state**. This is because **server state is totally different**. For starters, server state: - Is persisted remotely in a location you may not control or own - Requires asynchronous APIs for fetching and updating - Implies shared ownership and can be changed by other people without your knowledge - Can potentially become "out of date" in your applications if you're not careful Once you grasp the nature of server state in your application, **even more challenges will arise** as you go, for example: - Caching... (possibly the hardest thing to do in programming) - Deduping multiple requests for the same data into a single request - Updating "out of date" data in the background - Knowing when data is "out of date" - Reflecting updates to data as quickly as possible - Performance optimizations like pagination and lazy loading data - Managing memory and garbage collection of server state - Memoizing query results with structural sharing If you're not overwhelmed by that list, then that must mean that you've probably solved all of your server state problems already and deserve an award. However, if you are like a vast majority of people, you either have yet to tackle all or most of these challenges and we're only scratching the surface! TanStack Query is hands down one of the _best_ libraries for managing server state. It works amazingly well **out-of-the-box, with zero-config, and can be customized** to your liking as your application grows. TanStack Query allows you to defeat and overcome the tricky challenges and hurdles of _server state_ and control your app data before it starts to control you. On a more technical note, TanStack Query will likely: - Help you remove **many** lines of complicated and misunderstood code from your application and replace with just a handful of lines of Angular Query logic. - Make your application more maintainable and easier to build new features without worrying about wiring up new server state data sources - Have a direct impact on your end-users by making your application feel faster and more responsive than ever before. - Potentially help you save on bandwidth and increase memory performance [//]: # 'Example' ## Enough talk, show me some code already! In the example below, you can see TanStack Query in its most basic and simple form being used to fetch the GitHub stats for the TanStack Query GitHub project itself: [Open in StackBlitz](https://stackblitz.com/github/TanStack/query/tree/main/examples/angular/simple) ```angular-ts import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { HttpClient } from '@angular/common/http' import { CommonModule } from '@angular/common' import { injectQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'simple-example', template: ` @if (query.isPending()) { Loading... } @if (query.error()) { An error has occurred: {{ query.error().message }} } @if (query.data(); as data) {

    {{ data.name }}

    {{ data.description }}

    👀 {{ data.subscribers_count }} ✨ {{ data.stargazers_count }} 🍴 {{ data.forks_count }} } `, }) export class SimpleExampleComponent { http = inject(HttpClient) query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => lastValueFrom( this.http.get('https://api.github.com/repos/tanstack/query'), ), })) } interface Response { name: string description: string subscribers_count: number stargazers_count: number forks_count: number } ``` ## You talked me into it, so what now? - Learn TanStack Query at your own pace with our amazingly thorough [Walkthrough Guide](./installation.md) and [API Reference](./reference/functions/injectQuery.md) ================================================ FILE: docs/framework/angular/quick-start.md ================================================ --- id: quick-start title: Quick Start --- > IMPORTANT: This library is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Upgrade carefully. If you use this in production while in experimental stage, please lock your version to a patch-level version to avoid unexpected breaking changes. [//]: # 'Example' If you're looking for a fully functioning example, please have a look at our [basic codesandbox example](./examples/basic) ### Provide the client to your App ```ts import { provideHttpClient } from '@angular/common/http' import { provideTanStackQuery, QueryClient, } from '@tanstack/angular-query-experimental' bootstrapApplication(AppComponent, { providers: [provideHttpClient(), provideTanStackQuery(new QueryClient())], }) ``` or in a NgModule-based app ```ts import { provideHttpClient } from '@angular/common/http' import { provideTanStackQuery, QueryClient, } from '@tanstack/angular-query-experimental' @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [provideTanStackQuery(new QueryClient())], bootstrap: [AppComponent], }) export class AppModule {} ``` ### Component with query and mutation ```angular-ts import { Component, Injectable, inject } from '@angular/core' import { HttpClient } from '@angular/common/http' import { lastValueFrom } from 'rxjs' import { injectMutation, injectQuery, QueryClient, } from '@tanstack/angular-query-experimental' @Component({ template: `
      @for (todo of query.data(); track todo.title) {
    • {{ todo.title }}
    • }
    `, }) export class TodosComponent { todoService = inject(TodoService) queryClient = inject(QueryClient) query = injectQuery(() => ({ queryKey: ['todos'], queryFn: () => this.todoService.getTodos(), })) mutation = injectMutation(() => ({ mutationFn: (todo: Todo) => this.todoService.addTodo(todo), onSuccess: () => { this.queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })) onAddTodo() { this.mutation.mutate({ id: Date.now().toString(), title: 'Do Laundry', }) } } @Injectable({ providedIn: 'root' }) export class TodoService { private http = inject(HttpClient) getTodos(): Promise { return lastValueFrom( this.http.get('https://jsonplaceholder.typicode.com/todos'), ) } addTodo(todo: Todo): Promise { return lastValueFrom( this.http.post('https://jsonplaceholder.typicode.com/todos', todo), ) } } interface Todo { id: string title: string } ``` [//]: # 'Example' ================================================ FILE: docs/framework/angular/reference/functions/infiniteQueryOptions.md ================================================ --- id: infiniteQueryOptions title: infiniteQueryOptions --- # Function: infiniteQueryOptions() Allows to share and re-use infinite query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. ## Param The infinite query options to tag with the type from `queryFn`. ## Call Signature ```ts function infiniteQueryOptions(options): CreateInfiniteQueryOptions & object & object; ``` Defined in: [infinite-query-options.ts:88](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/infinite-query-options.ts#L88) Allows to share and re-use infinite query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> The infinite query options to tag with the type from `queryFn`. ### Returns [`CreateInfiniteQueryOptions`](../interfaces/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> & `object` & `object` The tagged infinite query options. ## Call Signature ```ts function infiniteQueryOptions(options): OmitKeyof, "queryFn"> & object & object; ``` Defined in: [infinite-query-options.ts:119](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/infinite-query-options.ts#L119) Allows to share and re-use infinite query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`UnusedSkipTokenInfiniteOptions`](../type-aliases/UnusedSkipTokenInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> The infinite query options to tag with the type from `queryFn`. ### Returns `OmitKeyof`\<[`CreateInfiniteQueryOptions`](../interfaces/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"queryFn"`\> & `object` & `object` The tagged infinite query options. ## Call Signature ```ts function infiniteQueryOptions(options): CreateInfiniteQueryOptions & object & object; ``` Defined in: [infinite-query-options.ts:150](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/infinite-query-options.ts#L150) Allows to share and re-use infinite query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> The infinite query options to tag with the type from `queryFn`. ### Returns [`CreateInfiniteQueryOptions`](../interfaces/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> & `object` & `object` The tagged infinite query options. ================================================ FILE: docs/framework/angular/reference/functions/injectInfiniteQuery.md ================================================ --- id: injectInfiniteQuery title: injectInfiniteQuery --- # Function: injectInfiniteQuery() Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" ## Param A function that returns infinite query options. ## Param Additional configuration. ## Call Signature ```ts function injectInfiniteQuery(injectInfiniteQueryFn, options?): DefinedCreateInfiniteQueryResult; ``` Defined in: [inject-infinite-query.ts:41](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L41) Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### injectInfiniteQueryFn () => [`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> A function that returns infinite query options. #### options? [`InjectInfiniteQueryOptions`](../interfaces/InjectInfiniteQueryOptions.md) Additional configuration. ### Returns [`DefinedCreateInfiniteQueryResult`](../type-aliases/DefinedCreateInfiniteQueryResult.md)\<`TData`, `TError`\> The infinite query result. ## Call Signature ```ts function injectInfiniteQuery(injectInfiniteQueryFn, options?): CreateInfiniteQueryResult; ``` Defined in: [inject-infinite-query.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L65) Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### injectInfiniteQueryFn () => [`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> A function that returns infinite query options. #### options? [`InjectInfiniteQueryOptions`](../interfaces/InjectInfiniteQueryOptions.md) Additional configuration. ### Returns [`CreateInfiniteQueryResult`](../type-aliases/CreateInfiniteQueryResult.md)\<`TData`, `TError`\> The infinite query result. ## Call Signature ```ts function injectInfiniteQuery(injectInfiniteQueryFn, options?): CreateInfiniteQueryResult; ``` Defined in: [inject-infinite-query.ts:89](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L89) Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### injectInfiniteQueryFn () => [`CreateInfiniteQueryOptions`](../interfaces/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> A function that returns infinite query options. #### options? [`InjectInfiniteQueryOptions`](../interfaces/InjectInfiniteQueryOptions.md) Additional configuration. ### Returns [`CreateInfiniteQueryResult`](../type-aliases/CreateInfiniteQueryResult.md)\<`TData`, `TError`\> The infinite query result. ================================================ FILE: docs/framework/angular/reference/functions/injectIsFetching.md ================================================ --- id: injectIsFetching title: injectIsFetching --- # Function: injectIsFetching() ```ts function injectIsFetching(filters?, options?): Signal; ``` Defined in: [inject-is-fetching.ts:31](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-fetching.ts#L31) Injects a signal that tracks the number of queries that your application is loading or fetching in the background. Can be used for app-wide loading indicators ## Parameters ### filters? `QueryFilters`\ The filters to apply to the query. ### options? [`InjectIsFetchingOptions`](../interfaces/InjectIsFetchingOptions.md) Additional configuration ## Returns `Signal`\<`number`\> signal with number of loading or fetching queries. ================================================ FILE: docs/framework/angular/reference/functions/injectIsMutating.md ================================================ --- id: injectIsMutating title: injectIsMutating --- # Function: injectIsMutating() ```ts function injectIsMutating(filters?, options?): Signal; ``` Defined in: [inject-is-mutating.ts:30](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-mutating.ts#L30) Injects a signal that tracks the number of mutations that your application is fetching. Can be used for app-wide loading indicators ## Parameters ### filters? `MutationFilters`\<`unknown`, `Error`, `unknown`, `unknown`\> The filters to apply to the query. ### options? [`InjectIsMutatingOptions`](../interfaces/InjectIsMutatingOptions.md) Additional configuration ## Returns `Signal`\<`number`\> A read-only signal with the number of fetching mutations. ================================================ FILE: docs/framework/angular/reference/functions/injectIsRestoring.md ================================================ --- id: injectIsRestoring title: injectIsRestoring --- # Function: injectIsRestoring() ```ts function injectIsRestoring(options?): Signal; ``` Defined in: [inject-is-restoring.ts:32](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-restoring.ts#L32) Injects a signal that tracks whether a restore is currently in progress. [injectQuery](injectQuery.md) and friends also check this internally to avoid race conditions between the restore and initializing queries. ## Parameters ### options? `InjectIsRestoringOptions` Options for injectIsRestoring. ## Returns `Signal`\<`boolean`\> readonly signal with boolean that indicates whether a restore is in progress. ================================================ FILE: docs/framework/angular/reference/functions/injectMutation.md ================================================ --- id: injectMutation title: injectMutation --- # Function: injectMutation() ```ts function injectMutation(injectMutationFn, options?): CreateMutationResult; ``` Defined in: [inject-mutation.ts:45](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L45) Injects a mutation: an imperative function that can be invoked which typically performs server side effects. Unlike queries, mutations are not run automatically. ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `Error` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ## Parameters ### injectMutationFn () => [`CreateMutationOptions`](../interfaces/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> A function that returns mutation options. ### options? [`InjectMutationOptions`](../interfaces/InjectMutationOptions.md) Additional configuration ## Returns [`CreateMutationResult`](../type-aliases/CreateMutationResult.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> The mutation. ================================================ FILE: docs/framework/angular/reference/functions/injectMutationState.md ================================================ --- id: injectMutationState title: injectMutationState --- # Function: injectMutationState() ```ts function injectMutationState(injectMutationStateFn, options?): Signal; ``` Defined in: [inject-mutation-state.ts:60](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation-state.ts#L60) Injects a signal that tracks the state of all mutations. ## Type Parameters ### TResult `TResult` = `MutationState`\<`unknown`, `Error`, `unknown`, `unknown`\> ## Parameters ### injectMutationStateFn () => `MutationStateOptions`\<`TResult`\> A function that returns mutation state options. ### options? [`InjectMutationStateOptions`](../interfaces/InjectMutationStateOptions.md) The Angular injector to use. ## Returns `Signal`\<`TResult`[]\> The signal that tracks the state of all mutations. ================================================ FILE: docs/framework/angular/reference/functions/injectQuery.md ================================================ --- id: injectQuery title: injectQuery --- # Function: injectQuery() Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => this.#http.get('https://api.github.com/repos/tanstack/query'), })) } ``` Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. In the example below, the query will be automatically enabled and executed when the filter signal changes to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. **Reactive example** ```ts class ServiceOrComponent { filter = signal('') todosQuery = injectQuery(() => ({ queryKey: ['todos', this.filter()], queryFn: () => fetchTodos(this.filter()), // Signals can be combined with expressions enabled: !!this.filter(), })) } ``` ## Param A function that returns query options. ## Param Additional configuration ## See https://tanstack.com/query/latest/docs/framework/angular/guides/queries ## Call Signature ```ts function injectQuery(injectQueryFn, options?): DefinedCreateQueryResult; ``` Defined in: [inject-query.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L65) Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => this.#http.get('https://api.github.com/repos/tanstack/query'), })) } ``` Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. In the example below, the query will be automatically enabled and executed when the filter signal changes to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. **Reactive example** ```ts class ServiceOrComponent { filter = signal('') todosQuery = injectQuery(() => ({ queryKey: ['todos', this.filter()], queryFn: () => fetchTodos(this.filter()), // Signals can be combined with expressions enabled: !!this.filter(), })) } ``` ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### injectQueryFn () => [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> A function that returns query options. #### options? [`InjectQueryOptions`](../interfaces/InjectQueryOptions.md) Additional configuration ### Returns [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`TData`, `TError`\> The query result. ### See https://tanstack.com/query/latest/docs/framework/angular/guides/queries ## Call Signature ```ts function injectQuery(injectQueryFn, options?): CreateQueryResult; ``` Defined in: [inject-query.ts:116](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L116) Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => this.#http.get('https://api.github.com/repos/tanstack/query'), })) } ``` Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. In the example below, the query will be automatically enabled and executed when the filter signal changes to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. **Reactive example** ```ts class ServiceOrComponent { filter = signal('') todosQuery = injectQuery(() => ({ queryKey: ['todos', this.filter()], queryFn: () => fetchTodos(this.filter()), // Signals can be combined with expressions enabled: !!this.filter(), })) } ``` ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### injectQueryFn () => [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> A function that returns query options. #### options? [`InjectQueryOptions`](../interfaces/InjectQueryOptions.md) Additional configuration ### Returns [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`TData`, `TError`\> The query result. ### See https://tanstack.com/query/latest/docs/framework/angular/guides/queries ## Call Signature ```ts function injectQuery(injectQueryFn, options?): CreateQueryResult; ``` Defined in: [inject-query.ts:167](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L167) Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => this.#http.get('https://api.github.com/repos/tanstack/query'), })) } ``` Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. In the example below, the query will be automatically enabled and executed when the filter signal changes to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. **Reactive example** ```ts class ServiceOrComponent { filter = signal('') todosQuery = injectQuery(() => ({ queryKey: ['todos', this.filter()], queryFn: () => fetchTodos(this.filter()), // Signals can be combined with expressions enabled: !!this.filter(), })) } ``` ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### injectQueryFn () => [`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> A function that returns query options. #### options? [`InjectQueryOptions`](../interfaces/InjectQueryOptions.md) Additional configuration ### Returns [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`TData`, `TError`\> The query result. ### See https://tanstack.com/query/latest/docs/framework/angular/guides/queries ================================================ FILE: docs/framework/angular/reference/functions/injectQueryClient.md ================================================ --- id: injectQueryClient title: injectQueryClient --- # ~~Function: injectQueryClient()~~ ```ts function injectQueryClient(injectOptions): QueryClient; ``` Defined in: [inject-query-client.ts:18](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query-client.ts#L18) Injects a `QueryClient` instance and allows passing a custom injector. ## Parameters ### injectOptions `InjectOptions` & `object` = `{}` Type of the options argument to inject and optionally a custom injector. ## Returns `QueryClient` The `QueryClient` instance. ## Deprecated Use `inject(QueryClient)` instead. If you need to get a `QueryClient` from a custom injector, use `injector.get(QueryClient)`. **Example** ```ts const queryClient = injectQueryClient(); ``` ================================================ FILE: docs/framework/angular/reference/functions/mutationOptions.md ================================================ --- id: mutationOptions title: mutationOptions --- # Function: mutationOptions() Allows to share and re-use mutation options in a type-safe way. **Example** ```ts export class QueriesService { private http = inject(HttpClient) private queryClient = inject(QueryClient) updatePost(id: number) { return mutationOptions({ mutationFn: (post: Post) => Promise.resolve(post), mutationKey: ["updatePost", id], onSuccess: (newPost) => { // ^? newPost: Post this.queryClient.setQueryData(["posts", id], newPost) }, }); } } class ComponentOrService { queries = inject(QueriesService) id = signal(0) mutation = injectMutation(() => this.queries.updatePost(this.id())) save() { this.mutation.mutate({ title: 'New Title' }) } } ``` ## Param The mutation options. ## Call Signature ```ts function mutationOptions(options): WithRequired, "mutationKey">; ``` Defined in: [mutation-options.ts:39](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/mutation-options.ts#L39) Allows to share and re-use mutation options in a type-safe way. **Example** ```ts export class QueriesService { private http = inject(HttpClient) private queryClient = inject(QueryClient) updatePost(id: number) { return mutationOptions({ mutationFn: (post: Post) => Promise.resolve(post), mutationKey: ["updatePost", id], onSuccess: (newPost) => { // ^? newPost: Post this.queryClient.setQueryData(["posts", id], newPost) }, }); } } class ComponentOrService { queries = inject(QueriesService) id = signal(0) mutation = injectMutation(() => this.queries.updatePost(this.id())) save() { this.mutation.mutate({ title: 'New Title' }) } } ``` ### Type Parameters #### TData `TData` = `unknown` #### TError `TError` = `Error` #### TVariables `TVariables` = `void` #### TOnMutateResult `TOnMutateResult` = `unknown` ### Parameters #### options `WithRequired`\<[`CreateMutationOptions`](../interfaces/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> The mutation options. ### Returns `WithRequired`\<[`CreateMutationOptions`](../interfaces/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> Mutation options. ## Call Signature ```ts function mutationOptions(options): Omit, "mutationKey">; ``` Defined in: [mutation-options.ts:53](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/mutation-options.ts#L53) Allows to share and re-use mutation options in a type-safe way. **Example** ```ts export class QueriesService { private http = inject(HttpClient) private queryClient = inject(QueryClient) updatePost(id: number) { return mutationOptions({ mutationFn: (post: Post) => Promise.resolve(post), mutationKey: ["updatePost", id], onSuccess: (newPost) => { // ^? newPost: Post this.queryClient.setQueryData(["posts", id], newPost) }, }); } } class ComponentOrService { queries = inject(QueriesService) id = signal(0) mutation = injectMutation(() => this.queries.updatePost(this.id())) save() { this.mutation.mutate({ title: 'New Title' }) } } ``` ### Type Parameters #### TData `TData` = `unknown` #### TError `TError` = `Error` #### TVariables `TVariables` = `void` #### TOnMutateResult `TOnMutateResult` = `unknown` ### Parameters #### options `Omit`\<[`CreateMutationOptions`](../interfaces/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> The mutation options. ### Returns `Omit`\<[`CreateMutationOptions`](../interfaces/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> Mutation options. ================================================ FILE: docs/framework/angular/reference/functions/provideAngularQuery.md ================================================ --- id: provideAngularQuery title: provideAngularQuery --- # ~~Function: provideAngularQuery()~~ ```ts function provideAngularQuery(queryClient): Provider[]; ``` Defined in: [providers.ts:124](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L124) Sets up providers necessary to enable TanStack Query functionality for Angular applications. Allows to configure a `QueryClient`. ## Parameters ### queryClient `QueryClient` A `QueryClient` instance. ## Returns `Provider`[] A set of providers to set up TanStack Query. ## See https://tanstack.com/query/v5/docs/framework/angular/quick-start ## Deprecated Use `provideTanStackQuery` instead. ================================================ FILE: docs/framework/angular/reference/functions/provideIsRestoring.md ================================================ --- id: provideIsRestoring title: provideIsRestoring --- # Function: provideIsRestoring() ```ts function provideIsRestoring(isRestoring): Provider; ``` Defined in: [inject-is-restoring.ts:43](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-restoring.ts#L43) Used by TanStack Query Angular persist client plugin to provide the signal that tracks the restore state ## Parameters ### isRestoring `Signal`\<`boolean`\> a readonly signal that returns a boolean ## Returns `Provider` Provider for the `isRestoring` signal ================================================ FILE: docs/framework/angular/reference/functions/provideQueryClient.md ================================================ --- id: provideQueryClient title: provideQueryClient --- # Function: provideQueryClient() ```ts function provideQueryClient(queryClient): Provider; ``` Defined in: [providers.ts:14](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L14) Usually [provideTanStackQuery](provideTanStackQuery.md) is used once to set up TanStack Query and the [https://tanstack.com/query/latest/docs/reference/QueryClient\|QueryClient](https://tanstack.com/query/latest/docs/reference/QueryClient|QueryClient) for the entire application. Internally it calls `provideQueryClient`. You can use `provideQueryClient` to provide a different `QueryClient` instance for a part of the application or for unit testing purposes. ## Parameters ### queryClient A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`. `QueryClient` | `InjectionToken`\<`QueryClient`\> ## Returns `Provider` a provider object that can be used to provide the `QueryClient` instance. ================================================ FILE: docs/framework/angular/reference/functions/provideTanStackQuery.md ================================================ --- id: provideTanStackQuery title: provideTanStackQuery --- # Function: provideTanStackQuery() ```ts function provideTanStackQuery(queryClient, ...features): Provider[]; ``` Defined in: [providers.ts:105](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L105) Sets up providers necessary to enable TanStack Query functionality for Angular applications. Allows to configure a `QueryClient` and optional features such as developer tools. **Example - standalone** ```ts import { provideTanStackQuery, QueryClient, } from '@tanstack/angular-query-experimental' bootstrapApplication(AppComponent, { providers: [provideTanStackQuery(new QueryClient())], }) ``` **Example - NgModule-based** ```ts import { provideTanStackQuery, QueryClient, } from '@tanstack/angular-query-experimental' @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [provideTanStackQuery(new QueryClient())], bootstrap: [AppComponent], }) export class AppModule {} ``` You can also enable optional developer tools by adding `withDevtools`. By default the tools will then be loaded when your app is in development mode. ```ts import { provideTanStackQuery, withDevtools QueryClient, } from '@tanstack/angular-query-experimental' bootstrapApplication(AppComponent, { providers: [ provideTanStackQuery(new QueryClient(), withDevtools()) ] } ) ``` **Example: using an InjectionToken** ```ts export const MY_QUERY_CLIENT = new InjectionToken('', { factory: () => new QueryClient(), }) // In a lazy loaded route or lazy loaded component's providers array: providers: [provideTanStackQuery(MY_QUERY_CLIENT)] ``` Using an InjectionToken for the QueryClient is an advanced optimization which allows TanStack Query to be absent from the main application bundle. This can be beneficial if you want to include TanStack Query on lazy loaded routes only while still sharing a `QueryClient`. Note that this is a small optimization and for most applications it's preferable to provide the `QueryClient` in the main application config. ## Parameters ### queryClient A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`. `QueryClient` | `InjectionToken`\<`QueryClient`\> ### features ...[`QueryFeatures`](../type-aliases/QueryFeatures.md)[] Optional features to configure additional Query functionality. ## Returns `Provider`[] A set of providers to set up TanStack Query. ## See - https://tanstack.com/query/v5/docs/framework/angular/quick-start - withDevtools ================================================ FILE: docs/framework/angular/reference/functions/queryFeature.md ================================================ --- id: queryFeature title: queryFeature --- # Function: queryFeature() ```ts function queryFeature(kind, providers): QueryFeature; ``` Defined in: [providers.ts:146](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L146) Helper function to create an object that represents a Query feature. ## Type Parameters ### TFeatureKind `TFeatureKind` *extends* `"Devtools"` \| `"PersistQueryClient"` ## Parameters ### kind `TFeatureKind` ### providers `Provider`[] ## Returns [`QueryFeature`](../interfaces/QueryFeature.md)\<`TFeatureKind`\> A Query feature. ================================================ FILE: docs/framework/angular/reference/functions/queryOptions.md ================================================ --- id: queryOptions title: queryOptions --- # Function: queryOptions() Allows to share and re-use query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. **Example** ```ts const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // ^? Promise }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) // ^? number | undefined ``` ## Param The query options to tag with the type from `queryFn`. ## Call Signature ```ts function queryOptions(options): Omit, "queryFn"> & object & object; ``` Defined in: [query-options.ts:76](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L76) Allows to share and re-use query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. **Example** ```ts const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // ^? Promise }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) // ^? number | undefined ``` ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> The query options to tag with the type from `queryFn`. ### Returns `Omit`\<[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` The tagged query options. ## Call Signature ```ts function queryOptions(options): OmitKeyof, "queryFn"> & object & object; ``` Defined in: [query-options.ts:108](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L108) Allows to share and re-use query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. **Example** ```ts const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // ^? Promise }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) // ^? number | undefined ``` ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UnusedSkipTokenOptions`](../type-aliases/UnusedSkipTokenOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> The query options to tag with the type from `queryFn`. ### Returns `OmitKeyof`\<[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` The tagged query options. ## Call Signature ```ts function queryOptions(options): CreateQueryOptions & object & object; ``` Defined in: [query-options.ts:140](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L140) Allows to share and re-use query options in a type-safe way. The `queryKey` will be tagged with the type from `queryFn`. **Example** ```ts const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // ^? Promise }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) // ^? number | undefined ``` ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> The query options to tag with the type from `queryFn`. ### Returns [`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` The tagged query options. ================================================ FILE: docs/framework/angular/reference/index.md ================================================ --- id: "@tanstack/angular-query-experimental" title: "@tanstack/angular-query-experimental" --- # @tanstack/angular-query-experimental ## Interfaces - [BaseMutationNarrowing](interfaces/BaseMutationNarrowing.md) - [BaseQueryNarrowing](interfaces/BaseQueryNarrowing.md) - [CreateBaseQueryOptions](interfaces/CreateBaseQueryOptions.md) - [CreateInfiniteQueryOptions](interfaces/CreateInfiniteQueryOptions.md) - [CreateMutationOptions](interfaces/CreateMutationOptions.md) - [CreateQueryOptions](interfaces/CreateQueryOptions.md) - [InjectInfiniteQueryOptions](interfaces/InjectInfiniteQueryOptions.md) - [InjectIsFetchingOptions](interfaces/InjectIsFetchingOptions.md) - [InjectIsMutatingOptions](interfaces/InjectIsMutatingOptions.md) - [InjectMutationOptions](interfaces/InjectMutationOptions.md) - [InjectMutationStateOptions](interfaces/InjectMutationStateOptions.md) - [InjectQueryOptions](interfaces/InjectQueryOptions.md) - [QueryFeature](interfaces/QueryFeature.md) ## Type Aliases - [CreateBaseMutationResult](type-aliases/CreateBaseMutationResult.md) - [CreateBaseQueryResult](type-aliases/CreateBaseQueryResult.md) - [CreateInfiniteQueryResult](type-aliases/CreateInfiniteQueryResult.md) - [CreateMutateAsyncFunction](type-aliases/CreateMutateAsyncFunction.md) - [CreateMutateFunction](type-aliases/CreateMutateFunction.md) - [CreateMutationResult](type-aliases/CreateMutationResult.md) - [CreateQueryResult](type-aliases/CreateQueryResult.md) - [DefinedCreateInfiniteQueryResult](type-aliases/DefinedCreateInfiniteQueryResult.md) - [DefinedCreateQueryResult](type-aliases/DefinedCreateQueryResult.md) - [DefinedInitialDataInfiniteOptions](type-aliases/DefinedInitialDataInfiniteOptions.md) - [DefinedInitialDataOptions](type-aliases/DefinedInitialDataOptions.md) - [DevtoolsFeature](type-aliases/DevtoolsFeature.md) - [PersistQueryClientFeature](type-aliases/PersistQueryClientFeature.md) - [QueriesOptions](type-aliases/QueriesOptions.md) - [QueriesResults](type-aliases/QueriesResults.md) - [QueryFeatures](type-aliases/QueryFeatures.md) - [UndefinedInitialDataInfiniteOptions](type-aliases/UndefinedInitialDataInfiniteOptions.md) - [UndefinedInitialDataOptions](type-aliases/UndefinedInitialDataOptions.md) - [UnusedSkipTokenInfiniteOptions](type-aliases/UnusedSkipTokenInfiniteOptions.md) - [UnusedSkipTokenOptions](type-aliases/UnusedSkipTokenOptions.md) ## Functions - [infiniteQueryOptions](functions/infiniteQueryOptions.md) - [injectInfiniteQuery](functions/injectInfiniteQuery.md) - [injectIsFetching](functions/injectIsFetching.md) - [injectIsMutating](functions/injectIsMutating.md) - [injectIsRestoring](functions/injectIsRestoring.md) - [injectMutation](functions/injectMutation.md) - [injectMutationState](functions/injectMutationState.md) - [injectQuery](functions/injectQuery.md) - [~~injectQueryClient~~](functions/injectQueryClient.md) - [mutationOptions](functions/mutationOptions.md) - [~~provideAngularQuery~~](functions/provideAngularQuery.md) - [provideIsRestoring](functions/provideIsRestoring.md) - [provideQueryClient](functions/provideQueryClient.md) - [provideTanStackQuery](functions/provideTanStackQuery.md) - [queryFeature](functions/queryFeature.md) - [queryOptions](functions/queryOptions.md) ================================================ FILE: docs/framework/angular/reference/interfaces/BaseMutationNarrowing.md ================================================ --- id: BaseMutationNarrowing title: BaseMutationNarrowing --- # Interface: BaseMutationNarrowing\ Defined in: [types.ts:190](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L190) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ## Properties ### isError ```ts isError: SignalFunction<(this) => this is CreateMutationResult, { mutate: CreateMutateFunction }> & { mutateAsync: CreateMutateAsyncFunction }>>; ``` Defined in: [types.ts:213](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L213) *** ### isIdle ```ts isIdle: SignalFunction<(this) => this is CreateMutationResult, { mutate: CreateMutateFunction }> & { mutateAsync: CreateMutateAsyncFunction }>>; ``` Defined in: [types.ts:247](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L247) *** ### isPending ```ts isPending: SignalFunction<(this) => this is CreateMutationResult, { mutate: CreateMutateFunction }> & { mutateAsync: CreateMutateAsyncFunction }>>; ``` Defined in: [types.ts:230](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L230) *** ### isSuccess ```ts isSuccess: SignalFunction<(this) => this is CreateMutationResult, { mutate: CreateMutateFunction }> & { mutateAsync: CreateMutateAsyncFunction }>>; ``` Defined in: [types.ts:196](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L196) ================================================ FILE: docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md ================================================ --- id: BaseQueryNarrowing title: BaseQueryNarrowing --- # Interface: BaseQueryNarrowing\ Defined in: [types.ts:57](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L57) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ## Properties ### isError() ```ts isError: (this) => this is CreateBaseQueryResult>; ``` Defined in: [types.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L65) #### Parameters ##### this [`CreateBaseQueryResult`](../type-aliases/CreateBaseQueryResult.md)\<`TData`, `TError`\> #### Returns `this is CreateBaseQueryResult>` *** ### isPending() ```ts isPending: (this) => this is CreateBaseQueryResult>; ``` Defined in: [types.ts:72](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L72) #### Parameters ##### this [`CreateBaseQueryResult`](../type-aliases/CreateBaseQueryResult.md)\<`TData`, `TError`\> #### Returns `this is CreateBaseQueryResult>` *** ### isSuccess() ```ts isSuccess: (this) => this is CreateBaseQueryResult>; ``` Defined in: [types.ts:58](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L58) #### Parameters ##### this [`CreateBaseQueryResult`](../type-aliases/CreateBaseQueryResult.md)\<`TData`, `TError`\> #### Returns `this is CreateBaseQueryResult>` ================================================ FILE: docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md ================================================ --- id: CreateBaseQueryOptions title: CreateBaseQueryOptions --- # Interface: CreateBaseQueryOptions\ Defined in: [types.ts:21](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L21) ## Extends - `QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryData`, `TQueryKey`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryData `TQueryData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md ================================================ --- id: CreateInfiniteQueryOptions title: CreateInfiniteQueryOptions --- # Interface: CreateInfiniteQueryOptions\ Defined in: [types.ts:81](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L81) ## Extends - `OmitKeyof`\<`InfiniteQueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"suspense"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/angular/reference/interfaces/CreateMutationOptions.md ================================================ --- id: CreateMutationOptions title: CreateMutationOptions --- # Interface: CreateMutationOptions\ Defined in: [types.ts:132](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L132) ## Extends - `OmitKeyof`\<`MutationObserverOptions`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"_defaulted"`\> ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/angular/reference/interfaces/CreateQueryOptions.md ================================================ --- id: CreateQueryOptions title: CreateQueryOptions --- # Interface: CreateQueryOptions\ Defined in: [types.ts:35](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L35) ## Extends - `OmitKeyof`\<[`CreateBaseQueryOptions`](CreateBaseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`\>, `"suspense"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md ================================================ --- id: InjectInfiniteQueryOptions title: InjectInfiniteQueryOptions --- # Interface: InjectInfiniteQueryOptions Defined in: [inject-infinite-query.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L25) ## Properties ### injector? ```ts optional injector: Injector; ``` Defined in: [inject-infinite-query.ts:31](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L31) The `Injector` in which to create the infinite query. If this is not provided, the current injection context will be used instead (via `inject`). ================================================ FILE: docs/framework/angular/reference/interfaces/InjectIsFetchingOptions.md ================================================ --- id: InjectIsFetchingOptions title: InjectIsFetchingOptions --- # Interface: InjectIsFetchingOptions Defined in: [inject-is-fetching.ts:13](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-fetching.ts#L13) ## Properties ### injector? ```ts optional injector: Injector; ``` Defined in: [inject-is-fetching.ts:19](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-fetching.ts#L19) The `Injector` in which to create the isFetching signal. If this is not provided, the current injection context will be used instead (via `inject`). ================================================ FILE: docs/framework/angular/reference/interfaces/InjectIsMutatingOptions.md ================================================ --- id: InjectIsMutatingOptions title: InjectIsMutatingOptions --- # Interface: InjectIsMutatingOptions Defined in: [inject-is-mutating.ts:13](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-mutating.ts#L13) ## Properties ### injector? ```ts optional injector: Injector; ``` Defined in: [inject-is-mutating.ts:19](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-is-mutating.ts#L19) The `Injector` in which to create the isMutating signal. If this is not provided, the current injection context will be used instead (via `inject`). ================================================ FILE: docs/framework/angular/reference/interfaces/InjectMutationOptions.md ================================================ --- id: InjectMutationOptions title: InjectMutationOptions --- # Interface: InjectMutationOptions Defined in: [inject-mutation.ts:28](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L28) ## Properties ### injector? ```ts optional injector: Injector; ``` Defined in: [inject-mutation.ts:34](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L34) The `Injector` in which to create the mutation. If this is not provided, the current injection context will be used instead (via `inject`). ================================================ FILE: docs/framework/angular/reference/interfaces/InjectMutationStateOptions.md ================================================ --- id: InjectMutationStateOptions title: InjectMutationStateOptions --- # Interface: InjectMutationStateOptions Defined in: [inject-mutation-state.ts:45](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation-state.ts#L45) ## Properties ### injector? ```ts optional injector: Injector; ``` Defined in: [inject-mutation-state.ts:51](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation-state.ts#L51) The `Injector` in which to create the mutation state signal. If this is not provided, the current injection context will be used instead (via `inject`). ================================================ FILE: docs/framework/angular/reference/interfaces/InjectQueryOptions.md ================================================ --- id: InjectQueryOptions title: InjectQueryOptions --- # Interface: InjectQueryOptions Defined in: [inject-query.ts:20](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L20) ## Properties ### injector? ```ts optional injector: Injector; ``` Defined in: [inject-query.ts:26](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L26) The `Injector` in which to create the query. If this is not provided, the current injection context will be used instead (via `inject`). ================================================ FILE: docs/framework/angular/reference/interfaces/QueryFeature.md ================================================ --- id: QueryFeature title: QueryFeature --- # Interface: QueryFeature\ Defined in: [providers.ts:135](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L135) Helper type to represent a Query feature. ## Type Parameters ### TFeatureKind `TFeatureKind` *extends* `QueryFeatureKind` ## Properties ### ɵkind ```ts ɵkind: TFeatureKind; ``` Defined in: [providers.ts:136](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L136) *** ### ɵproviders ```ts ɵproviders: Provider[]; ``` Defined in: [providers.ts:137](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L137) ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateBaseMutationResult.md ================================================ --- id: CreateBaseMutationResult title: CreateBaseMutationResult --- # Type Alias: CreateBaseMutationResult\ ```ts type CreateBaseMutationResult = Override, { mutate: CreateMutateFunction; }> & object; ``` Defined in: [types.ts:160](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L160) ## Type Declaration ### mutateAsync ```ts mutateAsync: CreateMutateAsyncFunction; ``` ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md ================================================ --- id: CreateBaseQueryResult title: CreateBaseQueryResult --- # Type Alias: CreateBaseQueryResult\ ```ts type CreateBaseQueryResult = BaseQueryNarrowing & MapToSignals>; ``` Defined in: [types.ts:98](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L98) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TState `TState` = `QueryObserverResult`\<`TData`, `TError`\> ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md ================================================ --- id: CreateInfiniteQueryResult title: CreateInfiniteQueryResult --- # Type Alias: CreateInfiniteQueryResult\ ```ts type CreateInfiniteQueryResult = BaseQueryNarrowing & MapToSignals>; ``` Defined in: [types.ts:117](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L117) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateMutateAsyncFunction.md ================================================ --- id: CreateMutateAsyncFunction title: CreateMutateAsyncFunction --- # Type Alias: CreateMutateAsyncFunction\ ```ts type CreateMutateAsyncFunction = MutateFunction; ``` Defined in: [types.ts:153](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L153) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateMutateFunction.md ================================================ --- id: CreateMutateFunction title: CreateMutateFunction --- # Type Alias: CreateMutateFunction()\ ```ts type CreateMutateFunction = (...args) => void; ``` Defined in: [types.ts:142](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L142) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ## Parameters ### args ...`Parameters`\<`MutateFunction`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>\> ## Returns `void` ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateMutationResult.md ================================================ --- id: CreateMutationResult title: CreateMutationResult --- # Type Alias: CreateMutationResult\ ```ts type CreateMutationResult = BaseMutationNarrowing & MapToSignals>; ``` Defined in: [types.ts:266](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L266) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ### TState `TState` = `CreateStatusBasedMutationResult`\<[`CreateBaseMutationResult`](CreateBaseMutationResult.md)\[`"status"`\], `TData`, `TError`, `TVariables`, `TOnMutateResult`\> ================================================ FILE: docs/framework/angular/reference/type-aliases/CreateQueryResult.md ================================================ --- id: CreateQueryResult title: CreateQueryResult --- # Type Alias: CreateQueryResult\ ```ts type CreateQueryResult = CreateBaseQueryResult; ``` Defined in: [types.ts:105](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L105) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md ================================================ --- id: DefinedCreateInfiniteQueryResult title: DefinedCreateInfiniteQueryResult --- # Type Alias: DefinedCreateInfiniteQueryResult\ ```ts type DefinedCreateInfiniteQueryResult = MapToSignals; ``` Defined in: [types.ts:123](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L123) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TDefinedInfiniteQueryObserver `TDefinedInfiniteQueryObserver` = `DefinedInfiniteQueryObserverResult`\<`TData`, `TError`\> ================================================ FILE: docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md ================================================ --- id: DefinedCreateQueryResult title: DefinedCreateQueryResult --- # Type Alias: DefinedCreateQueryResult\ ```ts type DefinedCreateQueryResult = BaseQueryNarrowing & MapToSignals>; ``` Defined in: [types.ts:110](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L110) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TState `TState` = `DefinedQueryObserverResult`\<`TData`, `TError`\> ================================================ FILE: docs/framework/angular/reference/type-aliases/DefinedInitialDataInfiniteOptions.md ================================================ --- id: DefinedInitialDataInfiniteOptions title: DefinedInitialDataInfiniteOptions --- # Type Alias: DefinedInitialDataInfiniteOptions\ ```ts type DefinedInitialDataInfiniteOptions = CreateInfiniteQueryOptions & object; ``` Defined in: [infinite-query-options.ts:62](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/infinite-query-options.ts#L62) ## Type Declaration ### initialData ```ts initialData: | NonUndefinedGuard> | () => NonUndefinedGuard> | undefined; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `DefaultError` ### TData `TData` = `InfiniteData`\<`TQueryFnData`\> ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md ================================================ --- id: DefinedInitialDataOptions title: DefinedInitialDataOptions --- # Type Alias: DefinedInitialDataOptions\ ```ts type DefinedInitialDataOptions = Omit, "queryFn"> & object; ``` Defined in: [query-options.ts:40](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L40) ## Type Declaration ### initialData ```ts initialData: | NonUndefinedGuard | () => NonUndefinedGuard; ``` ### queryFn? ```ts optional queryFn: QueryFunction; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/angular/reference/type-aliases/DevtoolsFeature.md ================================================ --- id: DevtoolsFeature title: DevtoolsFeature --- # Type Alias: DevtoolsFeature ```ts type DevtoolsFeature = QueryFeature<"Devtools">; ``` Defined in: [providers.ts:158](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L158) A type alias that represents a feature which enables developer tools. The type is used to describe the return value of the `withDevtools` function. ## See withDevtools ================================================ FILE: docs/framework/angular/reference/type-aliases/PersistQueryClientFeature.md ================================================ --- id: PersistQueryClientFeature title: PersistQueryClientFeature --- # Type Alias: PersistQueryClientFeature ```ts type PersistQueryClientFeature = QueryFeature<"PersistQueryClient">; ``` Defined in: [providers.ts:164](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L164) A type alias that represents a feature which enables persistence. The type is used to describe the return value of the `withPersistQueryClient` function. ================================================ FILE: docs/framework/angular/reference/type-aliases/QueriesOptions.md ================================================ --- id: QueriesOptions title: QueriesOptions --- # Type Alias: QueriesOptions\ ```ts type QueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? QueryObserverOptionsForCreateQueries[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryOptionsForCreateQueries] : T extends [infer Head, ...(infer Tails)] ? QueriesOptions<[...Tails], [...TResults, GetCreateQueryOptionsForCreateQueries], [...TDepth, 1]> : ReadonlyArray extends T ? T : T extends QueryObserverOptionsForCreateQueries[] ? QueryObserverOptionsForCreateQueries[] : QueryObserverOptionsForCreateQueries[]; ``` Defined in: [inject-queries.ts:144](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L144) QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/angular/reference/type-aliases/QueriesResults.md ================================================ --- id: QueriesResults title: QueriesResults --- # Type Alias: QueriesResults\ ```ts type QueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? CreateQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResult] : T extends [infer Head, ...(infer Tails)] ? QueriesResults<[...Tails], [...TResults, GetCreateQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetCreateQueryResult }; ``` Defined in: [inject-queries.ts:186](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L186) QueriesResults reducer recursively maps type param to results ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/angular/reference/type-aliases/QueryFeatures.md ================================================ --- id: QueryFeatures title: QueryFeatures --- # Type Alias: QueryFeatures ```ts type QueryFeatures = | DevtoolsFeature | PersistQueryClientFeature; ``` Defined in: [providers.ts:173](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L173) A type alias that represents all Query features available for use with `provideTanStackQuery`. Features can be enabled by adding special functions to the `provideTanStackQuery` call. See documentation for each symbol to find corresponding function name. See also `provideTanStackQuery` documentation on how to use those functions. ## See [provideTanStackQuery](../functions/provideTanStackQuery.md) ================================================ FILE: docs/framework/angular/reference/type-aliases/UndefinedInitialDataInfiniteOptions.md ================================================ --- id: UndefinedInitialDataInfiniteOptions title: UndefinedInitialDataInfiniteOptions --- # Type Alias: UndefinedInitialDataInfiniteOptions\ ```ts type UndefinedInitialDataInfiniteOptions = CreateInfiniteQueryOptions & object; ``` Defined in: [infinite-query-options.ts:13](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/infinite-query-options.ts#L13) ## Type Declaration ### initialData? ```ts optional initialData: | NonUndefinedGuard> | InitialDataFunction>>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `DefaultError` ### TData `TData` = `InfiniteData`\<`TQueryFnData`\> ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md ================================================ --- id: UndefinedInitialDataOptions title: UndefinedInitialDataOptions --- # Type Alias: UndefinedInitialDataOptions\ ```ts type UndefinedInitialDataOptions = CreateQueryOptions & object; ``` Defined in: [query-options.ts:13](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L13) ## Type Declaration ### initialData? ```ts optional initialData: | InitialDataFunction> | NonUndefinedGuard; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/angular/reference/type-aliases/UnusedSkipTokenInfiniteOptions.md ================================================ --- id: UnusedSkipTokenInfiniteOptions title: UnusedSkipTokenInfiniteOptions --- # Type Alias: UnusedSkipTokenInfiniteOptions\ ```ts type UnusedSkipTokenInfiniteOptions = OmitKeyof, "queryFn"> & object; ``` Defined in: [infinite-query-options.ts:34](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/infinite-query-options.ts#L34) ## Type Declaration ### queryFn? ```ts optional queryFn: Exclude["queryFn"], SkipToken | undefined>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `DefaultError` ### TData `TData` = `InfiniteData`\<`TQueryFnData`\> ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md ================================================ --- id: UnusedSkipTokenOptions title: UnusedSkipTokenOptions --- # Type Alias: UnusedSkipTokenOptions\ ```ts type UnusedSkipTokenOptions = OmitKeyof, "queryFn"> & object; ``` Defined in: [query-options.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L25) ## Type Declaration ### queryFn? ```ts optional queryFn: Exclude["queryFn"], SkipToken | undefined>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/angular/typescript.md ================================================ --- id: typescript title: TypeScript ref: docs/framework/react/typescript.md replace: { 'useQuery': 'injectQuery', 'useMutation': 'injectMutation', 'react-query': 'angular-query-experimental', 'public API of React Query': 'public API of TanStack Query and - after the experimental phase, the angular-query package', 'still follows': 'still follow', 'React Query': 'TanStack Query', '`success`': '`isSuccess()`', 'function:': 'function.', } --- [//]: # 'TypeInference1' ```angular-ts @Component({ // ... template: `@let data = query.data();`, // ^? data: number | undefined }) class MyComponent { query = injectQuery(() => ({ queryKey: ['test'], queryFn: () => Promise.resolve(5), })) } ``` [//]: # 'TypeInference1' [//]: # 'TypeInference2' ```angular-ts @Component({ // ... template: `@let data = query.data();`, // ^? data: string | undefined }) class MyComponent { query = injectQuery(() => ({ queryKey: ['test'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), })) } ``` [//]: # 'TypeInference2' [//]: # 'TypeInference3' In this example we pass Group[] to the type parameter of HttpClient's `get` method. ```angular-ts @Component({ template: `@let data = query.data();`, // ^? data: Group[] | undefined }) class MyComponent { http = inject(HttpClient) query = injectQuery(() => ({ queryKey: ['groups'], queryFn: () => lastValueFrom(this.http.get('/groups')), })) } ``` [//]: # 'TypeInference3' [//]: # 'TypeNarrowing' ```angular-ts @Component({ // ... template: ` @if (query.isSuccess()) { @let data = query.data(); // ^? data: number } `, }) class MyComponent { query = injectQuery(() => ({ queryKey: ['test'], queryFn: () => Promise.resolve(5), })) } ``` > TypeScript currently does not support discriminated unions on object methods. Narrowing on signal fields on objects such as query results only works on signals returning a boolean. Prefer using `isSuccess()` and similar boolean status signals over `status() === 'success'`. [//]: # 'TypeNarrowing' [//]: # 'TypingError' ```angular-ts @Component({ // ... template: `@let error = query.error();`, // ^? error: Error | null }) class MyComponent { query = injectQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) } ``` [//]: # 'TypingError' [//]: # 'TypingError2' ```angular-ts @Component({ // ... template: `@let error = query.error();`, // ^? error: string | null }) class MyComponent { query = injectQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) } ``` [//]: # 'TypingError2' [//]: # 'TypingError3' ```ts import axios from 'axios' query = injectQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups })) computed(() => { const error = query.error() // ^? error: Error | null if (axios.isAxiosError(error)) { error // ^? const error: AxiosError } }) ``` [//]: # 'TypingError3' [//]: # 'RegisterErrorType' ```ts import '@tanstack/angular-query-experimental' declare module '@tanstack/angular-query-experimental' { interface Register { // Use unknown so call sites must narrow explicitly. defaultError: unknown } } const query = injectQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) computed(() => { const error = query.error() // ^? error: unknown | null }) ``` [//]: # 'RegisterErrorType' [//]: # 'TypingQueryOptions' ## Typing Query Options If you inline query options into `injectQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `injectQuery` and e.g. `prefetchQuery` or manage them in a service. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: ```ts @Injectable({ providedIn: 'root', }) export class QueriesService { private http = inject(HttpClient) post(postId: number) { return queryOptions({ queryKey: ['post', postId], queryFn: () => { return lastValueFrom( this.http.get( `https://jsonplaceholder.typicode.com/posts/${postId}`, ), ) }, }) } } @Component({ // ... }) export class Component { queryClient = inject(QueryClient) postId = signal(1) queries = inject(QueriesService) optionsSignal = computed(() => this.queries.post(this.postId())) postQuery = injectQuery(() => this.queries.post(1)) postQuery = injectQuery(() => this.queries.post(this.postId())) // You can also pass a signal which returns query options postQuery = injectQuery(this.optionsSignal) someMethod() { this.queryClient.prefetchQuery(this.queries.post(23)) } } ``` Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: ```ts data = this.queryClient.getQueryData(groupOptions().queryKey) // ^? data: Post | undefined ``` Without `queryOptions`, the type of data would be unknown, unless we'd pass a type parameter: ```ts data = queryClient.getQueryData(['post', 1]) ``` ## Typing Mutation Options Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function: ```ts export class QueriesService { private http = inject(HttpClient) updatePost(id: number) { return mutationOptions({ mutationFn: (post: Post) => Promise.resolve(post), mutationKey: ['updatePost', id], onSuccess: (newPost) => { // ^? newPost: Post this.queryClient.setQueryData(['posts', id], newPost) }, }) } } ``` [//]: # 'TypingQueryOptions' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/angular/zoneless.md ================================================ --- id: zoneless title: Zoneless Angular --- Because the Angular adapter for TanStack Query is built on signals, it fully supports Zoneless! Among Zoneless benefits are improved performance and debugging experience. For details see the [Angular documentation](https://angular.dev/guide/zoneless). > Besides Zoneless, ZoneJS change detection is also fully supported. > When using Zoneless, ensure you are on Angular v19 or later to take advantage of the `PendingTasks` integration that keeps `ApplicationRef.whenStable()` in sync with ongoing queries and mutations. ================================================ FILE: docs/framework/preact/devtools.md ================================================ --- id: devtools title: Devtools --- Wave your hands in the air and shout hooray because Preact Query comes with dedicated devtools! 🥳 When you begin your Preact Query journey, you'll want these devtools by your side. They help visualize all of the inner workings of Preact Query and will likely save you hours of debugging if you find yourself in a pinch! > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > > - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) > - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) > - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) ## Install and Import the Devtools The devtools are a separate package that you need to install: ```bash npm i @tanstack/preact-query-devtools ``` or ```bash pnpm add @tanstack/preact-query-devtools ``` or ```bash yarn add @tanstack/preact-query-devtools ``` or ```bash bun add @tanstack/preact-query-devtools ``` You can import the devtools like this: ```tsx import { PreactQueryDevtools } from '@tanstack/preact-query-devtools' ``` By default, Preact Query Devtools are only included in bundles when `process.env.NODE_ENV === 'development'`, so you don't need to worry about excluding them during a production build. ## Floating Mode Floating Mode will mount the devtools as a fixed, floating element in your app and provide a toggle in the corner of the screen to show and hide the devtools. This toggle state will be stored and remembered in localStorage across reloads. Place the following code as high in your Preact app as you can. The closer it is to the root of the page, the better it will work! ```tsx import { PreactQueryDevtools } from '@tanstack/preact-query-devtools' function App() { return ( {/* The rest of your application */} ) } ``` ### Options - `initialIsOpen: boolean` - Set this `true` if you want the dev tools to default to being open - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right"` - Defaults to `bottom-right` - The position of the Preact Query logo to open and close the devtools panel - `position?: "top" | "bottom" | "left" | "right"` - Defaults to `bottom` - The position of the Preact Query devtools panel - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ================================================ FILE: docs/framework/preact/graphql.md ================================================ --- id: graphql title: GraphQL ref: docs/framework/react/graphql.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/background-fetching-indicators.md ================================================ --- id: background-fetching-indicators title: Background Fetching Indicators ref: docs/framework/react/guides/background-fetching-indicators.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/caching.md ================================================ --- id: caching title: Caching Examples ref: docs/framework/react/guides/caching.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/default-query-function.md ================================================ --- id: default-query-function title: Default Query Function ref: docs/framework/react/guides/default-query-function.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/dependent-queries.md ================================================ --- id: dependent-queries title: Dependent Queries ref: docs/framework/react/guides/dependent-queries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/disabling-queries.md ================================================ --- id: disabling-queries title: Disabling/Pausing Queries ref: docs/framework/react/guides/disabling-queries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'Example2' ```tsx function Todos() { const [filter, setFilter] = useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return (
    // 🚀 applying the filter will enable and execute the query {data && }
    ) } ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx import { skipToken, useQuery } from '@tanstack/preact-query' function Todos() { const [filter, setFilter] = useState() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return (
    // 🚀 applying the filter will enable and execute the query {data && }
    ) } ``` [//]: # 'Example3' ================================================ FILE: docs/framework/preact/guides/does-this-replace-client-state.md ================================================ --- id: does-this-replace-client-state title: Does TanStack Query replace Redux, MobX or other global state managers? ref: docs/framework/react/guides/does-this-replace-client-state.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/filters.md ================================================ --- id: filters title: Filters ref: docs/framework/react/guides/filters.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/important-defaults.md ================================================ --- id: important-defaults title: Important Defaults ref: docs/framework/react/guides/important-defaults.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/infinite-queries.md ================================================ --- id: infinite-queries title: Infinite Queries ref: docs/framework/react/guides/infinite-queries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'Example' ```tsx import { useInfiniteQuery } from '@tanstack/preact-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? (

    Loading...

    ) : status === 'error' ? (

    Error: {error.message}

    ) : ( <> {data.pages.map((group, i) => (
    {group.data.map((project) => (

    {project.name}

    ))}
    ))}
    {isFetching && !isFetchingNextPage ? 'Fetching...' : null}
    ) } ``` [//]: # 'Example' ================================================ FILE: docs/framework/preact/guides/initial-query-data.md ================================================ --- id: initial-query-data title: Initial Query Data ref: docs/framework/react/guides/initial-query-data.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/invalidations-from-mutations.md ================================================ --- id: invalidations-from-mutations title: Invalidations from Mutations ref: docs/framework/react/guides/invalidations-from-mutations.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/mutations.md ================================================ --- id: mutations title: Mutations ref: docs/framework/react/guides/mutations.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'Info1' [//]: # 'Info1' [//]: # 'Example2' [//]: # 'Example2' ================================================ FILE: docs/framework/preact/guides/network-mode.md ================================================ --- id: network-mode title: Network Mode ref: docs/framework/react/guides/network-mode.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/optimistic-updates.md ================================================ --- id: optimistic-updates title: Optimistic Updates ref: docs/framework/react/guides/optimistic-updates.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/paginated-queries.md ================================================ --- id: paginated-queries title: Paginated / Lagged Queries ref: docs/framework/react/guides/paginated-queries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'Example2' ```tsx import { keepPreviousData, useQuery } from '@tanstack/preact-query' import { useState } from 'preact/hooks' function Todos() { const [page, setPage] = useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return (
    {isPending ? (
    Loading...
    ) : isError ? (
    Error: {error.message}
    ) : (
    {data.projects.map((project) => (

    {project.name}

    ))}
    )} Current Page: {page + 1} {isFetching ? Loading... : null}
    ) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/preact/guides/parallel-queries.md ================================================ --- id: parallel-queries title: Parallel Queries ref: docs/framework/react/guides/parallel-queries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'Info' > When using Preact Query with compat's Suspense, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the `useSuspenseQueries` hook (which is suggested) or orchestrate your own parallelism with separate components for each `useSuspenseQuery` instance. [//]: # 'Info' ================================================ FILE: docs/framework/preact/guides/placeholder-query-data.md ================================================ --- id: placeholder-query-data title: Placeholder Query Data ref: docs/framework/react/guides/placeholder-query-data.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/prefetching.md ================================================ --- id: prefetching title: Prefetching & Router Integration ref: docs/framework/react/guides/prefetching.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'ExampleConditionally1' ```tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it import { lazy } from 'preact/iso' const GraphFeedItem = lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return } return })} ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } ``` [//]: # 'ExampleConditionally1' ================================================ FILE: docs/framework/preact/guides/queries.md ================================================ --- id: queries title: Queries ref: docs/framework/react/guides/queries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/query-cancellation.md ================================================ --- id: query-cancellation title: Query Cancellation ref: docs/framework/react/guides/query-cancellation.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/query-functions.md ================================================ --- id: query-functions title: Query Functions ref: docs/framework/react/guides/query-functions.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/query-invalidation.md ================================================ --- id: query-invalidation title: Query Invalidation ref: docs/framework/react/guides/query-invalidation.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/query-keys.md ================================================ --- id: query-keys title: Query Keys ref: docs/framework/react/guides/query-keys.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/query-options.md ================================================ --- id: query-options title: Query Options ref: docs/framework/react/guides/query-options.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/query-retries.md ================================================ --- id: query-retries title: Query Retries ref: docs/framework/react/guides/query-retries.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/render-optimizations.md ================================================ --- id: render-optimizations title: Render Optimizations ref: docs/framework/react/guides/render-optimizations.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/guides/request-waterfalls.md ================================================ --- id: request-waterfalls title: Performance & Request Waterfalls ref: docs/framework/react/guides/request-waterfalls.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'LazyExample' ```tsx import { lazy } from 'preact/iso' // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return } return })} ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } ``` [//]: # 'LazyExample' ================================================ FILE: docs/framework/preact/guides/scroll-restoration.md ================================================ --- id: scroll-restoration title: Scroll Restoration ref: docs/framework/react/guides/scroll-restoration.md --- ================================================ FILE: docs/framework/preact/guides/updates-from-mutation-responses.md ================================================ --- id: updates-from-mutation-responses title: Updates from Mutation Responses ref: docs/framework/react/guides/updates-from-mutation-responses.md --- ================================================ FILE: docs/framework/preact/guides/window-focus-refetching.md ================================================ --- id: window-focus-refetching title: Window Focus Refetching ref: docs/framework/react/guides/window-focus-refetching.md replace: { 'react-query': 'preact-query' } --- [//]: # 'ReactNative' [//]: # 'ReactNative' ================================================ FILE: docs/framework/preact/installation.md ================================================ --- id: installation title: Installation ref: docs/framework/react/installation.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- [//]: # 'Compatibility' > Wanna give it a spin before you download? Try out the [simple](./examples/simple) example [//]: # 'Compatibility' [//]: # 'CDNExample' ```html ``` [//]: # 'CDNExample' ================================================ FILE: docs/framework/preact/overview.md ================================================ --- id: overview title: Overview ref: docs/framework/react/overview.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/plugins/broadcastQueryClient.md ================================================ --- id: broadcastQueryClient title: broadcastQueryClient (Experimental) ref: docs/framework/react/plugins/broadcastQueryClient.md --- ================================================ FILE: docs/framework/preact/plugins/createAsyncStoragePersister.md ================================================ --- id: createAsyncStoragePersister title: createAsyncStoragePersister ref: docs/framework/react/plugins/createAsyncStoragePersister.md replace: { 'react-query': 'preact-query' } --- ================================================ FILE: docs/framework/preact/plugins/createPersister.md ================================================ --- id: createPersister title: experimental_createPersister ref: docs/framework/react/plugins/createPersister.md replace: { 'react-query': 'preact-query' } --- ================================================ FILE: docs/framework/preact/plugins/createSyncStoragePersister.md ================================================ --- id: createSyncStoragePersister title: createSyncStoragePersister ref: docs/framework/react/plugins/createSyncStoragePersister.md replace: { 'react-query': 'preact-query' } --- ================================================ FILE: docs/framework/preact/plugins/persistQueryClient.md ================================================ --- id: persistQueryClient title: persistQueryClient ref: docs/framework/react/plugins/persistQueryClient.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/quick-start.md ================================================ --- id: quick-start title: Quick Start ref: docs/framework/react/quick-start.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/preact/reference/functions/HydrationBoundary.md ================================================ --- id: HydrationBoundary title: HydrationBoundary --- # Function: HydrationBoundary() ```ts function HydrationBoundary(__namedParameters): ComponentChildren; ``` Defined in: [preact-query/src/HydrationBoundary.tsx:24](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/HydrationBoundary.tsx#L24) ## Parameters ### \_\_namedParameters [`HydrationBoundaryProps`](../interfaces/HydrationBoundaryProps.md) ## Returns `ComponentChildren` ================================================ FILE: docs/framework/preact/reference/functions/QueryClientProvider.md ================================================ --- id: QueryClientProvider title: QueryClientProvider --- # Function: QueryClientProvider() ```ts function QueryClientProvider(__namedParameters): VNode; ``` Defined in: [preact-query/src/QueryClientProvider.tsx:28](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryClientProvider.tsx#L28) ## Parameters ### \_\_namedParameters [`QueryClientProviderProps`](../type-aliases/QueryClientProviderProps.md) ## Returns `VNode` ================================================ FILE: docs/framework/preact/reference/functions/QueryErrorResetBoundary.md ================================================ --- id: QueryErrorResetBoundary title: QueryErrorResetBoundary --- # Function: QueryErrorResetBoundary() ```ts function QueryErrorResetBoundary(__namedParameters): Element; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:47](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L47) ## Parameters ### \_\_namedParameters [`QueryErrorResetBoundaryProps`](../interfaces/QueryErrorResetBoundaryProps.md) ## Returns `Element` ================================================ FILE: docs/framework/preact/reference/functions/infiniteQueryOptions.md ================================================ --- id: infiniteQueryOptions title: infiniteQueryOptions --- # Function: infiniteQueryOptions() ## Call Signature ```ts function infiniteQueryOptions(options): UseInfiniteQueryOptions & object & object; ``` Defined in: [preact-query/src/infiniteQueryOptions.ts:75](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L75) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ### Returns [`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> & `object` & `object` ## Call Signature ```ts function infiniteQueryOptions(options): OmitKeyof, "queryFn"> & object & object; ``` Defined in: [preact-query/src/infiniteQueryOptions.ts:99](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L99) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`UnusedSkipTokenInfiniteOptions`](../type-aliases/UnusedSkipTokenInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ### Returns `OmitKeyof`\<[`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"queryFn"`\> & `object` & `object` ## Call Signature ```ts function infiniteQueryOptions(options): UseInfiniteQueryOptions & object & object; ``` Defined in: [preact-query/src/infiniteQueryOptions.ts:123](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L123) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ### Returns [`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> & `object` & `object` ================================================ FILE: docs/framework/preact/reference/functions/mutationOptions.md ================================================ --- id: mutationOptions title: mutationOptions --- # Function: mutationOptions() ## Call Signature ```ts function mutationOptions(options): WithRequired, "mutationKey">; ``` Defined in: [preact-query/src/mutationOptions.ts:4](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/mutationOptions.ts#L4) ### Type Parameters #### TData `TData` = `unknown` #### TError `TError` = `Error` #### TVariables `TVariables` = `void` #### TOnMutateResult `TOnMutateResult` = `unknown` ### Parameters #### options `WithRequired`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> ### Returns `WithRequired`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> ## Call Signature ```ts function mutationOptions(options): Omit, "mutationKey">; ``` Defined in: [preact-query/src/mutationOptions.ts:18](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/mutationOptions.ts#L18) ### Type Parameters #### TData `TData` = `unknown` #### TError `TError` = `Error` #### TVariables `TVariables` = `void` #### TOnMutateResult `TOnMutateResult` = `unknown` ### Parameters #### options `Omit`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> ### Returns `Omit`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> ================================================ FILE: docs/framework/preact/reference/functions/queryOptions.md ================================================ --- id: queryOptions title: queryOptions --- # Function: queryOptions() ## Call Signature ```ts function queryOptions(options): Omit, "queryFn"> & object & object; ``` Defined in: [preact-query/src/queryOptions.ts:52](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/queryOptions.ts#L52) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### Returns `Omit`\<[`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` ## Call Signature ```ts function queryOptions(options): OmitKeyof, "queryFn"> & object & object; ``` Defined in: [preact-query/src/queryOptions.ts:63](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/queryOptions.ts#L63) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UnusedSkipTokenOptions`](../type-aliases/UnusedSkipTokenOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### Returns `OmitKeyof`\<[`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` ## Call Signature ```ts function queryOptions(options): UseQueryOptions & object & object; ``` Defined in: [preact-query/src/queryOptions.ts:74](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/queryOptions.ts#L74) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### Returns [`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` ================================================ FILE: docs/framework/preact/reference/functions/useInfiniteQuery.md ================================================ --- id: useInfiniteQuery title: useInfiniteQuery --- # Function: useInfiniteQuery() ## Call Signature ```ts function useInfiniteQuery(options, queryClient?): DefinedUseInfiniteQueryResult; ``` Defined in: [preact-query/src/useInfiniteQuery.ts:20](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L20) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> #### queryClient? `QueryClient` ### Returns [`DefinedUseInfiniteQueryResult`](../type-aliases/DefinedUseInfiniteQueryResult.md)\<`TData`, `TError`\> ## Call Signature ```ts function useInfiniteQuery(options, queryClient?): UseInfiniteQueryResult; ``` Defined in: [preact-query/src/useInfiniteQuery.ts:37](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L37) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> #### queryClient? `QueryClient` ### Returns [`UseInfiniteQueryResult`](../type-aliases/UseInfiniteQueryResult.md)\<`TData`, `TError`\> ## Call Signature ```ts function useInfiniteQuery(options, queryClient?): UseInfiniteQueryResult; ``` Defined in: [preact-query/src/useInfiniteQuery.ts:54](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L54) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] #### TPageParam `TPageParam` = `unknown` ### Parameters #### options [`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> #### queryClient? `QueryClient` ### Returns [`UseInfiniteQueryResult`](../type-aliases/UseInfiniteQueryResult.md)\<`TData`, `TError`\> ================================================ FILE: docs/framework/preact/reference/functions/useIsFetching.md ================================================ --- id: useIsFetching title: useIsFetching --- # Function: useIsFetching() ```ts function useIsFetching(filters?, queryClient?): number; ``` Defined in: [preact-query/src/useIsFetching.ts:8](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useIsFetching.ts#L8) ## Parameters ### filters? `QueryFilters`\ ### queryClient? `QueryClient` ## Returns `number` ================================================ FILE: docs/framework/preact/reference/functions/useIsMutating.md ================================================ --- id: useIsMutating title: useIsMutating --- # Function: useIsMutating() ```ts function useIsMutating(filters?, queryClient?): number; ``` Defined in: [preact-query/src/useMutationState.ts:13](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useMutationState.ts#L13) ## Parameters ### filters? `MutationFilters`\<`unknown`, `Error`, `unknown`, `unknown`\> ### queryClient? `QueryClient` ## Returns `number` ================================================ FILE: docs/framework/preact/reference/functions/useIsRestoring.md ================================================ --- id: useIsRestoring title: useIsRestoring --- # Function: useIsRestoring() ```ts function useIsRestoring(): boolean; ``` Defined in: [preact-query/src/IsRestoringProvider.ts:6](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/IsRestoringProvider.ts#L6) ## Returns `boolean` ================================================ FILE: docs/framework/preact/reference/functions/useMutation.md ================================================ --- id: useMutation title: useMutation --- # Function: useMutation() ```ts function useMutation(options, queryClient?): UseMutationResult; ``` Defined in: [preact-query/src/useMutation.ts:19](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useMutation.ts#L19) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `Error` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ## Parameters ### options [`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> ### queryClient? `QueryClient` ## Returns [`UseMutationResult`](../type-aliases/UseMutationResult.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> ================================================ FILE: docs/framework/preact/reference/functions/useMutationState.md ================================================ --- id: useMutationState title: useMutationState --- # Function: useMutationState() ```ts function useMutationState(options, queryClient?): TResult[]; ``` Defined in: [preact-query/src/useMutationState.ts:41](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useMutationState.ts#L41) ## Type Parameters ### TResult `TResult` = `MutationState`\<`unknown`, `Error`, `unknown`, `unknown`\> ## Parameters ### options `MutationStateOptions`\<`TResult`\> = `{}` ### queryClient? `QueryClient` ## Returns `TResult`[] ================================================ FILE: docs/framework/preact/reference/functions/usePrefetchInfiniteQuery.md ================================================ --- id: usePrefetchInfiniteQuery title: usePrefetchInfiniteQuery --- # Function: usePrefetchInfiniteQuery() ```ts function usePrefetchInfiniteQuery(options, queryClient?): void; ``` Defined in: [preact-query/src/usePrefetchInfiniteQuery.tsx:9](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/usePrefetchInfiniteQuery.tsx#L9) ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `Error` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### TPageParam `TPageParam` = `unknown` ## Parameters ### options `FetchInfiniteQueryOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ### queryClient? `QueryClient` ## Returns `void` ================================================ FILE: docs/framework/preact/reference/functions/usePrefetchQuery.md ================================================ --- id: usePrefetchQuery title: usePrefetchQuery --- # Function: usePrefetchQuery() ```ts function usePrefetchQuery(options, queryClient?): void; ``` Defined in: [preact-query/src/usePrefetchQuery.tsx:5](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/usePrefetchQuery.tsx#L5) ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `Error` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ## Parameters ### options [`UsePrefetchQueryOptions`](../interfaces/UsePrefetchQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### queryClient? `QueryClient` ## Returns `void` ================================================ FILE: docs/framework/preact/reference/functions/useQueries.md ================================================ --- id: useQueries title: useQueries --- # Function: useQueries() ```ts function useQueries(__namedParameters, queryClient?): TCombinedResult; ``` Defined in: [preact-query/src/useQueries.ts:207](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useQueries.ts#L207) ## Type Parameters ### T `T` *extends* `any`[] ### TCombinedResult `TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseQueryResult`\<`Head`\>, `GetUseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseQueryResult`\<`Head`\>, `GetUseQueryResult`\<`Head`\>, `GetUseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetUseQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetUseQueryResult\\]\> \} ## Parameters ### \_\_namedParameters #### combine? (`result`) => `TCombinedResult` #### queries \| readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseQueryOptionsForUseQueries`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseQueryOptionsForUseQueries`\<`Head`\>, `GetUseQueryOptionsForUseQueries`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : readonly ...[] *extends* \[`...(...)[]`\] ? \[`...(...)[]`\] : ... *extends* ... ? ... : ... : readonly `unknown`[] *extends* `T` ? `T` : `T` *extends* `UseQueryOptionsForUseQueries`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] ? `UseQueryOptionsForUseQueries`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] : `UseQueryOptionsForUseQueries`\<`unknown`, `Error`, `unknown`, readonly ...[]\>[]\] \| readonly \[\{ \[K in string \| number \| symbol\]: GetUseQueryOptionsForUseQueries\\]\> \}\] #### subscribed? `boolean` ### queryClient? `QueryClient` ## Returns `TCombinedResult` ================================================ FILE: docs/framework/preact/reference/functions/useQuery.md ================================================ --- id: useQuery title: useQuery --- # Function: useQuery() ## Call Signature ```ts function useQuery(options, queryClient?): DefinedUseQueryResult, TError>; ``` Defined in: [preact-query/src/useQuery.ts:19](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useQuery.ts#L19) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> #### queryClient? `QueryClient` ### Returns [`DefinedUseQueryResult`](../type-aliases/DefinedUseQueryResult.md)\<`NoInfer`\<`TData`\>, `TError`\> ## Call Signature ```ts function useQuery(options, queryClient?): UseQueryResult, TError>; ``` Defined in: [preact-query/src/useQuery.ts:29](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useQuery.ts#L29) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> #### queryClient? `QueryClient` ### Returns [`UseQueryResult`](../type-aliases/UseQueryResult.md)\<`NoInfer`\<`TData`\>, `TError`\> ## Call Signature ```ts function useQuery(options, queryClient?): UseQueryResult, TError>; ``` Defined in: [preact-query/src/useQuery.ts:39](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useQuery.ts#L39) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> #### queryClient? `QueryClient` ### Returns [`UseQueryResult`](../type-aliases/UseQueryResult.md)\<`NoInfer`\<`TData`\>, `TError`\> ================================================ FILE: docs/framework/preact/reference/functions/useQueryClient.md ================================================ --- id: useQueryClient title: useQueryClient --- # Function: useQueryClient() ```ts function useQueryClient(queryClient?): QueryClient; ``` Defined in: [preact-query/src/QueryClientProvider.tsx:9](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryClientProvider.tsx#L9) ## Parameters ### queryClient? `QueryClient` ## Returns `QueryClient` ================================================ FILE: docs/framework/preact/reference/functions/useQueryErrorResetBoundary.md ================================================ --- id: useQueryErrorResetBoundary title: useQueryErrorResetBoundary --- # Function: useQueryErrorResetBoundary() ```ts function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:34](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L34) ## Returns `QueryErrorResetBoundaryValue` ================================================ FILE: docs/framework/preact/reference/functions/useSuspenseInfiniteQuery.md ================================================ --- id: useSuspenseInfiniteQuery title: useSuspenseInfiniteQuery --- # Function: useSuspenseInfiniteQuery() ```ts function useSuspenseInfiniteQuery(options, queryClient?): UseSuspenseInfiniteQueryResult; ``` Defined in: [preact-query/src/useSuspenseInfiniteQuery.ts:17](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useSuspenseInfiniteQuery.ts#L17) ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `Error` ### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> ### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### TPageParam `TPageParam` = `unknown` ## Parameters ### options [`UseSuspenseInfiniteQueryOptions`](../interfaces/UseSuspenseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ### queryClient? `QueryClient` ## Returns [`UseSuspenseInfiniteQueryResult`](../type-aliases/UseSuspenseInfiniteQueryResult.md)\<`TData`, `TError`\> ================================================ FILE: docs/framework/preact/reference/functions/useSuspenseQueries.md ================================================ --- id: useSuspenseQueries title: useSuspenseQueries --- # Function: useSuspenseQueries() ## Call Signature ```ts function useSuspenseQueries(options, queryClient?): TCombinedResult; ``` Defined in: [preact-query/src/useSuspenseQueries.ts:164](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useSuspenseQueries.ts#L164) ### Type Parameters #### T `T` *extends* `any`[] #### TCombinedResult `TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \} ### Parameters #### options ##### combine? (`result`) => `TCombinedResult` ##### queries \| readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>, `GetUseSuspenseQueryOptions`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : ...[] *extends* \[`...(...)[]`\] ? \[`...(...)[]`\] : ... *extends* ... ? ... : ... : `unknown`[] *extends* `T` ? `T` : `T` *extends* [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] ? [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] : [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`unknown`, `Error`, `unknown`, readonly ...[]\>[]\] \| readonly \[\{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryOptions\\]\> \}\] #### queryClient? `QueryClient` ### Returns `TCombinedResult` ## Call Signature ```ts function useSuspenseQueries(options, queryClient?): TCombinedResult; ``` Defined in: [preact-query/src/useSuspenseQueries.ts:177](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useSuspenseQueries.ts#L177) ### Type Parameters #### T `T` *extends* `any`[] #### TCombinedResult `TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \} ### Parameters #### options ##### combine? (`result`) => `TCombinedResult` ##### queries readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>, `GetUseSuspenseQueryOptions`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...(...)[]`\] *extends* \[...\] ? \[..., ..., ...\] : ... *extends* ... ? ... : ... : `unknown`[] *extends* \[`...Tails[]`\] ? \[`...Tails[]`\] : \[`...(...)[]`\] *extends* ...[] ? ...[] : ...[] : `unknown`[] *extends* `T` ? `T` : `T` *extends* [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] ? [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] : [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`unknown`, `Error`, `unknown`, readonly `unknown`[]\>[]\] #### queryClient? `QueryClient` ### Returns `TCombinedResult` ================================================ FILE: docs/framework/preact/reference/functions/useSuspenseQuery.md ================================================ --- id: useSuspenseQuery title: useSuspenseQuery --- # Function: useSuspenseQuery() ```ts function useSuspenseQuery(options, queryClient?): UseSuspenseQueryResult; ``` Defined in: [preact-query/src/useSuspenseQuery.ts:7](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useSuspenseQuery.ts#L7) ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `Error` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ## Parameters ### options [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### queryClient? `QueryClient` ## Returns [`UseSuspenseQueryResult`](../type-aliases/UseSuspenseQueryResult.md)\<`TData`, `TError`\> ================================================ FILE: docs/framework/preact/reference/index.md ================================================ --- id: "@tanstack/preact-query" title: "@tanstack/preact-query" --- # @tanstack/preact-query ## Interfaces - [HydrationBoundaryProps](interfaces/HydrationBoundaryProps.md) - [QueryErrorResetBoundaryProps](interfaces/QueryErrorResetBoundaryProps.md) - [UseBaseQueryOptions](interfaces/UseBaseQueryOptions.md) - [UseInfiniteQueryOptions](interfaces/UseInfiniteQueryOptions.md) - [UseMutationOptions](interfaces/UseMutationOptions.md) - [UsePrefetchQueryOptions](interfaces/UsePrefetchQueryOptions.md) - [UseQueryOptions](interfaces/UseQueryOptions.md) - [UseSuspenseInfiniteQueryOptions](interfaces/UseSuspenseInfiniteQueryOptions.md) - [UseSuspenseQueryOptions](interfaces/UseSuspenseQueryOptions.md) ## Type Aliases - [AnyUseBaseQueryOptions](type-aliases/AnyUseBaseQueryOptions.md) - [AnyUseInfiniteQueryOptions](type-aliases/AnyUseInfiniteQueryOptions.md) - [AnyUseMutationOptions](type-aliases/AnyUseMutationOptions.md) - [AnyUseQueryOptions](type-aliases/AnyUseQueryOptions.md) - [AnyUseSuspenseInfiniteQueryOptions](type-aliases/AnyUseSuspenseInfiniteQueryOptions.md) - [AnyUseSuspenseQueryOptions](type-aliases/AnyUseSuspenseQueryOptions.md) - [DefinedInitialDataInfiniteOptions](type-aliases/DefinedInitialDataInfiniteOptions.md) - [DefinedInitialDataOptions](type-aliases/DefinedInitialDataOptions.md) - [DefinedUseInfiniteQueryResult](type-aliases/DefinedUseInfiniteQueryResult.md) - [DefinedUseQueryResult](type-aliases/DefinedUseQueryResult.md) - [QueriesOptions](type-aliases/QueriesOptions.md) - [QueriesResults](type-aliases/QueriesResults.md) - [QueryClientProviderProps](type-aliases/QueryClientProviderProps.md) - [QueryErrorClearResetFunction](type-aliases/QueryErrorClearResetFunction.md) - [QueryErrorIsResetFunction](type-aliases/QueryErrorIsResetFunction.md) - [QueryErrorResetBoundaryFunction](type-aliases/QueryErrorResetBoundaryFunction.md) - [QueryErrorResetFunction](type-aliases/QueryErrorResetFunction.md) - [SuspenseQueriesOptions](type-aliases/SuspenseQueriesOptions.md) - [SuspenseQueriesResults](type-aliases/SuspenseQueriesResults.md) - [UndefinedInitialDataInfiniteOptions](type-aliases/UndefinedInitialDataInfiniteOptions.md) - [UndefinedInitialDataOptions](type-aliases/UndefinedInitialDataOptions.md) - [UnusedSkipTokenInfiniteOptions](type-aliases/UnusedSkipTokenInfiniteOptions.md) - [UnusedSkipTokenOptions](type-aliases/UnusedSkipTokenOptions.md) - [UseBaseMutationResult](type-aliases/UseBaseMutationResult.md) - [UseBaseQueryResult](type-aliases/UseBaseQueryResult.md) - [UseInfiniteQueryResult](type-aliases/UseInfiniteQueryResult.md) - [UseMutateAsyncFunction](type-aliases/UseMutateAsyncFunction.md) - [UseMutateFunction](type-aliases/UseMutateFunction.md) - [UseMutationResult](type-aliases/UseMutationResult.md) - [UseQueryResult](type-aliases/UseQueryResult.md) - [UseSuspenseInfiniteQueryResult](type-aliases/UseSuspenseInfiniteQueryResult.md) - [UseSuspenseQueryResult](type-aliases/UseSuspenseQueryResult.md) ## Variables - [IsRestoringProvider](variables/IsRestoringProvider.md) - [QueryClientContext](variables/QueryClientContext.md) ## Functions - [HydrationBoundary](functions/HydrationBoundary.md) - [infiniteQueryOptions](functions/infiniteQueryOptions.md) - [mutationOptions](functions/mutationOptions.md) - [QueryClientProvider](functions/QueryClientProvider.md) - [QueryErrorResetBoundary](functions/QueryErrorResetBoundary.md) - [queryOptions](functions/queryOptions.md) - [useInfiniteQuery](functions/useInfiniteQuery.md) - [useIsFetching](functions/useIsFetching.md) - [useIsMutating](functions/useIsMutating.md) - [useIsRestoring](functions/useIsRestoring.md) - [useMutation](functions/useMutation.md) - [useMutationState](functions/useMutationState.md) - [usePrefetchInfiniteQuery](functions/usePrefetchInfiniteQuery.md) - [usePrefetchQuery](functions/usePrefetchQuery.md) - [useQueries](functions/useQueries.md) - [useQuery](functions/useQuery.md) - [useQueryClient](functions/useQueryClient.md) - [useQueryErrorResetBoundary](functions/useQueryErrorResetBoundary.md) - [useSuspenseInfiniteQuery](functions/useSuspenseInfiniteQuery.md) - [useSuspenseQueries](functions/useSuspenseQueries.md) - [useSuspenseQuery](functions/useSuspenseQuery.md) ================================================ FILE: docs/framework/preact/reference/interfaces/HydrationBoundaryProps.md ================================================ --- id: HydrationBoundaryProps title: HydrationBoundaryProps --- # Interface: HydrationBoundaryProps Defined in: [preact-query/src/HydrationBoundary.tsx:12](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/HydrationBoundary.tsx#L12) ## Properties ### children? ```ts optional children: ComponentChildren; ``` Defined in: [preact-query/src/HydrationBoundary.tsx:20](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/HydrationBoundary.tsx#L20) *** ### options? ```ts optional options: OmitKeyof & object; ``` Defined in: [preact-query/src/HydrationBoundary.tsx:14](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/HydrationBoundary.tsx#L14) #### Type Declaration ##### defaultOptions? ```ts optional defaultOptions: OmitKeyof<{ }, "mutations">; ``` *** ### queryClient? ```ts optional queryClient: QueryClient; ``` Defined in: [preact-query/src/HydrationBoundary.tsx:21](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/HydrationBoundary.tsx#L21) *** ### state ```ts state: DehydratedState | null | undefined; ``` Defined in: [preact-query/src/HydrationBoundary.tsx:13](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/HydrationBoundary.tsx#L13) ================================================ FILE: docs/framework/preact/reference/interfaces/QueryErrorResetBoundaryProps.md ================================================ --- id: QueryErrorResetBoundaryProps title: QueryErrorResetBoundaryProps --- # Interface: QueryErrorResetBoundaryProps Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:43](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L43) ## Properties ### children ```ts children: | ComponentChildren | QueryErrorResetBoundaryFunction; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:44](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L44) ================================================ FILE: docs/framework/preact/reference/interfaces/UseBaseQueryOptions.md ================================================ --- id: UseBaseQueryOptions title: UseBaseQueryOptions --- # Interface: UseBaseQueryOptions\ Defined in: [preact-query/src/types.ts:29](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L29) ## Extends - `QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryData`, `TQueryKey`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryData `TQueryData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ## Properties ### subscribed? ```ts optional subscribed: boolean; ``` Defined in: [preact-query/src/types.ts:46](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L46) Set this to `false` to unsubscribe this observer from updates to the query cache. Defaults to `true`. ================================================ FILE: docs/framework/preact/reference/interfaces/UseInfiniteQueryOptions.md ================================================ --- id: UseInfiniteQueryOptions title: UseInfiniteQueryOptions --- # Interface: UseInfiniteQueryOptions\ Defined in: [preact-query/src/types.ts:103](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L103) ## Extends - `OmitKeyof`\<`InfiniteQueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"suspense"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ## Properties ### subscribed? ```ts optional subscribed: boolean; ``` Defined in: [preact-query/src/types.ts:123](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L123) Set this to `false` to unsubscribe this observer from updates to the query cache. Defaults to `true`. ================================================ FILE: docs/framework/preact/reference/interfaces/UseMutationOptions.md ================================================ --- id: UseMutationOptions title: UseMutationOptions --- # Interface: UseMutationOptions\ Defined in: [preact-query/src/types.ts:192](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L192) ## Extends - `OmitKeyof`\<`MutationObserverOptions`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"_defaulted"`\> ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/preact/reference/interfaces/UsePrefetchQueryOptions.md ================================================ --- id: UsePrefetchQueryOptions title: UsePrefetchQueryOptions --- # Interface: UsePrefetchQueryOptions\ Defined in: [preact-query/src/types.ts:49](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L49) ## Extends - `OmitKeyof`\<`FetchQueryOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ## Properties ### queryFn? ```ts optional queryFn: QueryFunction; ``` Defined in: [preact-query/src/types.ts:58](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L58) ================================================ FILE: docs/framework/preact/reference/interfaces/UseQueryOptions.md ================================================ --- id: UseQueryOptions title: UseQueryOptions --- # Interface: UseQueryOptions\ Defined in: [preact-query/src/types.ts:65](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L65) ## Extends - `OmitKeyof`\<[`UseBaseQueryOptions`](UseBaseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`\>, `"suspense"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ## Properties ### subscribed? ```ts optional subscribed: boolean; ``` Defined in: [preact-query/src/types.ts:46](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L46) Set this to `false` to unsubscribe this observer from updates to the query cache. Defaults to `true`. #### Inherited from ```ts OmitKeyof.subscribed ``` ================================================ FILE: docs/framework/preact/reference/interfaces/UseSuspenseInfiniteQueryOptions.md ================================================ --- id: UseSuspenseInfiniteQueryOptions title: UseSuspenseInfiniteQueryOptions --- # Interface: UseSuspenseInfiniteQueryOptions\ Defined in: [preact-query/src/types.ts:128](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L128) ## Extends - `OmitKeyof`\<[`UseInfiniteQueryOptions`](UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"queryFn"` \| `"enabled"` \| `"throwOnError"` \| `"placeholderData"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ## Properties ### queryFn? ```ts optional queryFn: QueryFunction; ``` Defined in: [preact-query/src/types.ts:138](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L138) *** ### subscribed? ```ts optional subscribed: boolean; ``` Defined in: [preact-query/src/types.ts:123](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L123) Set this to `false` to unsubscribe this observer from updates to the query cache. Defaults to `true`. #### Inherited from ```ts OmitKeyof.subscribed ``` ================================================ FILE: docs/framework/preact/reference/interfaces/UseSuspenseQueryOptions.md ================================================ --- id: UseSuspenseQueryOptions title: UseSuspenseQueryOptions --- # Interface: UseSuspenseQueryOptions\ Defined in: [preact-query/src/types.ts:81](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L81) ## Extends - `OmitKeyof`\<[`UseQueryOptions`](UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"` \| `"enabled"` \| `"throwOnError"` \| `"placeholderData"`\> ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ## Properties ### queryFn? ```ts optional queryFn: QueryFunction; ``` Defined in: [preact-query/src/types.ts:90](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L90) *** ### subscribed? ```ts optional subscribed: boolean; ``` Defined in: [preact-query/src/types.ts:46](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L46) Set this to `false` to unsubscribe this observer from updates to the query cache. Defaults to `true`. #### Inherited from ```ts OmitKeyof.subscribed ``` ================================================ FILE: docs/framework/preact/reference/type-aliases/AnyUseBaseQueryOptions.md ================================================ --- id: AnyUseBaseQueryOptions title: AnyUseBaseQueryOptions --- # Type Alias: AnyUseBaseQueryOptions ```ts type AnyUseBaseQueryOptions = UseBaseQueryOptions; ``` Defined in: [preact-query/src/types.ts:22](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L22) ================================================ FILE: docs/framework/preact/reference/type-aliases/AnyUseInfiniteQueryOptions.md ================================================ --- id: AnyUseInfiniteQueryOptions title: AnyUseInfiniteQueryOptions --- # Type Alias: AnyUseInfiniteQueryOptions ```ts type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions; ``` Defined in: [preact-query/src/types.ts:96](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L96) ================================================ FILE: docs/framework/preact/reference/type-aliases/AnyUseMutationOptions.md ================================================ --- id: AnyUseMutationOptions title: AnyUseMutationOptions --- # Type Alias: AnyUseMutationOptions ```ts type AnyUseMutationOptions = UseMutationOptions; ``` Defined in: [preact-query/src/types.ts:191](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L191) ================================================ FILE: docs/framework/preact/reference/type-aliases/AnyUseQueryOptions.md ================================================ --- id: AnyUseQueryOptions title: AnyUseQueryOptions --- # Type Alias: AnyUseQueryOptions ```ts type AnyUseQueryOptions = UseQueryOptions; ``` Defined in: [preact-query/src/types.ts:64](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L64) ================================================ FILE: docs/framework/preact/reference/type-aliases/AnyUseSuspenseInfiniteQueryOptions.md ================================================ --- id: AnyUseSuspenseInfiniteQueryOptions title: AnyUseSuspenseInfiniteQueryOptions --- # Type Alias: AnyUseSuspenseInfiniteQueryOptions ```ts type AnyUseSuspenseInfiniteQueryOptions = UseSuspenseInfiniteQueryOptions; ``` Defined in: [preact-query/src/types.ts:126](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L126) ================================================ FILE: docs/framework/preact/reference/type-aliases/AnyUseSuspenseQueryOptions.md ================================================ --- id: AnyUseSuspenseQueryOptions title: AnyUseSuspenseQueryOptions --- # Type Alias: AnyUseSuspenseQueryOptions ```ts type AnyUseSuspenseQueryOptions = UseSuspenseQueryOptions; ``` Defined in: [preact-query/src/types.ts:75](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L75) ================================================ FILE: docs/framework/preact/reference/type-aliases/DefinedInitialDataInfiniteOptions.md ================================================ --- id: DefinedInitialDataInfiniteOptions title: DefinedInitialDataInfiniteOptions --- # Type Alias: DefinedInitialDataInfiniteOptions\ ```ts type DefinedInitialDataInfiniteOptions = UseInfiniteQueryOptions & object; ``` Defined in: [preact-query/src/infiniteQueryOptions.ts:56](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L56) ## Type Declaration ### initialData ```ts initialData: | NonUndefinedGuard> | () => NonUndefinedGuard> | undefined; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `DefaultError` ### TData `TData` = `InfiniteData`\<`TQueryFnData`\> ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/preact/reference/type-aliases/DefinedInitialDataOptions.md ================================================ --- id: DefinedInitialDataOptions title: DefinedInitialDataOptions --- # Type Alias: DefinedInitialDataOptions\ ```ts type DefinedInitialDataOptions = Omit, "queryFn"> & object; ``` Defined in: [preact-query/src/queryOptions.ts:40](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/queryOptions.ts#L40) ## Type Declaration ### initialData ```ts initialData: | NonUndefinedGuard | () => NonUndefinedGuard; ``` ### queryFn? ```ts optional queryFn: QueryFunction; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/preact/reference/type-aliases/DefinedUseInfiniteQueryResult.md ================================================ --- id: DefinedUseInfiniteQueryResult title: DefinedUseInfiniteQueryResult --- # Type Alias: DefinedUseInfiniteQueryResult\ ```ts type DefinedUseInfiniteQueryResult = DefinedInfiniteQueryObserverResult; ``` Defined in: [preact-query/src/types.ts:178](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L178) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/type-aliases/DefinedUseQueryResult.md ================================================ --- id: DefinedUseQueryResult title: DefinedUseQueryResult --- # Type Alias: DefinedUseQueryResult\ ```ts type DefinedUseQueryResult = DefinedQueryObserverResult; ``` Defined in: [preact-query/src/types.ts:168](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L168) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/type-aliases/QueriesOptions.md ================================================ --- id: QueriesOptions title: QueriesOptions --- # Type Alias: QueriesOptions\ ```ts type QueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? UseQueryOptionsForUseQueries[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueries] : T extends [infer Head, ...(infer Tails)] ? QueriesOptions<[...Tails], [...TResults, GetUseQueryOptionsForUseQueries], [...TDepth, 1]> : ReadonlyArray extends T ? T : T extends UseQueryOptionsForUseQueries[] ? UseQueryOptionsForUseQueries[] : UseQueryOptionsForUseQueries[]; ``` Defined in: [preact-query/src/useQueries.ts:147](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useQueries.ts#L147) QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/preact/reference/type-aliases/QueriesResults.md ================================================ --- id: QueriesResults title: QueriesResults --- # Type Alias: QueriesResults\ ```ts type QueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? UseQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryResult] : T extends [infer Head, ...(infer Tails)] ? QueriesResults<[...Tails], [...TResults, GetUseQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetUseQueryResult }; ``` Defined in: [preact-query/src/useQueries.ts:189](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useQueries.ts#L189) QueriesResults reducer recursively maps type param to results ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/preact/reference/type-aliases/QueryClientProviderProps.md ================================================ --- id: QueryClientProviderProps title: QueryClientProviderProps --- # Type Alias: QueryClientProviderProps ```ts type QueryClientProviderProps = object; ``` Defined in: [preact-query/src/QueryClientProvider.tsx:23](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryClientProvider.tsx#L23) ## Properties ### children? ```ts optional children: ComponentChildren; ``` Defined in: [preact-query/src/QueryClientProvider.tsx:25](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryClientProvider.tsx#L25) *** ### client ```ts client: QueryClient; ``` Defined in: [preact-query/src/QueryClientProvider.tsx:24](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryClientProvider.tsx#L24) ================================================ FILE: docs/framework/preact/reference/type-aliases/QueryErrorClearResetFunction.md ================================================ --- id: QueryErrorClearResetFunction title: QueryErrorClearResetFunction --- # Type Alias: QueryErrorClearResetFunction() ```ts type QueryErrorClearResetFunction = () => void; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:7](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L7) ## Returns `void` ================================================ FILE: docs/framework/preact/reference/type-aliases/QueryErrorIsResetFunction.md ================================================ --- id: QueryErrorIsResetFunction title: QueryErrorIsResetFunction --- # Type Alias: QueryErrorIsResetFunction() ```ts type QueryErrorIsResetFunction = () => boolean; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:6](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L6) ## Returns `boolean` ================================================ FILE: docs/framework/preact/reference/type-aliases/QueryErrorResetBoundaryFunction.md ================================================ --- id: QueryErrorResetBoundaryFunction title: QueryErrorResetBoundaryFunction --- # Type Alias: QueryErrorResetBoundaryFunction() ```ts type QueryErrorResetBoundaryFunction = (value) => ComponentChildren; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:39](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L39) ## Parameters ### value `QueryErrorResetBoundaryValue` ## Returns `ComponentChildren` ================================================ FILE: docs/framework/preact/reference/type-aliases/QueryErrorResetFunction.md ================================================ --- id: QueryErrorResetFunction title: QueryErrorResetFunction --- # Type Alias: QueryErrorResetFunction() ```ts type QueryErrorResetFunction = () => void; ``` Defined in: [preact-query/src/QueryErrorResetBoundary.tsx:5](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryErrorResetBoundary.tsx#L5) ## Returns `void` ================================================ FILE: docs/framework/preact/reference/type-aliases/SuspenseQueriesOptions.md ================================================ --- id: SuspenseQueriesOptions title: SuspenseQueriesOptions --- # Type Alias: SuspenseQueriesOptions\ ```ts type SuspenseQueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? UseSuspenseQueryOptions[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryOptions] : T extends [infer Head, ...(infer Tails)] ? SuspenseQueriesOptions<[...Tails], [...TResults, GetUseSuspenseQueryOptions], [...TDepth, 1]> : unknown[] extends T ? T : T extends UseSuspenseQueryOptions[] ? UseSuspenseQueryOptions[] : UseSuspenseQueryOptions[]; ``` Defined in: [preact-query/src/useSuspenseQueries.ts:109](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useSuspenseQueries.ts#L109) SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/preact/reference/type-aliases/SuspenseQueriesResults.md ================================================ --- id: SuspenseQueriesResults title: SuspenseQueriesResults --- # Type Alias: SuspenseQueriesResults\ ```ts type SuspenseQueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? UseSuspenseQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryResult] : T extends [infer Head, ...(infer Tails)] ? SuspenseQueriesResults<[...Tails], [...TResults, GetUseSuspenseQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetUseSuspenseQueryResult }; ``` Defined in: [preact-query/src/useSuspenseQueries.ts:146](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/useSuspenseQueries.ts#L146) SuspenseQueriesResults reducer recursively maps type param to results ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/preact/reference/type-aliases/UndefinedInitialDataInfiniteOptions.md ================================================ --- id: UndefinedInitialDataInfiniteOptions title: UndefinedInitialDataInfiniteOptions --- # Type Alias: UndefinedInitialDataInfiniteOptions\ ```ts type UndefinedInitialDataInfiniteOptions = UseInfiniteQueryOptions & object; ``` Defined in: [preact-query/src/infiniteQueryOptions.ts:13](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L13) ## Type Declaration ### initialData? ```ts optional initialData: | NonUndefinedGuard> | InitialDataFunction>>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `DefaultError` ### TData `TData` = `InfiniteData`\<`TQueryFnData`\> ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/preact/reference/type-aliases/UndefinedInitialDataOptions.md ================================================ --- id: UndefinedInitialDataOptions title: UndefinedInitialDataOptions --- # Type Alias: UndefinedInitialDataOptions\ ```ts type UndefinedInitialDataOptions = UseQueryOptions & object; ``` Defined in: [preact-query/src/queryOptions.ts:13](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/queryOptions.ts#L13) ## Type Declaration ### initialData? ```ts optional initialData: | InitialDataFunction> | NonUndefinedGuard; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/preact/reference/type-aliases/UnusedSkipTokenInfiniteOptions.md ================================================ --- id: UnusedSkipTokenInfiniteOptions title: UnusedSkipTokenInfiniteOptions --- # Type Alias: UnusedSkipTokenInfiniteOptions\ ```ts type UnusedSkipTokenInfiniteOptions = OmitKeyof, "queryFn"> & object; ``` Defined in: [preact-query/src/infiniteQueryOptions.ts:34](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L34) ## Type Declaration ### queryFn? ```ts optional queryFn: Exclude["queryFn"], SkipToken | undefined>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `DefaultError` ### TData `TData` = `InfiniteData`\<`TQueryFnData`\> ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/preact/reference/type-aliases/UnusedSkipTokenOptions.md ================================================ --- id: UnusedSkipTokenOptions title: UnusedSkipTokenOptions --- # Type Alias: UnusedSkipTokenOptions\ ```ts type UnusedSkipTokenOptions = OmitKeyof, "queryFn"> & object; ``` Defined in: [preact-query/src/queryOptions.ts:25](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/queryOptions.ts#L25) ## Type Declaration ### queryFn? ```ts optional queryFn: Exclude["queryFn"], SkipToken | undefined>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseBaseMutationResult.md ================================================ --- id: UseBaseMutationResult title: UseBaseMutationResult --- # Type Alias: UseBaseMutationResult\ ```ts type UseBaseMutationResult = Override, { mutate: UseMutateFunction; }> & object; ``` Defined in: [preact-query/src/types.ts:220](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L220) ## Type Declaration ### mutateAsync ```ts mutateAsync: UseMutateAsyncFunction; ``` ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseBaseQueryResult.md ================================================ --- id: UseBaseQueryResult title: UseBaseQueryResult --- # Type Alias: UseBaseQueryResult\ ```ts type UseBaseQueryResult = QueryObserverResult; ``` Defined in: [preact-query/src/types.ts:150](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L150) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseInfiniteQueryResult.md ================================================ --- id: UseInfiniteQueryResult title: UseInfiniteQueryResult --- # Type Alias: UseInfiniteQueryResult\ ```ts type UseInfiniteQueryResult = InfiniteQueryObserverResult; ``` Defined in: [preact-query/src/types.ts:173](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L173) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseMutateAsyncFunction.md ================================================ --- id: UseMutateAsyncFunction title: UseMutateAsyncFunction --- # Type Alias: UseMutateAsyncFunction\ ```ts type UseMutateAsyncFunction = MutateFunction; ``` Defined in: [preact-query/src/types.ts:213](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L213) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseMutateFunction.md ================================================ --- id: UseMutateFunction title: UseMutateFunction --- # Type Alias: UseMutateFunction()\ ```ts type UseMutateFunction = (...args) => void; ``` Defined in: [preact-query/src/types.ts:202](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L202) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ## Parameters ### args ...`Parameters`\<`MutateFunction`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>\> ## Returns `void` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseMutationResult.md ================================================ --- id: UseMutationResult title: UseMutationResult --- # Type Alias: UseMutationResult\ ```ts type UseMutationResult = UseBaseMutationResult; ``` Defined in: [preact-query/src/types.ts:237](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L237) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseQueryResult.md ================================================ --- id: UseQueryResult title: UseQueryResult --- # Type Alias: UseQueryResult\ ```ts type UseQueryResult = UseBaseQueryResult; ``` Defined in: [preact-query/src/types.ts:155](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L155) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseSuspenseInfiniteQueryResult.md ================================================ --- id: UseSuspenseInfiniteQueryResult title: UseSuspenseInfiniteQueryResult --- # Type Alias: UseSuspenseInfiniteQueryResult\ ```ts type UseSuspenseInfiniteQueryResult = OmitKeyof, "isPlaceholderData" | "promise">; ``` Defined in: [preact-query/src/types.ts:183](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L183) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/type-aliases/UseSuspenseQueryResult.md ================================================ --- id: UseSuspenseQueryResult title: UseSuspenseQueryResult --- # Type Alias: UseSuspenseQueryResult\ ```ts type UseSuspenseQueryResult = DistributiveOmit, "isPlaceholderData" | "promise">; ``` Defined in: [preact-query/src/types.ts:160](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L160) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/preact/reference/variables/IsRestoringProvider.md ================================================ --- id: IsRestoringProvider title: IsRestoringProvider --- # Variable: IsRestoringProvider ```ts const IsRestoringProvider: Provider = IsRestoringContext.Provider; ``` Defined in: [preact-query/src/IsRestoringProvider.ts:7](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/IsRestoringProvider.ts#L7) ================================================ FILE: docs/framework/preact/reference/variables/QueryClientContext.md ================================================ --- id: QueryClientContext title: QueryClientContext --- # Variable: QueryClientContext ```ts const QueryClientContext: Context; ``` Defined in: [preact-query/src/QueryClientProvider.tsx:5](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/QueryClientProvider.tsx#L5) ================================================ FILE: docs/framework/preact/typescript.md ================================================ --- id: typescript title: TypeScript ref: docs/framework/react/typescript.md replace: { 'react-query': 'preact-query', 'React': 'Preact' } --- ================================================ FILE: docs/framework/react/comparison.md ================================================ --- id: comparison title: Comparison | React Query vs SWR vs Apollo vs RTK Query vs React Router --- > This comparison table strives to be as accurate and as unbiased as possible. If you use any of these libraries and feel the information could be improved, feel free to suggest changes (with notes or evidence of claims) using the "Edit this page on Github" link at the bottom of this page. Feature/Capability Key: - ✅ 1st-class, built-in, and ready to use with no added configuration or code - 🟡 Supported, but as an unofficial 3rd party or community library/contribution - 🔶 Supported and documented, but requires extra user-code to implement - 🛑 Not officially supported or documented. | | React Query | SWR [_(Website)_][swr] | Apollo Client [_(Website)_][apollo] | RTK-Query [_(Website)_][rtk-query] | React Router [_(Website)_][react-router] | | -------------------------------------------------- | ---------------------------------------- | ---------------------------------------- | ------------------------------------------ | ------------------------------------ | ------------------------------------------------------------------------- | | Github Repo / Stars | [![][stars-react-query]][gh-react-query] | [![][stars-swr]][gh-swr] | [![][stars-apollo]][gh-apollo] | [![][stars-rtk-query]][gh-rtk-query] | [![][stars-react-router]][gh-react-router] | | Platform Requirements | React | React | React, GraphQL | Redux | React | | Their Comparison | | (none) | (none) | [Comparison][rtk-query-comparison] | (none) | | Supported Query Syntax | Promise, REST, GraphQL | Promise, REST, GraphQL | GraphQL, Any (Reactive Variables) | Promise, REST, GraphQL | Promise, REST, GraphQL | | Supported Frameworks | React | React | React + Others | Any | React | | Caching Strategy | Hierarchical Key -> Value | Unique Key -> Value | Normalized Schema | Unique Key -> Value | Nested Route -> value | | Cache Key Strategy | JSON | JSON | GraphQL Query | JSON | Route Path | | Cache Change Detection | Deep Compare Keys (Stable Serialization) | Deep Compare Keys (Stable Serialization) | Deep Compare Keys (Unstable Serialization) | Key Referential Equality (===) | Route Change | | Data Change Detection | Deep Comparison + Structural Sharing | Deep Compare (via `stable-hash`) | Deep Compare (Unstable Serialization) | Key Referential Equality (===) | Loader Run | | Data Memoization | Full Structural Sharing | Identity (===) | Normalized Identity | Identity (===) | Identity (===) | | Bundle Size | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr] | [![][bp-apollo]][bpl-apollo] | [![][bp-rtk-query]][bpl-rtk-query] | [![][bp-react-router]][bpl-react-router] + [![][bp-history]][bpl-history] | | API Definition Location | Component, External Config | Component | GraphQL Schema | External Config | Route Tree Configuration | | Queries | ✅ | ✅ | ✅ | ✅ | ✅ | | Cache Persistence | ✅ | ✅ | ✅ | ✅ | 🛑 Active Routes Only 8 | | Devtools | ✅ | ✅ | ✅ | ✅ | 🛑 | | Polling/Intervals | ✅ | ✅ | ✅ | ✅ | 🛑 | | Parallel Queries | ✅ | ✅ | ✅ | ✅ | ✅ | | Dependent Queries | ✅ | ✅ | ✅ | ✅ | ✅ | | Paginated Queries | ✅ | ✅ | ✅ | ✅ | ✅ | | Infinite Queries | ✅ | ✅ | ✅ | ✅ | 🛑 | | Bi-directional Infinite Queries | ✅ | 🔶 | 🔶 | ✅ | 🛑 | | Infinite Query Refetching | ✅ | ✅ | 🛑 | ✅ | 🛑 | | Lagged Query Data1 | ✅ | ✅ | ✅ | ✅ | ✅ | | Selectors | ✅ | 🛑 | ✅ | ✅ | N/A | | Initial Data | ✅ | ✅ | ✅ | ✅ | ✅ | | Scroll Recovery | ✅ | ✅ | ✅ | ✅ | ✅ | | Cache Manipulation | ✅ | ✅ | ✅ | ✅ | 🛑 | | Outdated Query Dismissal | ✅ | ✅ | ✅ | ✅ | ✅ | | Render Batching & Optimization2 | ✅ | ✅ | 🛑 | ✅ | ✅ | | Auto Garbage Collection | ✅ | 🛑 | 🛑 | ✅ | N/A | | Mutation Hooks | ✅ | ✅ | ✅ | ✅ | ✅ | | Offline Mutation Support | ✅ | 🛑 | 🟡 | 🛑 | 🛑 | | Prefetching APIs | ✅ | ✅ | ✅ | ✅ | ✅ | | Query Cancellation | ✅ | 🛑 | 🛑 | 🛑 | ✅ | | Partial Query Matching3 | ✅ | 🔶 | ✅ | ✅ | N/A | | Stale While Revalidate | ✅ | ✅ | ✅ | ✅ | 🛑 | | Stale Time Configuration | ✅ | 🛑7 | 🛑 | ✅ | 🛑 | | Pre-usage Query/Mutation Configuration4 | ✅ | 🛑 | ✅ | ✅ | ✅ | | Window Focus Refetching | ✅ | ✅ | 🛑 | ✅ | 🛑 | | Network Status Refetching | ✅ | ✅ | ✅ | ✅ | 🛑 | | General Cache Dehydration/Rehydration | ✅ | 🛑 | ✅ | ✅ | ✅ | | Offline Caching | ✅ | 🛑 | ✅ | 🔶 | 🛑 | | React Suspense | ✅ | ✅ | ✅ | 🛑 | ✅ | | Abstracted/Agnostic Core | ✅ | 🛑 | ✅ | ✅ | 🛑 | | Automatic Refetch after Mutation5 | 🔶 | 🔶 | ✅ | ✅ | ✅ | | Normalized Caching6 | 🛑 | 🛑 | ✅ | 🛑 | 🛑 | ### Notes > **1 Lagged Query Data** - React Query provides a way to continue to see an existing query's data while the next query loads (similar to the same UX that suspense will soon provide natively). This is extremely important when writing pagination UIs or infinite loading UIs where you do not want to show a hard loading state whenever a new query is requested. Other libraries do not have this capability and render a hard loading state for the new query (unless it has been prefetched), while the new query loads. > **2 Render Optimization** - React Query has excellent rendering performance. By default, it will automatically track which fields are accessed and only re-render if one of them changes. If you would like to opt-out of this optimization, setting `notifyOnChangeProps` to `'all'` will re-render your components whenever the query is updated. For example because it has new data, or to indicate it is fetching. React Query also batches updates together to make sure your application only re-renders once when multiple components are using the same query. If you are only interested in the `data` or `error` properties, you can reduce the number of renders even more by setting `notifyOnChangeProps` to `['data', 'error']`. > **3 Partial query matching** - Because React Query uses deterministic query key serialization, this allows you to manipulate variable groups of queries without having to know each individual query-key that you want to match, eg. you can refetch every query that starts with `todos` in its key, regardless of variables, or you can target specific queries with (or without) variables or nested properties, and even use a filter function to only match queries that pass your specific conditions. > **4 Pre-usage Query Configuration** - This is simply a fancy name for being able to configure how queries and mutations will behave before they are used. For instance, a query can be fully configured with defaults beforehand and when the time comes to use it, only `useQuery({ queryKey })` is necessary, instead of being required to pass the fetcher and/or options with every usage. SWR does have a partial form of this feature by allowing you to pre-configure a default fetcher, but only as a global fetcher, not on a per-query basis and definitely not for mutations. > **5 Automatic Refetch after Mutation** - For truly automatic refetching to happen after a mutation occurs, a schema is necessary (like the one graphQL provides) along with heuristics that help the library know how to identify individual entities and entities types in that schema. > **6 Normalized Caching** - React Query, SWR and RTK-Query do not currently support automatic-normalized caching which describes storing entities in a flat architecture to avoid some high-level data duplication. > **7 SWR's Immutable Mode** - SWR ships with an "immutable" mode that does allow you to only fetch a query once for the life of the cache, but it still does not have the concept of stale-time or conditional auto-revalidation > **8 React Router cache persistence** - React Router does not cache data beyond the currently matched routes. If a route is left, its data is lost. [bpl-react-query]: https://bundlephobia.com/result?p=@tanstack/react-query [bp-react-query]: https://badgen.net/bundlephobia/minzip/@tanstack/react-query?label=💾 [gh-react-query]: https://github.com/TanStack/query [stars-react-query]: https://img.shields.io/github/stars/TanStack/query?label=%F0%9F%8C%9F [swr]: https://github.com/vercel/swr [bp-swr]: https://badgen.net/bundlephobia/minzip/swr?label=💾 [gh-swr]: https://github.com/vercel/swr [stars-swr]: https://img.shields.io/github/stars/vercel/swr?label=%F0%9F%8C%9F [bpl-swr]: https://bundlephobia.com/result?p=swr [apollo]: https://github.com/apollographql/apollo-client [bp-apollo]: https://badgen.net/bundlephobia/minzip/@apollo/client?label=💾 [gh-apollo]: https://github.com/apollographql/apollo-client [stars-apollo]: https://img.shields.io/github/stars/apollographql/apollo-client?label=%F0%9F%8C%9F [bpl-apollo]: https://bundlephobia.com/result?p=@apollo/client [rtk-query]: https://redux-toolkit.js.org/rtk-query/overview [rtk-query-comparison]: https://redux-toolkit.js.org/rtk-query/comparison [rtk-query-bundle-size]: https://redux-toolkit.js.org/rtk-query/comparison#bundle-size [bp-rtk]: https://badgen.net/bundlephobia/minzip/@reduxjs/toolkit?label=💾 [bp-rtk-query]: https://badgen.net/bundlephobia/minzip/@reduxjs/toolkit?label=💾 [gh-rtk-query]: https://github.com/reduxjs/redux-toolkit [stars-rtk-query]: https://img.shields.io/github/stars/reduxjs/redux-toolkit?label=🌟 [bpl-rtk]: https://bundlephobia.com/result?p=@reduxjs/toolkit [bpl-rtk-query]: https://bundlephobia.com/package/@reduxjs/toolkit [react-router]: https://github.com/remix-run/react-router [bp-react-router]: https://badgen.net/bundlephobia/minzip/react-router-dom?label=💾 [gh-react-router]: https://github.com/remix-run/react-router [stars-react-router]: https://img.shields.io/github/stars/remix-run/react-router?label=%F0%9F%8C%9F [bpl-react-router]: https://bundlephobia.com/result?p=react-router-dom [bp-history]: https://badgen.net/bundlephobia/minzip/history?label=💾 [bpl-history]: https://bundlephobia.com/result?p=history ================================================ FILE: docs/framework/react/devtools.md ================================================ --- id: devtools title: Devtools --- Wave your hands in the air and shout hooray because React Query comes with dedicated devtools! 🥳 When you begin your React Query journey, you'll want these devtools by your side. They help visualize all the inner workings of React Query and will likely save you hours of debugging if you find yourself in a pinch! > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > > - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) > - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) > - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) > For React Native users: A third-party native macOS app is available for debugging React Query in ANY js-based application. Monitor queries across devices in real-time. Check it out here: [rn-better-dev-tools](https://github.com/LovesWorking/rn-better-dev-tools) > Note that since version 5, the dev tools support observing mutations as well. ## Install and Import the Devtools The devtools are a separate package that you need to install: ```bash npm i @tanstack/react-query-devtools ``` or ```bash pnpm add @tanstack/react-query-devtools ``` or ```bash yarn add @tanstack/react-query-devtools ``` or ```bash bun add @tanstack/react-query-devtools ``` For Next 13+ App Dir you must install it as a dev dependency for it to work. You can import the devtools like this: ```tsx import { ReactQueryDevtools } from '@tanstack/react-query-devtools' ``` By default, React Query Devtools are only included in bundles when `process.env.NODE_ENV === 'development'`, so you don't need to worry about excluding them during a production build. ## Floating Mode Floating Mode will mount the devtools as a fixed, floating element in your app and provide a toggle in the corner of the screen to show and hide the devtools. This toggle state will be stored and remembered in localStorage across reloads. Place the following code as high in your React app as you can. The closer it is to the root of the page, the better it will work! ```tsx import { ReactQueryDevtools } from '@tanstack/react-query-devtools' function App() { return ( {/* The rest of your application */} ) } ``` ### Options - `initialIsOpen: boolean` - Set this `true` if you want the dev tools to default to being open - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative"` - Defaults to `bottom-right` - The position of the React Query logo to open and close the devtools panel - If `relative`, the button is placed in the location that you render the devtools. - `position?: "top" | "bottom" | "left" | "right"` - Defaults to `bottom` - The position of the React Query devtools panel - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}[]` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ## Embedded Mode Embedded mode will show the development tools as a fixed element in your application, so you can use our panel in your own development tools. Place the following code as high in your React app as you can. The closer it is to the root of the page, the better it will work! ```tsx import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' function App() { const [isOpen, setIsOpen] = React.useState(false) return ( {/* The rest of your application */} {isOpen && setIsOpen(false)} />} ) } ``` ### Options - `style?: React.CSSProperties` - Custom styles for the devtools panel - Default: `{ height: '500px' }` - Example: `{ height: '100%' }` - Example: `{ height: '100%', width: '100%' }` - `onClose?: () => unknown` - Callback function that is called when the devtools panel is closed - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}[]` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ## Devtools in production Devtools are excluded in production builds. However, it might be desirable to lazy load the devtools in production: ```tsx import * as React from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { Example } from './Example' const queryClient = new QueryClient() const ReactQueryDevtoolsProduction = React.lazy(() => import('@tanstack/react-query-devtools/build/modern/production.js').then( (d) => ({ default: d.ReactQueryDevtools, }), ), ) function App() { const [showDevtools, setShowDevtools] = React.useState(false) React.useEffect(() => { // @ts-expect-error window.toggleDevtools = () => setShowDevtools((old) => !old) }, []) return ( {showDevtools && ( )} ) } export default App ``` With this, calling `window.toggleDevtools()` will download the devtools bundle and show them. ### Modern bundlers If your bundler supports package exports, you can use the following import path: ```tsx const ReactQueryDevtoolsProduction = React.lazy(() => import('@tanstack/react-query-devtools/production').then((d) => ({ default: d.ReactQueryDevtools, })), ) ``` For TypeScript, you would need to set `moduleResolution: 'nodenext'` in your tsconfig, which requires at least TypeScript v4.7. ================================================ FILE: docs/framework/react/graphql.md ================================================ --- id: graphql title: GraphQL --- Because React Query's fetching mechanisms are agnostically built on Promises, you can use React Query with literally any asynchronous data fetching client, including GraphQL! > Keep in mind that React Query does not support normalized caching. While a vast majority of users do not actually need a normalized cache or even benefit from it as much as they believe they do, there may be very rare circumstances that may warrant it so be sure to check with us first to make sure it's truly something you need! [//]: # 'Codegen' ## Type-Safety and Code Generation React Query, used in combination with `graphql-request^5` and [GraphQL Code Generator](https://graphql-code-generator.com/) provides full-typed GraphQL operations: ```tsx import request from 'graphql-request' import { useQuery } from '@tanstack/react-query' import { graphql } from './gql/gql' const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ ` query allFilmsWithVariablesQuery($first: Int!) { allFilms(first: $first) { edges { node { id title } } } } `) function App() { // `data` is fully typed! const { data } = useQuery({ queryKey: ['films'], queryFn: async () => request( 'https://swapi-graphql.netlify.app/.netlify/functions/index', allFilmsWithVariablesQueryDocument, // variables are type-checked too! { first: 10 }, ), }) // ... } ``` _You can find a [complete example in the repo](https://github.com/dotansimha/graphql-code-generator/tree/7c25c4eeb77f88677fd79da557b7b5326e3f3950/examples/front-end/react/tanstack-react-query)_ Get started with the [dedicated guide on GraphQL Code Generator documentation](https://www.the-guild.dev/graphql/codegen/docs/guides/react-vue). [//]: # 'Codegen' ================================================ FILE: docs/framework/react/guides/advanced-ssr.md ================================================ --- id: advanced-ssr title: Advanced Server Rendering --- Welcome to the Advanced Server Rendering guide, where you will learn all about using React Query with streaming, Server Components and the Next.js app router. You might want to read the [Server Rendering & Hydration guide](./ssr.md) before this one as it teaches the basics for using React Query with SSR, and [Performance & Request Waterfalls](./request-waterfalls.md) as well as [Prefetching & Router Integration](./prefetching.md) also contains valuable background. Before we start, let's note that while the `initialData` approach outlined in the SSR guide also works with Server Components, we'll focus this guide on the hydration APIs. ## Server Components & Next.js app router We won't cover Server Components in depth here, but the short version is that they are components that are guaranteed to _only_ run on the server, both for the initial page view and **also on page transitions**. This is similar to how Next.js `getServerSideProps`/`getStaticProps` and Remix `loader` works, as these also always run on the server but while those can only return data, Server Components can do a lot more. The data part is central to React Query however, so let's focus on that. How do we take what we learned in the Server Rendering guide about [passing data prefetched in framework loaders to the app](./ssr.md#using-the-hydration-apis) and apply that to Server Components and the Next.js app router? The best way to start thinking about this is to consider Server Components as "just" another framework loader. ### A quick note on terminology So far in these guides, we've been talking about the _server_ and the _client_. It's important to note that confusingly enough this does not match 1-1 with _Server Components_ and _Client Components_. Server Components are guaranteed to only run on the server, but Client Components can actually run in both places. The reason for this is that they can also render during the initial _server rendering_ pass. One way to think of this is that even though Server Components also _render_, they happen during a "loader phase" (always happens on the server), while Client Components run during the "application phase". That application can run both on the server during SSR, and in for example a browser. Where exactly that application runs and if it runs during SSR or not might differ between frameworks. ### Initial setup The first step of any React Query setup is always to create a `queryClient` and wrap your application in a `QueryClientProvider`. With Server Components, this looks mostly the same across frameworks, one difference being the filename conventions: ```tsx // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( {children} ) } ``` ```tsx // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ``` This part is pretty similar to what we did in the SSR guide, we just need to split things up into two different files. ### Prefetching and de/hydrating data Next, let’s look at how to actually prefetch data, then dehydrate and hydrate it. This is what it looked like using the **Next.js Pages Router**: ```tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the , data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( ) } ``` Converting this to the app router actually looks pretty similar, we just need to move things around a bit. First, we'll create a Server Component to do the prefetching part: ```tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. ) } ``` Next, we'll look at what the Client Component part looks like: ```tsx // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to , data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } ``` One neat thing about the examples above is that the only thing that is Next.js-specific here are the file names, everything else would look the same in any other framework that supports Server Components. In the SSR guide, we noted that you could get rid of the boilerplate of having `` in every route. This is not possible with Server Components. > NOTE: If you encounter a type error while using async Server Components with TypeScript versions lower than `5.1.3` and `@types/react` versions lower than `18.2.8`, it is recommended to update to the latest versions of both. Alternatively, you can use the temporary workaround of adding `{/* @ts-expect-error Server Component */}` when calling this component inside another. For more information, see [Async Server Component TypeScript Error](https://nextjs.org/docs/app/building-your-application/configuring/typescript#async-server-component-typescript-error) in the Next.js 13 docs. > NOTE: If you encounter an error `Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.` make sure that you're **not** passing to queryFn a function reference, instead call the function because queryFn args has a bunch of properties and not all of it would be serializable. see [Server Action only works when queryFn isn't a reference](https://github.com/TanStack/query/issues/6264). ### Nesting Server Components A nice thing about Server Components is that they can be nested and exist on many levels in the React tree, making it possible to prefetch data closer to where it's actually used instead of only at the top of the application (just like Remix loaders). This can be as simple as a Server Component rendering another Server Component (we'll leave the Client Components out in this example for brevity): ```tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( ) } ``` As you can see, it's perfectly fine to use `` in multiple places, and create and dehydrate multiple `queryClient` for prefetching. Note that because we are awaiting `getPosts` before rendering `CommentsServerComponent` this would lead to a server side waterfall: ``` 1. |> getPosts() 2. |> getComments() ``` If the server latency to the data is low, this might not be a huge issue, but is still worth pointing out. In Next.js, besides prefetching data in `page.tsx`, you can also do it in `layout.tsx`, and in [parallel routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes). Because these are all part of the routing, Next.js knows how to fetch them all in parallel. So if `CommentsServerComponent` above was instead expressed as a parallel route, the waterfall would be flattened automatically. As more frameworks start supporting Server Components, they might have other routing conventions. Read your framework docs for details. ### Alternative: Use a single `queryClient` for prefetching In the example above, we create a new `queryClient` for each Server Component that fetches data. This is the recommended approach, but if you want to, you can alternatively create a single one that is reused across all Server Components: ```tsx // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient ``` The benefit of this is that you can call `getQueryClient()` to get a hold of this client anywhere that gets called from a Server Component, including utility functions. The downside is that every time you call `dehydrate(getQueryClient())`, you serialize _the entire_ `queryClient`, including queries that have already been serialized before and are unrelated to the current Server Component which is unnecessary overhead. Next.js already dedupes requests that utilize `fetch()`, but if you are using something else in your `queryFn`, or if you use a framework that does _not_ dedupe these requests automatically, using a single `queryClient` as described above might make sense, despite the duplicated serialization. > As a future improvement, we might look into creating a `dehydrateNew()` function (name pending) that only dehydrate queries that are _new_ since the last call to `dehydrateNew()`. Feel free to get in touch if this sounds interesting and like something you want to help out with! ### Data ownership and revalidation With Server Components, it's important to think about data ownership and revalidation. To explain why, let's look at a modified example from above: ```tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( {/* This is the new part */}
    Nr of posts: {posts.length}
    ) } ``` We are now rendering data from the `getPosts` query both in a Server Component and in a Client Component. This will be fine for the initial page render, but what happens when the query revalidates on the client for some reason when `staleTime` has been passed? React Query has no idea of how to _revalidate the Server Component_, so if it refetches the data on the client, causing React to rerender the list of posts, the `Nr of posts: {posts.length}` will end up out of sync. This is fine if you set `staleTime: Infinity`, so that React Query never revalidates, but this is probably not what you want if you are using React Query in the first place. Using React Query with Server Components makes most sense if: - You have an app using React Query and want to migrate to Server Components without rewriting all the data fetching - You want a familiar programming paradigm, but want to still sprinkle in the benefits of Server Components where it makes most sense - You have some use case that React Query covers, but that your framework of choice does not cover It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. **If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it.** This might be never, and that's fine, use the right tool for the job! If you do use it, a good rule of thumb is to avoid `queryClient.fetchQuery` unless you need to catch errors. If you do use it, don't render its result on the server or pass the result to another component, even a Client Component one. From the React Query perspective, treat Server Components as a place to prefetch data, nothing more. Of course, it's fine to have Server Components own some data, and Client Components own other, just make sure those two realities don't get out of sync. ## Streaming with Server Components The Next.js app router automatically streams any part of the application that is ready to be displayed to the browser as soon as possible, so finished content can be displayed immediately without waiting for still pending content. It does this along `` boundary lines. Note that if you create a file `loading.tsx`, this automatically creates a `` boundary behind the scenes. With the prefetching patterns described above, React Query is perfectly compatible with this form of streaming. As the data for each Suspense boundary resolves, Next.js can render and stream the finished content to the browser. This works even if you are using `useQuery` as outlined above because the suspending actually happens when you `await` the prefetch. As of React Query v5.40.0, you don't have to `await` all prefetches for this to work, as `pending` Queries can also be dehydrated and sent to the client. This lets you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streams the _data_ to the client as the query finishes. This can be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to `await` and render the first page of an infinite query, but start prefetching page 2 without blocking rendering. To make this work, we have to instruct the `queryClient` to also `dehydrate` pending Queries. We can do this globally, or by passing that option directly to `dehydrate`. We will also need to move the `getQueryClient()` function out of our `app/providers.tsx` file as we want to use it in our server component and our client provider. ```tsx // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } ``` > Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components. Then, all we need to do is provide a `HydrationBoundary`, but we don't need to `await` prefetches anymore: ```tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( ) } ``` On the client, the Promise will be put into the QueryCache for us. That means we can now call `useSuspenseQuery` inside the `Posts` component to "use" that Promise (which was created on the Server): ```tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } ``` > Note that you could also `useQuery` instead of `useSuspenseQuery`, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the `pending` status, which also opts out of server rendering the content. If you're using non-JSON data types and serialize the query results on the server, you can specify the `dehydrate.serializeData` and `hydrate.deserializeData` options to serialize and deserialize the data on each side of the boundary to ensure the data in the cache is the same format both on the server and the client: ```tsx // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... ``` ```tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( ) } ``` ```tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } ``` Now, your `getPosts` function can return e.g. `Temporal` datetime objects and the data will be serialized and deserialized on the client, assuming your transformer can serialize and deserialize those data types. For more information, check out the [Next.js App with Prefetching Example](../examples/nextjs-app-prefetching). ### Using the Persist Adapter with Streaming If you're using the persist adapter with the [Streaming with Server Components](#streaming-with-server-components) feature, you need to be careful not to save promises to storage. Since pending queries can be dehydrated and streamed to the client, you should configure the persister to only persist successful queries: ```tsx {children} ``` This ensures that only successfully resolved queries are persisted to storage, preventing serialization issues with pending promises. ## Experimental streaming without prefetching in Next.js While we recommend the prefetching solution detailed above because it flattens request waterfalls both on the initial page load **and** any subsequent page navigation, there is an experimental way to skip prefetching altogether and still have streaming SSR work: `@tanstack/react-query-next-experimental` This package will allow you to fetch data on the server (in a Client Component) by just calling `useSuspenseQuery` in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. If you call `useSuspenseQuery` without wrapping it in a `` boundary, the HTML response won't start until the fetch resolves. This can be when you want depending on the situation, but keep in mind that this will hurt your TTFB. To achieve this, wrap your app in the `ReactQueryStreamedHydration` component: ```tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( {props.children} ) } ``` For more information, check out the [NextJs Suspense Streaming Example](../examples/nextjs-suspense-streaming). The big upside is that you no longer need to prefetch queries manually to have SSR work, and it even still streams in the result! This gives you phenomenal DX and lower code complexity. The downside is easiest to explain if we look back at [the complex request waterfall example](./request-waterfalls.md#code-splitting) in the Performance & Request Waterfalls guide. Server Components with prefetching effectively eliminates the request waterfalls both for the initial page load **and** any subsequent navigation. This prefetch-less approach however will only flatten the waterfalls on the initial page load but ends up the same deep waterfall as the original example on page navigations: ``` 1. |> JS for 2. |> getFeed() 3. |> JS for 4. |> getGraphDataById() ``` This is even worse than with `getServerSideProps`/`getStaticProps`, since with those we could at least parallelize data- and code-fetching. If you value DX/iteration/shipping speed with low code complexity over performance, don't have deeply nested queries, or are on top of your request waterfalls with parallel fetching using tools like `useSuspenseQueries`, this can be a good tradeoff. > It might be possible to combine the two approaches, but even we haven't tried that out yet. If you do try this, please report back your findings, or even update these docs with some tips! ## Final words Server Components and streaming are still fairly new concepts and we are still figuring out how React Query fits in and what improvements we can make to the API. We welcome suggestions, feedback and bug reports! Similarly, it would be impossible to teach all the intricacies of this new paradigm all in one guide, on the first try. If you are missing some piece of information here or have suggestions on how to improve this content, also get in touch, or even better, click the "Edit on GitHub" button below and help us out. [//]: # 'Materials' ## Further reading To understand if your application can benefit from React Query when also using Server Components, see the article [You Might Not Need React Query](https://tkdodo.eu/blog/you-might-not-need-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/background-fetching-indicators.md ================================================ --- id: background-fetching-indicators title: Background Fetching Indicators --- A query's `status === 'pending'` state is sufficient enough to show the initial hard-loading state for a query, but sometimes you may want to display an additional indicator that a query is refetching in the background. To do this, queries also supply you with an `isFetching` boolean that you can use to show that it's in a fetching state, regardless of the state of the `status` variable: [//]: # 'Example' ```tsx function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( Loading... ) : status === 'error' ? ( Error: {error.message} ) : ( <> {isFetching ?
    Refreshing...
    : null}
    {todos.map((todo) => ( ))}
    ) } ``` [//]: # 'Example' ## Displaying Global Background Fetching Loading State In addition to individual query loading states, if you would like to show a global loading indicator when **any** queries are fetching (including in the background), you can use the `useIsFetching` hook: [//]: # 'Example2' ```tsx import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? (
    Queries are fetching in the background...
    ) : null } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/react/guides/caching.md ================================================ --- id: caching title: Caching Examples --- > Please thoroughly read the [Important Defaults](./important-defaults.md) before reading this guide ## Basic Example This caching example illustrates the story and lifecycle of: - Query Instances with and without cache data - Background Refetching - Inactive Queries - Garbage Collection Let's assume we are using the default `gcTime` of **5 minutes** and the default `staleTime` of `0`. - A new instance of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` mounts. - Since no other queries have been made with the `['todos']` query key, this query will show a hard loading state and make a network request to fetch the data. - When the network request has completed, the returned data will be cached under the `['todos']` key. - The data will be marked as stale after the configured `staleTime` (defaults to `0`, or immediately). - A second instance of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` mounts elsewhere. - Since the cache already has data for the `['todos']` key from the first query, that data is immediately returned from the cache. - The new instance triggers a new network request using its query function. - Note that regardless of whether both `fetchTodos` query functions are identical or not, both queries' [`status`](../reference/useQuery.md) are updated (including `isFetching`, `isPending`, and other related values) because they have the same query key. - When the request completes successfully, the cache's data under the `['todos']` key is updated with the new data, and both instances are updated with the new data. - Both instances of the `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` query are unmounted and no longer in use. - Since there are no more active instances of this query, a garbage collection timeout is set using `gcTime` to delete and garbage collect the query (defaults to **5 minutes**). - Before the cache timeout (gcTime) has completed, another instance of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` mounts. The query immediately returns the available cached data while the `fetchTodos` function is being run in the background. When it completes successfully, it will populate the cache with fresh data. - The final instance of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` unmounts. - No more instances of `useQuery({ queryKey: ['todos'], queryFn: fetchTodos })` appear within **5 minutes**. - The cached data under the `['todos']` key is deleted and garbage collected. ================================================ FILE: docs/framework/react/guides/default-query-function.md ================================================ --- id: default-query-function title: Default Query Function --- If you find yourself wishing for whatever reason that you could just share the same query function for your entire app and just use query keys to identify what it should fetch, you can do that by providing a **default query function** to TanStack Query: [//]: # 'Example' ```tsx // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } ``` [//]: # 'Example' If you ever want to override the default queryFn, you can just provide your own like you normally would. ================================================ FILE: docs/framework/react/guides/dependent-queries.md ================================================ --- id: dependent-queries title: Dependent Queries --- ## useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the `enabled` option to tell a query when it is ready to run: [//]: # 'Example' ```tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) ``` [//]: # 'Example' The `projects` query will start in: ```tsx status: 'pending' isPending: true fetchStatus: 'idle' ``` As soon as the `user` is available, the `projects` query will be `enabled` and will then transition to: ```tsx status: 'pending' isPending: true fetchStatus: 'fetching' ``` Once we have the projects, it will go to: ```tsx status: 'success' isPending: false fetchStatus: 'idle' ``` ## useQueries dependent Query Dynamic parallel query - `useQueries` can depend on a previous query also, here's how to achieve this: [//]: # 'Example2' ```tsx // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if userIds is undefined, an empty array will be returned }) ``` [//]: # 'Example2' **Note** that `useQueries` return an **array of query results** ## A note about performance Dependent queries by definition constitutes a form of [request waterfall](./request-waterfalls.md), which hurts performance. If we pretend both queries take the same amount of time, doing them serially instead of in parallel always takes twice as much time, which is especially hurtful when it happens on a client that has high latency. If you can, it's always better to restructure the backend APIs so that both queries can be fetched in parallel, though that might not always be practically feasible. In the example above, instead of first fetching `getUserByEmail` to be able to `getProjectsByUser`, introducing a new `getProjectsByUserEmail` query would flatten the waterfall. ================================================ FILE: docs/framework/react/guides/disabling-queries.md ================================================ --- id: disabling-queries title: Disabling/Pausing Queries --- If you ever want to disable a query from automatically running, you can use the `enabled = false` option. The enabled option also accepts a callback that returns a boolean. When `enabled` is `false`: - If the query has cached data, then the query will be initialized in the `status === 'success'` or `isSuccess` state. - If the query does not have cached data, then the query will start in the `status === 'pending'` and `fetchStatus === 'idle'` state. - The query will not automatically fetch on mount. - The query will not automatically refetch in the background. - The query will ignore query client `invalidateQueries` and `refetchQueries` calls that would normally result in the query refetching. - `refetch` returned from `useQuery` can be used to manually trigger the query to fetch. However, it will not work with `skipToken`. > TypeScript users may prefer to use [skipToken](#typesafe-disabling-of-queries-using-skiptoken) as an alternative to `enabled = false`. [//]: # 'Example' ```tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return (
    {data ? (
      {data.map((todo) => (
    • {todo.title}
    • ))}
    ) : isError ? ( Error: {error.message} ) : isLoading ? ( Loading... ) : ( Not ready ... )}
    {isFetching ? 'Fetching...' : null}
    ) } ``` [//]: # 'Example' Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to `refetch`. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: [//]: # 'Example2' ```tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return (
    // 🚀 applying the filter will enable and execute the query {data && }
    ) } ``` [//]: # 'Example2' ### isLoading (Previously: `isInitialLoading`) Lazy queries will be in `status: 'pending'` right from the start because `pending` means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the `isLoading` flag instead. It's a derived flag that is computed from: `isPending && isFetching` so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using `skipToken` If you are using TypeScript, you can use the `skipToken` to disable a query. This is useful when you want to disable a query based on a condition, but you still want the query to be type safe. > **IMPORTANT**: `refetch` from `useQuery` will not work with `skipToken`. Calling `refetch()` on a query that uses `skipToken` will result in a `Missing queryFn` error because there is no valid query function to execute. If you need to manually trigger queries, consider using `enabled: false` instead, which allows `refetch()` to work properly. Other than this limitation, `skipToken` works the same as `enabled: false`. [//]: # 'Example3' ```tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return (
    // 🚀 applying the filter will enable and execute the query {data && }
    ) } ``` [//]: # 'Example3' ================================================ FILE: docs/framework/react/guides/does-this-replace-client-state.md ================================================ --- id: does-this-replace-client-state title: Does TanStack Query replace Redux, MobX or other global state managers? --- Well, let's start with a few important items: - TanStack Query is a **server-state** library, responsible for managing asynchronous operations between your server and client - Redux, MobX, Zustand, etc. are **client-state** libraries that _can be used to store asynchronous data, albeit inefficiently when compared to a tool like TanStack Query_ With those points in mind, the short answer is that TanStack Query **replaces the boilerplate code and related wiring used to manage cache data in your client-state and replaces it with just a few lines of code.** For a vast majority of applications, the truly **globally accessible client state** that is left over after migrating all of your async code to TanStack Query is usually very tiny. > There are still some circumstances where an application might indeed have a massive amount of synchronous client-only state (like a visual designer or music production application), in which case, you will probably still want a client state manager. In this situation it's important to note that **TanStack Query is not a replacement for local/client state management**. However, you can use TanStack Query alongside most client state managers with zero issues. ## A Contrived Example Here we have some "global" state being managed by a global state library: ```tsx const globalState = { projects, teams, tasks, users, themeMode, sidebarStatus, } ``` Currently, the global state manager is caching 4 types of server-state: `projects`, `teams`, `tasks`, and `users`. If we were to move these server-state assets to TanStack Query, our remaining global state would look more like this: ```tsx const globalState = { themeMode, sidebarStatus, } ``` This also means that with a few hook calls to `useQuery` and `useMutation`, we also get to remove any boilerplate code that was used to manage our server state e.g. - Connectors - Action Creators - Middlewares - Reducers - Loading/Error/Result states - Contexts With all of those things removed, you may ask yourself, **"Is it worth it to keep using our client state manager for this tiny global state?"** **And that's up to you!** But TanStack Query's role is clear. It removes asynchronous wiring and boilerplate from your application and replaces it with just a few lines of code. What are you waiting for, give it a go already! ================================================ FILE: docs/framework/react/guides/filters.md ================================================ --- id: filters title: Filters --- Some methods within TanStack Query accept a `QueryFilters` or `MutationFilters` object. ## `Query Filters` A query filter is an object with certain conditions to match a query with: ```tsx // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) ``` A query filter object supports the following properties: - `queryKey?: QueryKey` - Set this property to define a query key to match on. - `exact?: boolean` - If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed. - `type?: 'active' | 'inactive' | 'all'` - Defaults to `all` - When set to `active` it will match active queries. - When set to `inactive` it will match inactive queries. - `stale?: boolean` - When set to `true` it will match stale queries. - When set to `false` it will match fresh queries. - `fetchStatus?: FetchStatus` - When set to `fetching` it will match queries that are currently fetching. - When set to `paused` it will match queries that wanted to fetch, but have been `paused`. - When set to `idle` it will match queries that are not fetching. - `predicate?: (query: Query) => boolean` - This predicate function will be used as a final filter on all matching queries. If no other filters are specified, this function will be evaluated against every query in the cache. ## `Mutation Filters` A mutation filter is an object with certain conditions to match a mutation with: ```tsx // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) ``` A mutation filter object supports the following properties: - `mutationKey?: MutationKey` - Set this property to define a mutation key to match on. - `exact?: boolean` - If you don't want to search mutations inclusively by mutation key, you can pass the `exact: true` option to return only the mutation with the exact mutation key you have passed. - `status?: MutationStatus` - Allows for filtering mutations according to their status. - `predicate?: (mutation: Mutation) => boolean` - This predicate function will be used as a final filter on all matching mutations. If no other filters are specified, this function will be evaluated against every mutation in the cache. ## Utils ### `matchQuery` ```tsx const isMatching = matchQuery(filters, query) ``` Returns a boolean that indicates whether a query matches the provided set of query filters. ### `matchMutation` ```tsx const isMatching = matchMutation(filters, mutation) ``` Returns a boolean that indicates whether a mutation matches the provided set of mutation filters. ================================================ FILE: docs/framework/react/guides/important-defaults.md ================================================ --- id: important-defaults title: Important Defaults --- Out of the box, TanStack Query is configured with **aggressive but sane** defaults. **Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.** Keep them in mind as you continue to learn and use TanStack Query: - Query instances via `useQuery` or `useInfiniteQuery` by default **consider cached data as stale**. > To change this behavior, you can configure your queries both globally and per-query using the `staleTime` option. Specifying a longer `staleTime` means queries will not refetch their data as often - A Query that has a `staleTime` set is considered **fresh** until that `staleTime` has elapsed. - set `staleTime` to e.g. `2 * 60 * 1000` to make sure data is read from the cache, without triggering any kinds of refetches, for 2 minutes, or until the Query is [invalidated manually](./query-invalidation.md). - set `staleTime` to `Infinity` to never trigger a refetch until the Query is [invalidated manually](./query-invalidation.md). - set `staleTime` to `'static'` to **never** trigger a refetch, even if the Query is [invalidated manually](./query-invalidation.md). - Stale queries are refetched automatically in the background when: - New instances of the query mount - The window is refocused - The network is reconnected > Setting `staleTime` is the recommended way to avoid excessive refetches, but you can also customize the points in time for refetches by setting options like `refetchOnMount`, `refetchOnWindowFocus` and `refetchOnReconnect`. - Queries can optionally be configured with a `refetchInterval` to trigger refetches periodically, which is independent of the `staleTime` setting. - Query results that have no more active instances of `useQuery`, `useInfiniteQuery` or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time. - By default, "inactive" queries are garbage collected after **5 minutes**. > To change this, you can alter the default `gcTime` for queries to something other than `1000 * 60 * 5` milliseconds. - Queries that fail are **silently retried 3 times, with exponential backoff delay** before capturing and displaying an error to the UI. > To change this, you can alter the default `retry` and `retryDelay` options for queries to something other than `3` and the default exponential backoff function. [//]: # 'StructuralSharing' - Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. [//]: # 'StructuralSharing' > Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can provide your own custom function as `config.structuralSharing` to compute a value from the old and new responses, retaining references as required. [//]: # 'Materials' ## Further Reading Have a look at the following articles from our [Community Resources](../../../community-resources) for further explanations of the defaults: - [Practical React Query](https://tkdodo.eu/blog/practical-react-query) - [React Query as a State Manager](https://tkdodo.eu/blog/react-query-as-a-state-manager) - [Thinking in React Query](https://tkdodo.eu/blog/thinking-in-react-query) [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/infinite-queries.md ================================================ --- id: infinite-queries title: Infinite Queries --- Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of `useQuery` called `useInfiniteQuery` for querying these types of lists. When using `useInfiniteQuery`, you'll notice a few things are different: - `data` is now an object containing infinite query data: - `data.pages` array containing the fetched pages - `data.pageParams` array containing the page params used to fetch the pages - The `fetchNextPage` and `fetchPreviousPage` functions are now available (`fetchNextPage` is required) - The `initialPageParam` option is now available (and required) to specify the initial page param - The `getNextPageParam` and `getPreviousPageParam` options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function - A `hasNextPage` boolean is now available and is `true` if `getNextPageParam` returns a value other than `null` or `undefined` - A `hasPreviousPage` boolean is now available and is `true` if `getPreviousPageParam` returns a value other than `null` or `undefined` - The `isFetchingNextPage` and `isFetchingPreviousPage` booleans are now available to distinguish between a background refresh state and a loading more state > Note: Options `initialData` or `placeholderData` need to conform to the same structure of an object with `data.pages` and `data.pageParams` properties. ## Example Let's assume we have an API that returns pages of `projects` 3 at a time based on a `cursor` index along with a cursor that can be used to fetch the next group of projects: ```tsx fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } ``` With this information, we can create a "Load More" UI by: - Waiting for `useInfiniteQuery` to request the first group of data by default - Returning the information for the next query in `getNextPageParam` - Calling `fetchNextPage` function [//]: # 'Example' ```tsx import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? (

    Loading...

    ) : status === 'error' ? (

    Error: {error.message}

    ) : ( <> {data.pages.map((group, i) => ( {group.data.map((project) => (

    {project.name}

    ))}
    ))}
    {isFetching && !isFetchingNextPage ? 'Fetching...' : null}
    ) } ``` [//]: # 'Example' It's essential to understand that calling `fetchNextPage` while an ongoing fetch is in progress runs the risk of overwriting data refreshes happening in the background. This situation becomes particularly critical when rendering a list and triggering `fetchNextPage` simultaneously. Remember, there can only be a single ongoing fetch for an InfiniteQuery. A single cache entry is shared for all pages, attempting to fetch twice simultaneously might lead to data overwrites. If you intend to enable simultaneous fetching, you can utilize the `{ cancelRefetch: false }` option (default: true) within `fetchNextPage`. To ensure a seamless querying process without conflicts, it's highly recommended to verify that the query is not in an `isFetching` state, especially if the user won't directly control that call. [//]: # 'Example1' ```jsx hasNextPage && !isFetching && fetchNextPage()} /> ``` [//]: # 'Example1' ## What happens when an infinite query needs to be refetched? When an infinite query becomes `stale` and needs to be refetched, each group is fetched `sequentially`, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested. ## What if I want to implement a bi-directional infinite list? Bi-directional lists can be implemented by using the `getPreviousPageParam`, `fetchPreviousPage`, `hasPreviousPage` and `isFetchingPreviousPage` properties and functions. [//]: # 'Example3' ```tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) ``` [//]: # 'Example3' ## What if I want to show the pages in reversed order? Sometimes you may want to show the pages in reversed order. If this is case, you can use the `select` option: [//]: # 'Example4' ```tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) ``` [//]: # 'Example4' ## What if I want to manually update the infinite query? ### Manually removing first page: [//]: # 'Example5' ```tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) ``` [//]: # 'Example5' ### Manually removing a single value from an individual page: [//]: # 'Example6' ```tsx const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) ``` [//]: # 'Example6' ### Keep only the first page: [//]: # 'Example7' ```tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) ``` [//]: # 'Example7' Make sure to always keep the same data structure of pages and pageParams! ## What if I want to limit the number of pages? In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX: - when the user can load a large number of pages (memory usage) - when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched) The solution is to use a "Limited Infinite Query". This is made possible by using the `maxPages` option in conjunction with `getNextPageParam` and `getPreviousPageParam` to allow fetching pages when needed in both directions. In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially. [//]: # 'Example8' ```tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) ``` [//]: # 'Example8' ## What if my API doesn't return a cursor? If your API doesn't return a cursor, you can use the `pageParam` as a cursor. Because `getNextPageParam` and `getPreviousPageParam` also get the `pageParam`of the current page, you can use it to calculate the next / previous page param. [//]: # 'Example9' ```tsx return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) ``` [//]: # 'Example9' [//]: # 'Materials' ## Further reading To get a better understanding of how Infinite Queries work under the hood, see the article [How Infinite Queries work](https://tkdodo.eu/blog/how-infinite-queries-work). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/initial-query-data.md ================================================ --- id: initial-query-data title: Initial Query Data --- There are many ways to supply initial data for a query to the cache before you need it: - Declaratively: - Provide `initialData` to a query to prepopulate its cache if empty - Imperatively: - [Prefetch the data using `queryClient.prefetchQuery`](./prefetching.md) - [Manually place the data into the cache using `queryClient.setQueryData`](./prefetching.md) ## Using `initialData` to prepopulate a query There may be times when you already have the initial data for a query available in your app and can simply provide it directly to your query. If and when this is the case, you can use the `config.initialData` option to set the initial data for a query and skip the initial loading state! > IMPORTANT: `initialData` is persisted to the cache, so it is not recommended to provide placeholder, partial or incomplete data to this option and instead use `placeholderData` [//]: # 'Example' ```tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) ``` [//]: # 'Example' ### `staleTime` and `initialDataUpdatedAt` By default, `initialData` is treated as totally fresh, as if it were just fetched. This also means that it will affect how it is interpreted by the `staleTime` option. - If you configure your query observer with `initialData`, and no `staleTime` (the default `staleTime: 0`), the query will immediately refetch when it mounts: [//]: # 'Example2' ```tsx // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) ``` [//]: # 'Example2' - If you configure your query observer with `initialData` and a `staleTime` of `1000` ms, the data will be considered fresh for that same amount of time, as if it was just fetched from your query function. [//]: # 'Example3' ```tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) ``` [//]: # 'Example3' - So what if your `initialData` isn't totally fresh? That leaves us with the last configuration that is actually the most accurate and uses an option called `initialDataUpdatedAt`. This option allows you to pass a numeric JS timestamp in milliseconds of when the initialData itself was last updated, e.g. what `Date.now()` provides. Take note that if you have a unix timestamp, you'll need to convert it to a JS timestamp by multiplying it by `1000`. [//]: # 'Example4' ```tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) ``` [//]: # 'Example4' This option allows the staleTime to be used for its original purpose, determining how fresh the data needs to be, while also allowing the data to be refetched on mount if the `initialData` is older than the `staleTime`. In the example above, our data needs to be fresh within 1 minute, and we can hint to the query when the initialData was last updated so the query can decide for itself whether the data needs to be refetched again or not. > If you would rather treat your data as **prefetched data**, we recommend that you use the `prefetchQuery` or `fetchQuery` APIs to populate the cache beforehand, thus letting you configure your `staleTime` independently from your initialData ### Initial Data Function If the process for accessing a query's initial data is intensive or just not something you want to perform on every render, you can pass a function as the `initialData` value. This function will be executed only once when the query is initialized, saving you precious memory and/or CPU: [//]: # 'Example5' ```tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) ``` [//]: # 'Example5' ### Initial Data from Cache In some circumstances, you may be able to provide the initial data for a query from the cached result of another. A good example of this would be searching the cached data from a todos list query for an individual todo item, then using that as the initial data for your individual todo query: [//]: # 'Example6' ```tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) ``` [//]: # 'Example6' ### Initial Data from the cache with `initialDataUpdatedAt` Getting initial data from the cache means the source query you're using to look up the initial data from is likely old. Instead of using an artificial `staleTime` to keep your query from refetching immediately, it's suggested that you pass the source query's `dataUpdatedAt` to `initialDataUpdatedAt`. This provides the query instance with all the information it needs to determine if and when the query needs to be refetched, regardless of initial data being provided. [//]: # 'Example7' ```tsx const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) ``` [//]: # 'Example7' ### Conditional Initial Data from Cache If the source query you're using to look up the initial data from is old, you may not want to use the cached data at all and just fetch from the server. To make this decision easier, you can use the `queryClient.getQueryState` method instead to get more information about the source query, including a `state.dataUpdatedAt` timestamp you can use to decide if the query is "fresh" enough for your needs: [//]: # 'Example8' ```tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) ``` [//]: # 'Example8' [//]: # 'Materials' ## Further reading For a comparison between `Initial Data` and `Placeholder Data`, see the [article by TkDodo](https://tkdodo.eu/blog/placeholder-and-initial-data-in-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/invalidations-from-mutations.md ================================================ --- id: invalidations-from-mutations title: Invalidations from Mutations --- Invalidating queries is only half the battle. Knowing **when** to invalidate them is the other half. Usually when a mutation in your app succeeds, it's VERY likely that there are related queries in your application that need to be invalidated and possibly refetched to account for the new changes from your mutation. For example, assume we have a mutation to post a new todo: [//]: # 'Example' ```tsx const mutation = useMutation({ mutationFn: postTodo }) ``` [//]: # 'Example' When a successful `postTodo` mutation happens, we likely want all `todos` queries to get invalidated and possibly refetched to show the new todo item. To do this, you can use `useMutation`'s `onSuccess` options and the `client`'s `invalidateQueries` function: [//]: # 'Example2' ```tsx import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: async () => { // If you're invalidating a single query await queryClient.invalidateQueries({ queryKey: ['todos'] }) // If you're invalidating multiple queries await Promise.all([ queryClient.invalidateQueries({ queryKey: ['todos'] }), queryClient.invalidateQueries({ queryKey: ['reminders'] }), ]) }, }) ``` [//]: # 'Example2' Returning a Promise on `onSuccess` makes sure the data is updated before the mutation is entirely complete (i.e., isPending is true until onSuccess is fulfilled) [//]: # 'Example2' You can wire up your invalidations to happen using any of the callbacks available in the [`useMutation` hook](./mutations.md) [//]: # 'Materials' ## Further reading For a technique to automatically invalidate Queries after Mutations, have a look at [TkDodo's article on Automatic Query Invalidation after Mutations](https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/migrating-to-react-query-3.md ================================================ --- id: migrating-to-react-query-3 title: Migrating to React Query 3 --- Previous versions of React Query were awesome and brought some amazing new features, more magic, and an overall better experience to the library. They also brought on massive adoption and likewise a lot of refining fire (issues/contributions) to the library and brought to light a few things that needed more polish to make the library even better. v3 contains that very polish. ## Overview - More scalable and testable cache configuration - Better SSR support - Data-lag (previously usePaginatedQuery) anywhere! - Bi-directional Infinite Queries - Query data selectors! - Fully configure defaults for queries and/or mutations before use - More granularity for optional rendering optimization - New `useQueries` hook! (Variable-length parallel query execution) - Query filter support for the `useIsFetching()` hook! - Retry/offline/replay support for mutations - Observe queries/mutations outside of React - Use the React Query core logic anywhere you want! - Bundled/Colocated Devtools via `react-query/devtools` - Cache Persistence to web storage (experimental via `react-query/persistQueryClient-experimental` and `react-query/createWebStoragePersistor-experimental`) ## Breaking Changes ### The `QueryCache` has been split into a `QueryClient` and lower-level `QueryCache` and `MutationCache` instances. The `QueryCache` contains all queries, the `MutationCache` contains all mutations, and the `QueryClient` can be used to set configuration and to interact with them. This has some benefits: - Allows for different types of caches. - Multiple clients with different configurations can use the same cache. - Clients can be used to track queries, which can be used for shared caches on SSR. - The client API is more focused towards general usage. - Easier to test the individual components. When creating a `new QueryClient()`, a `QueryCache` and `MutationCache` are automatically created for you if you don't supply them. ```tsx import { QueryClient } from 'react-query' const queryClient = new QueryClient() ``` ### `ReactQueryConfigProvider` and `ReactQueryCacheProvider` have both been replaced by `QueryClientProvider` Default options for queries and mutations can now be specified in `QueryClient`: **Notice that it's now defaultOptions instead of defaultConfig** ```tsx const queryClient = new QueryClient({ defaultOptions: { queries: { // query options }, mutations: { // mutation options }, }, }) ``` The `QueryClientProvider` component is now used to connect a `QueryClient` to your application: ```tsx import { QueryClient, QueryClientProvider } from 'react-query' const queryClient = new QueryClient() function App() { return ... } ``` ### The default `QueryCache` is gone. **For real this time!** As previously noted with a deprecation, there is no longer a default `QueryCache` that is created or exported from the main package. **You must create your own via `new QueryClient()` or `new QueryCache()` (which you can then pass to `new QueryClient({ queryCache })` )** ### The deprecated `makeQueryCache` utility has been removed. It's been a long time coming, but it's finally gone :) ### `QueryCache.prefetchQuery()` has been moved to `QueryClient.prefetchQuery()` The new `QueryClient.prefetchQuery()` function is async, but **does not return the data from the query**. If you require the data, use the new `QueryClient.fetchQuery()` function ```tsx // Prefetch a query: await queryClient.prefetchQuery('posts', fetchPosts) // Fetch a query: try { const data = await queryClient.fetchQuery('posts', fetchPosts) } catch (error) { // Error handling } ``` ### `ReactQueryErrorResetBoundary` and `QueryCache.resetErrorBoundaries()` have been replaced by `QueryErrorResetBoundary` and `useQueryErrorResetBoundary()`. Together, these provide the same experience as before, but with added control to choose which component trees you want to reset. For more information, see: - [QueryErrorResetBoundary](../reference/QueryErrorResetBoundary.md) - [useQueryErrorResetBoundary](../reference/useQueryErrorResetBoundary.md) ### `QueryCache.getQuery()` has been replaced by `QueryCache.find()`. `QueryCache.find()` should now be used to look up individual queries from a cache ### `QueryCache.getQueries()` has been moved to `QueryCache.findAll()`. `QueryCache.findAll()` should now be used to look up multiple queries from a cache ### `QueryCache.isFetching` has been moved to `QueryClient.isFetching()`. **Notice that it's now a function instead of a property** ### The `useQueryCache` hook has been replaced by the `useQueryClient` hook. It returns the provided `queryClient` for its component tree and shouldn't need much tweaking beyond a rename. ### Query key parts/pieces are no longer automatically spread to the query function. Inline functions are now the suggested way of passing parameters to your query functions: ```tsx // Old useQuery(['post', id], (_key, id) => fetchPost(id)) // New useQuery(['post', id], () => fetchPost(id)) ``` If you still insist on not using inline functions, you can use the newly passed `QueryFunctionContext`: ```tsx useQuery(['post', id], (context) => fetchPost(context.queryKey[1])) ``` ### Infinite Query Page params are now passed via `QueryFunctionContext.pageParam` They were previously added as the last query key parameter in your query function, but this proved to be difficult for some patterns ```tsx // Old useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam)) // New useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam)) ``` ### usePaginatedQuery() has been removed in favor of the `keepPreviousData` option The new `keepPreviousData` options is available for both `useQuery` and `useInfiniteQuery` and will have the same "lagging" effect on your data: ```tsx import { useQuery } from 'react-query' function Page({ page }) { const { data } = useQuery(['page', page], fetchPage, { keepPreviousData: true, }) } ``` ### useInfiniteQuery() is now bi-directional The `useInfiniteQuery()` interface has changed to fully support bi-directional infinite lists. - `options.getFetchMore` has been renamed to `options.getNextPageParam` - `queryResult.canFetchMore` has been renamed to `queryResult.hasNextPage` - `queryResult.fetchMore` has been renamed to `queryResult.fetchNextPage` - `queryResult.isFetchingMore` has been renamed to `queryResult.isFetchingNextPage` - Added the `options.getPreviousPageParam` option - Added the `queryResult.hasPreviousPage` property - Added the `queryResult.fetchPreviousPage` property - Added the `queryResult.isFetchingPreviousPage` - The `data` of an infinite query is now an object containing the `pages` and the `pageParams` used to fetch the pages: `{ pages: [data, data, data], pageParams: [...]}` One direction: ```tsx const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery( 'projects', ({ pageParam = 0 }) => fetchProjects(pageParam), { getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }, ) ``` Both directions: ```tsx const { data, fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, } = useInfiniteQuery( 'projects', ({ pageParam = 0 }) => fetchProjects(pageParam), { getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }, ) ``` One direction reversed: ```tsx const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery( 'projects', ({ pageParam = 0 }) => fetchProjects(pageParam), { select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }, ) ``` ### Infinite Query data now contains the array of pages and pageParams used to fetch those pages. This allows for easier manipulation of the data and the page params, like, for example, removing the first page of data along with it's params: ```tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) ``` ### useMutation now returns an object instead of an array Though the old way gave us warm fuzzy feelings of when we first discovered `useState` for the first time, they didn't last long. Now the mutation return is a single object. ```tsx // Old: const [mutate, { status, reset }] = useMutation() // New: const { mutate, status, reset } = useMutation() ``` ### `mutation.mutate` no longer return a promise - The `[mutate]` variable has been changed to the `mutation.mutate` function - Added the `mutation.mutateAsync` function We got a lot of questions regarding this behavior as users expected the promise to behave like a regular promise. Because of this the `mutate` function is now split into a `mutate` and `mutateAsync` function. The `mutate` function can be used when using callbacks: ```tsx const { mutate } = useMutation({ mutationFn: addTodo }) mutate('todo', { onSuccess: (data) => { console.log(data) }, onError: (error) => { console.error(error) }, onSettled: () => { console.log('settled') }, }) ``` The `mutateAsync` function can be used when using async/await: ```tsx const { mutateAsync } = useMutation({ mutationFn: addTodo }) try { const data = await mutateAsync('todo') console.log(data) } catch (error) { console.error(error) } finally { console.log('settled') } ``` ### The object syntax for useQuery now uses a collapsed config: ```tsx // Old: useQuery({ queryKey: 'posts', queryFn: fetchPosts, config: { staleTime: Infinity }, }) // New: useQuery({ queryKey: 'posts', queryFn: fetchPosts, staleTime: Infinity, }) ``` ### If set, the QueryOptions.enabled option must be a boolean (`true`/`false`) The `enabled` query option will now only disable a query when the value is `false`. If needed, values can be casted with `!!userId` or `Boolean(userId)` and a handy error will be thrown if a non-boolean value is passed. ### The QueryOptions.initialStale option has been removed The `initialStale` query option has been removed and initial data is now treated as regular data. Which means that if `initialData` is provided, the query will refetch on mount by default. If you do not want to refetch immediately, you can define a `staleTime`. ### The `QueryOptions.forceFetchOnMount` option has been replaced by `refetchOnMount: 'always'` Honestly, we were accruing way too many `refetchOn____` options, so this should clean things up. ### The `QueryOptions.refetchOnMount` options now only applies to its parent component instead of all query observers When `refetchOnMount` was set to `false` any additional components were prevented from refetching on mount. In version 3 only the component where the option has been set will not refetch on mount. ### The `QueryOptions.queryFnParamsFilter` has been removed in favor of the new `QueryFunctionContext` object. The `queryFnParamsFilter` option has been removed because query functions now get a `QueryFunctionContext` object instead of the query key. Parameters can still be filtered within the query function itself as the `QueryFunctionContext` also contains the query key. ### The `QueryOptions.notifyOnStatusChange` option has been superseded by the new `notifyOnChangeProps` and `notifyOnChangePropsExclusions` options. With these new options it is possible to configure when a component should re-render on a granular level. Only re-render when the `data` or `error` properties change: ```tsx import { useQuery } from 'react-query' function User() { const { data } = useQuery(['user'], fetchUser, { notifyOnChangeProps: ['data', 'error'], }) return
    Username: {data.username}
    } ``` Prevent re-render when the `isStale` property changes: ```tsx import { useQuery } from 'react-query' function User() { const { data } = useQuery(['user'], fetchUser, { notifyOnChangePropsExclusions: ['isStale'], }) return
    Username: {data.username}
    } ``` ### The `QueryResult.clear()` function has been renamed to `QueryResult.remove()` Although it was called `clear`, it really just removed the query from the cache. The name now matches the functionality. ### The `QueryResult.updatedAt` property has been split into `QueryResult.dataUpdatedAt` and `QueryResult.errorUpdatedAt` properties Because data and errors can be present at the same time, the `updatedAt` property has been split into `dataUpdatedAt` and `errorUpdatedAt`. ### `setConsole()` has been replaced by the new `setLogger()` function ```tsx import { setLogger } from 'react-query' // Log with Sentry setLogger({ error: (error) => { Sentry.captureException(error) }, }) // Log with Winston setLogger(winston.createLogger()) ``` ### React Native no longer requires overriding the logger To prevent showing error screens in React Native when a query fails it was necessary to manually change the Console: ```tsx import { setConsole } from 'react-query' setConsole({ log: console.log, warn: console.warn, error: console.warn, }) ``` In version 3 **this is done automatically when React Query is used in React Native**. ### Typescript #### `QueryStatus` has been changed from an [enum](https://www.typescriptlang.org/docs/handbook/enums.html#string-enums) to a [union type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) So, if you were checking the status property of a query or mutation against a QueryStatus enum property you will have to check it now against the string literal the enum previously held for each property. Therefore you have to change the enum properties to their equivalent string literal, like this: - `QueryStatus.Idle` -> `'idle'` - `QueryStatus.Loading` -> `'loading'` - `QueryStatus.Error` -> `'error'` - `QueryStatus.Success` -> `'success'` Here is an example of the changes you would have to make: ```tsx - import { useQuery, QueryStatus } from 'react-query'; // [!code --] + import { useQuery } from 'react-query'; // [!code ++] const { data, status } = useQuery(['post', id], () => fetchPost(id)) - if (status === QueryStatus.Loading) { // [!code --] + if (status === 'loading') { // [!code ++] ... } - if (status === QueryStatus.Error) { // [!code --] + if (status === 'error') { // [!code ++] ... } ``` ## New features #### Query Data Selectors The `useQuery` and `useInfiniteQuery` hooks now have a `select` option to select or transform parts of the query result. ```tsx import { useQuery } from 'react-query' function User() { const { data } = useQuery(['user'], fetchUser, { select: (user) => user.username, }) return
    Username: {data}
    } ``` Set the `notifyOnChangeProps` option to `['data', 'error']` to only re-render when the selected data changes. #### The useQueries() hook, for variable-length parallel query execution Wish you could run `useQuery` in a loop? The rules of hooks say no, but with the new `useQueries()` hook, you can! ```tsx import { useQueries } from 'react-query' function Overview() { const results = useQueries([ { queryKey: ['post', 1], queryFn: fetchPost }, { queryKey: ['post', 2], queryFn: fetchPost }, ]) return (
      {results.map(({ data }) => data &&
    • {data.title})
    • )}
    ) } ``` #### Retry/offline mutations By default React Query will not retry a mutation on error, but it is possible with the `retry` option: ```tsx const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) ``` If mutations fail because the device is offline, they will be retried in the same order when the device reconnects. #### Persist mutations Mutations can now be persisted to storage and resumed at a later point. More information can be found in the mutations documentation. #### QueryObserver A `QueryObserver` can be used to create and/or watch a query: ```tsx const observer = new QueryObserver(queryClient, { queryKey: 'posts' }) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() }) ``` #### InfiniteQueryObserver A `InfiniteQueryObserver` can be used to create and/or watch an infinite query: ```tsx const observer = new InfiniteQueryObserver(queryClient, { queryKey: 'posts', queryFn: fetchPosts, getNextPageParam: (lastPage, allPages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor, }) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() }) ``` #### QueriesObserver A `QueriesObserver` can be used to create and/or watch multiple queries: ```tsx const observer = new QueriesObserver(queryClient, [ { queryKey: ['post', 1], queryFn: fetchPost }, { queryKey: ['post', 2], queryFn: fetchPost }, ]) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() }) ``` #### Set default options for specific queries The `QueryClient.setQueryDefaults()` method can be used to set default options for specific queries: ```tsx queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts }) function Component() { const { data } = useQuery(['posts']) } ``` #### Set default options for specific mutations The `QueryClient.setMutationDefaults()` method can be used to set default options for specific mutations: ```tsx queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost }) function Component() { const { mutate } = useMutation({ mutationKey: ['addPost'] }) } ``` #### useIsFetching() The `useIsFetching()` hook now accepts filters which can be used to for example only show a spinner for certain type of queries: ```tsx const fetches = useIsFetching({ queryKey: ['posts'] }) ``` #### Core separation The core of React Query is now fully separated from React, which means it can also be used standalone or in other frameworks. Use the `react-query/core` entry point to only import the core functionality: ```tsx import { QueryClient } from 'react-query/core' ``` ### Devtools are now part of the main repo and npm package The devtools are now included in the `react-query` package itself under the import `react-query/devtools`. Simply replace `react-query-devtools` imports with `react-query/devtools` ================================================ FILE: docs/framework/react/guides/migrating-to-react-query-4.md ================================================ --- id: migrating-to-react-query-4 title: Migrating to React Query 4 --- ## Breaking Changes v4 is a major version, so there are some breaking changes to be aware of: ### react-query is now @tanstack/react-query You will need to un-/install dependencies and change the imports: ``` npm uninstall react-query npm install @tanstack/react-query npm install @tanstack/react-query-devtools ``` ```tsx - import { useQuery } from 'react-query' // [!code --] - import { ReactQueryDevtools } from 'react-query/devtools' // [!code --] + import { useQuery } from '@tanstack/react-query' // [!code ++] + import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // [!code ++] ``` #### Codemod To make the import migration easier, v4 comes with a codemod. > The codemod is a best efforts attempt to help you migrate the breaking change. Please review the generated code thoroughly! Also, there are edge cases that cannot be found by the code mod, so please keep an eye on the log output. You can easily apply it by using one (or both) of the following commands: If you want to run it against `.js` or `.jsx` files, please use the command below: ``` npx jscodeshift ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.js ``` If you want to run it against `.ts` or `.tsx` files, please use the command below: ``` npx jscodeshift ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.js ``` Please note in the case of `TypeScript` you need to use `tsx` as the parser; otherwise, the codemod won't be applied properly! **Note:** Applying the codemod might break your code formatting, so please don't forget to run `prettier` and/or `eslint` after you've applied the codemod! **Note:** The codemod will _only_ change the imports - you still have to install the separate devtools package manually. ### Query Keys (and Mutation Keys) need to be an Array In v3, Query and Mutation Keys could be a String or an Array. Internally, React Query has always worked with Array Keys only, and we've sometimes exposed this to consumers. For example, in the `queryFn`, you would always get the key as an Array to make working with [Default Query Functions](./default-query-function.md) easier. However, we have not followed this concept through to all apis. For example, when using the `predicate` function on [Query Filters](./filters.md) you would get the raw Query Key. This makes it difficult to work with such functions if you use Query Keys that are mixed Arrays and Strings. The same was true when using global callbacks. To streamline all apis, we've decided to make all keys Arrays only: ```tsx ;-useQuery('todos', fetchTodos) + // [!code --] useQuery(['todos'], fetchTodos) // [!code ++] ``` #### Codemod To make this migration easier, we decided to deliver a codemod. > The codemod is a best efforts attempt to help you migrate the breaking change. Please review the generated code thoroughly! Also, there are edge cases that cannot be found by the code mod, so please keep an eye on the log output. You can easily apply it by using one (or both) of the following commands: If you want to run it against `.js` or `.jsx` files, please use the command below: ``` npx jscodeshift ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.js ``` If you want to run it against `.ts` or `.tsx` files, please use the command below: ``` npx jscodeshift ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.js ``` Please note in the case of `TypeScript` you need to use `tsx` as the parser; otherwise, the codemod won't be applied properly! **Note:** Applying the codemod might break your code formatting, so please don't forget to run `prettier` and/or `eslint` after you've applied the codemod! ### The idle state has been removed With the introduction of the new [fetchStatus](./queries.md#fetchstatus) for better offline support, the `idle` state became irrelevant, because `fetchStatus: 'idle'` captures the same state better. For more information, please read [Why two different states](./queries.md#why-two-different-states). This will mostly affect `disabled` queries that don't have any `data` yet, as those were in `idle` state before: ```tsx - status: 'idle' // [!code --] + status: 'loading' // [!code ++] + fetchStatus: 'idle' // [!code ++] ``` Also, have a look at [the guide on dependent queries](./dependent-queries.md) #### disabled queries Due to this change, disabled queries (even temporarily disabled ones) will start in `loading` state. To make migration easier, especially for having a good flag to know when to display a loading spinner, you can check for `isInitialLoading` instead of `isLoading`: ```tsx ;-isLoading + // [!code --] isInitialLoading // [!code ++] ``` See also the guide on [disabling queries](./disabling-queries.md#isloading-previously-isinitialloading) ### new API for `useQueries` The `useQueries` hook now accepts an object with a `queries` prop as its input. The value of the `queries` prop is an array of queries (this array is identical to what was passed into `useQueries` in v3). ```tsx ;-useQueries([ { queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }, ]) + // [!code --] useQueries({ queries: [ { queryKey1, queryFn1, options1 }, { queryKey2, queryFn2, options2 }, ], }) // [!code ++] ``` ### Undefined is an illegal cache value for successful queries In order to make bailing out of updates possible by returning `undefined`, we had to make `undefined` an illegal cache value. This is in-line with other concepts of react-query, for example, returning `undefined` from the [initialData function](./initial-query-data.md#initial-data-function) will also _not_ set data. Further, it is an easy bug to produce `Promise` by adding logging in the queryFn: ```tsx useQuery(['key'], () => axios.get(url).then((result) => console.log(result.data)), ) ``` This is now disallowed on type level; at runtime, `undefined` will be transformed to a _failed Promise_, which means you will get an `error`, which will also be logged to the console in development mode. ### Queries and mutations, per default, need network connection to run Please read the [New Features announcement](#proper-offline-support) about online / offline support, and also the dedicated page about [Network mode](./network-mode.md) Even though React Query is an Async State Manager that can be used for anything that produces a Promise, it is most often used for data fetching in combination with data fetching libraries. That is why, per default, queries and mutations will be `paused` if there is no network connection. If you want to opt-in to the previous behavior, you can globally set `networkMode: offlineFirst` for both queries and mutations: ```tsx new QueryClient({ defaultOptions: { queries: { networkMode: 'offlineFirst', }, mutations: { networkMode: 'offlineFirst', }, }, }) ``` ### `notifyOnChangeProps` property no longer accepts `"tracked"` as a value The `notifyOnChangeProps` option no longer accepts a `"tracked"` value. Instead, `useQuery` defaults to tracking properties. All queries using `notifyOnChangeProps: "tracked"` should be updated by removing this option. If you would like to bypass this in any queries to emulate the v3 default behavior of re-rendering whenever a query changes, `notifyOnChangeProps` now accepts an `"all"` value to opt-out of the default smart tracking optimization. ### `notifyOnChangePropsExclusion` has been removed In v4, `notifyOnChangeProps` defaults to the `"tracked"` behavior of v3 instead of `undefined`. Now that `"tracked"` is the default behavior for v4, it no longer makes sense to include this config option. ### Consistent behavior for `cancelRefetch` The `cancelRefetch` option can be passed to all functions that imperatively fetch a query, namely: - `queryClient.refetchQueries` - `queryClient.invalidateQueries` - `queryClient.resetQueries` - `refetch` returned from `useQuery` - `fetchNextPage` and `fetchPreviousPage` returned from `useInfiniteQuery` Except for `fetchNextPage` and `fetchPreviousPage`, this flag was defaulting to `false`, which was inconsistent and potentially troublesome: Calling `refetchQueries` or `invalidateQueries` after a mutation might not yield the latest result if a previous slow fetch was already ongoing, because this refetch would have been skipped. We believe that if a query is actively refetched by some code you write, it should, per default, re-start the fetch. That is why this flag now defaults to _true_ for all methods mentioned above. It also means that if you call `refetchQueries` twice in a row, without awaiting it, it will now cancel the first fetch and re-start it with the second one: ``` queryClient.refetchQueries({ queryKey: ['todos'] }) // this will abort the previous refetch and start a new fetch queryClient.refetchQueries({ queryKey: ['todos'] }) ``` You can opt-out of this behaviour by explicitly passing `cancelRefetch:false`: ``` queryClient.refetchQueries({ queryKey: ['todos'] }) // this will not abort the previous refetch - it will just be ignored queryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false }) ``` > Note: There is no change in behaviour for automatically triggered fetches, e.g. because a query mounts or because of a window focus refetch. ### Query Filters A [query filter](./filters.md) is an object with certain conditions to match a query. Historically, the filter options have mostly been a combination of boolean flags. However, combining those flags can lead to impossible states. Specifically: ``` active?: boolean - When set to true it will match active queries. - When set to false it will match inactive queries. inactive?: boolean - When set to true it will match inactive queries. - When set to false it will match active queries. ``` Those flags don't work well when used together, because they are mutually exclusive. Setting `false` for both flags could match all queries, judging from the description, or no queries, which doesn't make much sense. With v4, those filters have been combined into a single filter to better show the intent: ```tsx - active?: boolean // [!code --] - inactive?: boolean // [!code --] + type?: 'active' | 'inactive' | 'all' // [!code ++] ``` The filter defaults to `all`, and you can choose to only match `active` or `inactive` queries. #### refetchActive / refetchInactive [queryClient.invalidateQueries](../../../reference/QueryClient.md#queryclientinvalidatequeries) had two additional, similar flags: ``` refetchActive: Boolean - Defaults to true - When set to false, queries that match the refetch predicate and are actively being rendered via useQuery and friends will NOT be refetched in the background, and only marked as invalid. refetchInactive: Boolean - Defaults to false - When set to true, queries that match the refetch predicate and are not being rendered via useQuery and friends will be both marked as invalid and also refetched in the background ``` For the same reason, those have also been combined: ```tsx - refetchActive?: boolean // [!code --] - refetchInactive?: boolean // [!code --] + refetchType?: 'active' | 'inactive' | 'all' | 'none' // [!code ++] ``` This flag defaults to `active` because `refetchActive` defaulted to `true`. This means we also need a way to tell `invalidateQueries` to not refetch at all, which is why a fourth option (`none`) is also allowed here. ### `onSuccess` is no longer called from `setQueryData` This was confusing to many and also created infinite loops if `setQueryData` was called from within `onSuccess`. It was also a frequent source of error when combined with `staleTime`, because if data was read from the cache only, `onSuccess` was _not_ called. Similar to `onError` and `onSettled`, the `onSuccess` callback is now tied to a request being made. No request -> no callback. If you want to listen to changes of the `data` field, you can best do this with a `useEffect`, where `data` is part of the dependency Array. Since React Query ensures stable data through structural sharing, the effect will not execute with every background refetch, but only if something within data has changed: ``` const { data } = useQuery({ queryKey, queryFn }) React.useEffect(() => mySideEffectHere(data), [data]) ``` ### `persistQueryClient` and the corresponding persister plugins are no longer experimental and have been renamed The plugins `createWebStoragePersistor` and `createAsyncStoragePersistor` have been renamed to [`createSyncStoragePersister`](../plugins/createSyncStoragePersister.md) and [`createAsyncStoragePersister`](../plugins/createAsyncStoragePersister.md) respectively. The interface `Persistor` in `persistQueryClient` has also been renamed to `Persister`. Checkout [this stackexchange](https://english.stackexchange.com/questions/206893/persister-or-persistor) for the motivation of this change. Since these plugins are no longer experimental, their import paths have also been updated: ```tsx - import { persistQueryClient } from 'react-query/persistQueryClient-experimental' // [!code --] - import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental' // [!code --] - import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental' // [!code --] + import { persistQueryClient } from '@tanstack/react-query-persist-client' // [!code ++] + import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' // [!code ++] + import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' // [!code ++] ``` ### The `cancel` method on promises is no longer supported The old `cancel` method (which allowed you to define a `cancel` function on promises and was used by the library to support query cancellation) has been removed. We recommend using the [newer API](./query-cancellation.md) (introduced with v3.30.0) for query cancellation, which uses the [`AbortController` API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) internally and provides you with an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) in your query function to support query cancellation. ### TypeScript Types now require using TypeScript v4.1 or greater ### Supported Browsers As of v4, React Query is optimized for modern browsers. We have updated our browserslist to produce a more modern, performant and smaller bundle. You can read about the requirements [here](../installation#requirements). ### `setLogger` is removed It was possible to change the logger globally by calling `setLogger`. In v4, that function is replaced with an optional field when creating a `QueryClient`. ```tsx - import { QueryClient, setLogger } from 'react-query'; // [!code --] + import { QueryClient } from '@tanstack/react-query'; // [!code ++] - setLogger(customLogger) // [!code --] - const queryClient = new QueryClient(); // [!code --] + const queryClient = new QueryClient({ logger: customLogger }) // [!code ++] ``` ### No _default_ manual Garbage Collection server-side In v3, React Query would cache query results for a default of 5 minutes, then manually garbage collect that data. This default was applied to server-side React Query as well. This lead to high memory consumption and hanging processes waiting for this manual garbage collection to complete. In v4, by default the server-side `cacheTime` is now set to `Infinity` effectively disabling manual garbage collection (the NodeJS process will clear everything once a request is complete). This change only impacts users of server-side React Query, such as with Next.js. If you are setting a `cacheTime` manually this will not impact you (although you may want to mirror behavior). ### Logging in production Starting with v4, react-query will no longer log errors (e.g. failed fetches) to the console in production mode, as this was confusing to many. Errors will still show up in development mode. ### ESM Support React Query now supports [package.json `"exports"`](https://nodejs.org/api/packages.html#exports) and is fully compatible with Node's native resolution for both CommonJS and ESM. We don't expect this to be a breaking change for most users, but this restricts the files you can import into your project to only the entry points we officially support. ### Streamlined NotifyEvents Subscribing manually to the `QueryCache` has always given you a `QueryCacheNotifyEvent`, but this was not true for the `MutationCache`. We have streamlined the behavior and also adapted event names accordingly. #### QueryCacheNotifyEvent ```tsx - type: 'queryAdded' // [!code --] + type: 'added' // [!code ++] - type: 'queryRemoved' // [!code --] + type: 'removed' // [!code ++] - type: 'queryUpdated' // [!code --] + type: 'updated' // [!code ++] ``` #### MutationCacheNotifyEvent The `MutationCacheNotifyEvent` uses the same types as the `QueryCacheNotifyEvent`. > Note: This is only relevant if you manually subscribe to the caches via `queryCache.subscribe` or `mutationCache.subscribe` ### Separate hydration exports have been removed With version [3.22.0](https://github.com/TanStack/query/releases/tag/v3.22.0), hydration utilities moved into the React Query core. With v3, you could still use the old exports from `react-query/hydration`, but these exports have been removed with v4. ```tsx - import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query/hydration' // [!code --] + import { dehydrate, hydrate, useHydrate, Hydrate } from '@tanstack/react-query' // [!code ++] ``` ### Removed undocumented methods from the `queryClient`, `query` and `mutation` The methods `cancelMutations` and `executeMutation` on the `QueryClient` were undocumented and unused internally, so we removed them. Since it was just a wrapper around a method available on the `mutationCache`, you can still use the functionality of `executeMutation` ```tsx - executeMutation< // [!code --] - TData = unknown, // [!code --] - TError = unknown, // [!code --] - TVariables = void, // [!code --] - TContext = unknown // [!code --] - >( // [!code --] - options: MutationOptions // [!code --] - ): Promise { // [!code --] - return this.mutationCache.build(this, options).execute() // [!code --] - } // [!code --] ``` Additionally, `query.setDefaultOptions` was removed because it was also unused. `mutation.cancel` was removed because it didn't actually cancel the outgoing request. ### The `src/react` directory was renamed to `src/reactjs` Previously, React Query had a directory named `react` which imported from the `react` module. This could cause problems with some Jest configurations, resulting in errors when running tests like: ``` TypeError: Cannot read property 'createContext' of undefined ``` With the renamed directory this no longer is an issue. If you were importing anything from `'react-query/react'` directly in your project (as opposed to just `'react-query'`), then you need to update your imports: ```tsx - import { QueryClientProvider } from 'react-query/react'; // [!code --] + import { QueryClientProvider } from '@tanstack/react-query/reactjs'; // [!code ++] ``` ## New Features 🚀 v4 comes with an awesome set of new features: ### Support for React 18 React 18 was released earlier this year, and v4 now has first class support for it and the new concurrent features it brings. ### Proper offline support In v3, React Query has always fired off queries and mutations, but then taken the assumption that if you want to retry it, you need to be connected to the internet. This has led to several confusing situations: - You are offline and mount a query - it goes to loading state, the request fails, and it stays in loading state until you go online again, even though it is not really fetching. - Similarly, if you are offline and have retries turned off, your query will just fire and fail, and the query goes to error state. - You are offline and want to fire off a query that doesn't necessarily need network connection (because you _can_ use React Query for something other than data fetching), but it fails for some other reason. That query will now be paused until you go online again. - Window focus refetching didn't do anything at all if you were offline. With v4, React Query introduces a new `networkMode` to tackle all these issues. Please read the dedicated page about the new [Network mode](./network-mode) for more information. ### Tracked Queries per default React Query defaults to "tracking" query properties, which should give you a nice boost in render optimization. The feature has existed since [v3.6.0](https://github.com/TanStack/query/releases/tag/v3.6.0) and has now become the default behavior with v4. ### Bailing out of updates with setQueryData When using the [functional updater form of setQueryData](../../../reference/QueryClient.md#queryclientsetquerydata), you can now bail out of the update by returning `undefined`. This is helpful if `undefined` is given to you as `previousValue`, which means that currently, no cached entry exists and you don't want to / cannot create one, like in the example of toggling a todo: ```tsx queryClient.setQueryData(['todo', id], (previousTodo) => previousTodo ? { ...previousTodo, done: true } : undefined, ) ``` ### Mutation Cache Garbage Collection Mutations can now also be garbage collected automatically, just like queries. The default `cacheTime` for mutations is also set to 5 minutes. ### Custom Contexts for Multiple Providers Custom contexts can now be specified to pair hooks with their matching `Provider`. This is critical when there may be multiple React Query `Provider` instances in the component tree, and you need to ensure your hook uses the correct `Provider` instance. An example: 1. Create a data package. ```tsx // Our first data package: @my-scope/container-data const context = React.createContext(undefined) const queryClient = new QueryClient() export const useUser = () => { return useQuery(USER_KEY, USER_FETCHER, { context, }) } export const ContainerDataProvider = ({ children, }: { children: React.ReactNode }) => { return ( {children} ) } ``` 2. Create a second data package. ```tsx // Our second data package: @my-scope/my-component-data const context = React.createContext(undefined) const queryClient = new QueryClient() export const useItems = () => { return useQuery(ITEMS_KEY, ITEMS_FETCHER, { context, }) } export const MyComponentDataProvider = ({ children, }: { children: React.ReactNode }) => { return ( {children} ) } ``` 3. Use these two data packages in your application. ```tsx // Our application import { ContainerDataProvider, useUser } from "@my-scope/container-data"; import { AppDataProvider } from "@my-scope/app-data"; import { MyComponentDataProvider, useItems } from "@my-scope/my-component-data"; // <-- Provides container data (like "user") using its own React Query provider ... // <-- Provides app data using its own React Query provider (unused in this example) ... // <-- Provides component data (like "items") using its own React Query provider ... ... // Example of hooks provided by the "DataProvider" components above: const MyComponent = () => { const user = useUser() // <-- Uses the context specified in ContainerDataProvider. const items = useItems() // <-- Uses the context specified in MyComponentDataProvider ... } ``` ================================================ FILE: docs/framework/react/guides/migrating-to-v5.md ================================================ --- id: migrating-to-tanstack-query-5 title: Migrating to TanStack Query v5 --- ## Breaking Changes v5 is a major version, so there are some breaking changes to be aware of: ### Supports a single signature, one object useQuery and friends used to have many overloads in TypeScript: different ways how the function could be invoked. Not only was this tough to maintain, type wise, it also required a runtime check to see which types the first and the second parameter were, to correctly create options. now we only support the object format. ```tsx useQuery(key, fn, options) // [!code --] useQuery({ queryKey, queryFn, ...options }) // [!code ++] useInfiniteQuery(key, fn, options) // [!code --] useInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++] useMutation(fn, options) // [!code --] useMutation({ mutationFn, ...options }) // [!code ++] useIsFetching(key, filters) // [!code --] useIsFetching({ queryKey, ...filters }) // [!code ++] useIsMutating(key, filters) // [!code --] useIsMutating({ mutationKey, ...filters }) // [!code ++] ``` ```tsx queryClient.isFetching(key, filters) // [!code --] queryClient.isFetching({ queryKey, ...filters }) // [!code ++] queryClient.ensureQueryData(key, filters) // [!code --] queryClient.ensureQueryData({ queryKey, ...filters }) // [!code ++] queryClient.getQueriesData(key, filters) // [!code --] queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++] queryClient.setQueriesData(key, updater, filters, options) // [!code --] queryClient.setQueriesData({ queryKey, ...filters }, updater, options) // [!code ++] queryClient.removeQueries(key, filters) // [!code --] queryClient.removeQueries({ queryKey, ...filters }) // [!code ++] queryClient.resetQueries(key, filters, options) // [!code --] queryClient.resetQueries({ queryKey, ...filters }, options) // [!code ++] queryClient.cancelQueries(key, filters, options) // [!code --] queryClient.cancelQueries({ queryKey, ...filters }, options) // [!code ++] queryClient.invalidateQueries(key, filters, options) // [!code --] queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++] queryClient.refetchQueries(key, filters, options) // [!code --] queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++] queryClient.fetchQuery(key, fn, options) // [!code --] queryClient.fetchQuery({ queryKey, queryFn, ...options }) // [!code ++] queryClient.prefetchQuery(key, fn, options) // [!code --] queryClient.prefetchQuery({ queryKey, queryFn, ...options }) // [!code ++] queryClient.fetchInfiniteQuery(key, fn, options) // [!code --] queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++] queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --] queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++] ``` ```tsx queryCache.find(key, filters) // [!code --] queryCache.find({ queryKey, ...filters }) // [!code ++] queryCache.findAll(key, filters) // [!code --] queryCache.findAll({ queryKey, ...filters }) // [!code ++] ``` ### `queryClient.getQueryData` now accepts queryKey only as an Argument `queryClient.getQueryData` argument is changed to accept only a `queryKey` ```tsx queryClient.getQueryData(queryKey, filters) // [!code --] queryClient.getQueryData(queryKey) // [!code ++] ``` ### `queryClient.getQueryState` now accepts queryKey only as an Argument `queryClient.getQueryState` argument is changed to accept only a `queryKey` ```tsx queryClient.getQueryState(queryKey, filters) // [!code --] queryClient.getQueryState(queryKey) // [!code ++] ``` #### Codemod To make the remove overloads migration easier, v5 comes with a codemod. > The codemod is a best efforts attempt to help you migrate the breaking change. Please review the generated code thoroughly! Also, there are edge cases that cannot be found by the code mod, so please keep an eye on the log output. If you want to run it against `.js` or `.jsx` files, please use the command below: ``` npx jscodeshift@latest ./path/to/src/ \ --extensions=js,jsx \ --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs ``` If you want to run it against `.ts` or `.tsx` files, please use the command below: ``` npx jscodeshift@latest ./path/to/src/ \ --extensions=ts,tsx \ --parser=tsx \ --transform=./node_modules/@tanstack/react-query/build/codemods/src/v5/remove-overloads/remove-overloads.cjs ``` Please note in the case of `TypeScript` you need to use `tsx` as the parser; otherwise, the codemod won't be applied properly! **Note:** Applying the codemod might break your code formatting, so please don't forget to run `prettier` and/or `eslint` after you've applied the codemod! A few notes about how codemod works: - Generally, we're looking for the lucky case, when the first parameter is an object expression and contains the "queryKey" or "mutationKey" property (depending on which hook/method call is being transformed). If this is the case, your code already matches the new signature, so the codemod won't touch it. 🎉 - If the condition above is not fulfilled, then the codemod will check whether the first parameter is an array expression or an identifier that references an array expression. If this is the case, the codemod will put it into an object expression, then it will be the first parameter. - If object parameters can be inferred, the codemod will attempt to copy the already existing properties to the newly created one. - If the codemod cannot infer the usage, then it will leave a message on the console. The message contains the file name and the line number of the usage. In this case, you need to do the migration manually. - If the transformation results in an error, you will also see a message on the console. This message will notify you something unexpected happened, please do the migration manually. ### Callbacks on useQuery (and QueryObserver) have been removed `onSuccess`, `onError` and `onSettled` have been removed from Queries. They haven't been touched for Mutations. Please see [this RFC](https://github.com/TanStack/query/discussions/5279) for motivations behind this change and what to do instead. ### The `refetchInterval` callback function only gets `query` passed This streamlines how callbacks are invoked (the `refetchOnWindowFocus`, `refetchOnMount` and `refetchOnReconnect` callbacks all only get the query passed as well), and it fixes some typing issues when callbacks get data transformed by `select`. ```tsx - refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false | undefined) // [!code --] + refetchInterval: number | false | ((query: Query) => number | false | undefined) // [!code ++] ``` You can still access data with `query.state.data`, however, it will not be data that has been transformed by `select`. If you need to access the transformed data, you can call the transformation again on `query.state.data`. ### The `remove` method has been removed from useQuery Previously, remove method used to remove the query from the queryCache without informing observers about it. It was best used to remove data imperatively that is no longer needed, e.g. when logging a user out. But It doesn't make much sense to do this while a query is still active, because it will just trigger a hard loading state with the next re-render. if you still need to remove a query, you can use `queryClient.removeQueries({queryKey: key})` ```tsx const queryClient = useQueryClient() const query = useQuery({ queryKey, queryFn }) query.remove() // [!code --] queryClient.removeQueries({ queryKey }) // [!code ++] ``` ### The minimum required TypeScript version is now 4.7 Mainly because an important fix was shipped around type inference. Please see this [TypeScript issue](https://github.com/microsoft/TypeScript/issues/43371) for more information. ### The `isDataEqual` option has been removed from useQuery Previously, This function was used to indicate whether to use previous `data` (`true`) or new data (`false`) as a resolved data for the query. You can achieve the same functionality by passing a function to `structuralSharing` instead: ```tsx import { replaceEqualDeep } from '@tanstack/react-query' - isDataEqual: (oldData, newData) => customCheck(oldData, newData) // [!code --] + structuralSharing: (oldData, newData) => customCheck(oldData, newData) ? oldData : replaceEqualDeep(oldData, newData) // [!code ++] ``` ### The deprecated custom logger has been removed Custom loggers were already deprecated in 4 and have been removed in this version. Logging only had an effect in development mode, where passing a custom logger is not necessary. ### Supported Browsers We have updated our browserslist to produce a more modern, performant and smaller bundle. You can read about the requirements [here](../installation#requirements). ### Private class fields and methods TanStack Query has always had private fields and methods on classes, but they weren't really private - they were just private in `TypeScript`. We now use [ECMAScript Private class features](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields), which means those fields are now truly private and can't be accessed from the outside at runtime. ### Rename `cacheTime` to `gcTime` Almost everyone gets `cacheTime` wrong. It sounds like "the amount of time that data is cached for", but that is not correct. `cacheTime` does nothing as long as a query is still in use. It only kicks in as soon as the query becomes unused. After the time has passed, data will be "garbage collected" to avoid the cache from growing. `gc` is referring to "garbage collect" time. It's a bit more technical, but also a quite [well known abbreviation]() in computer science. ```tsx const MINUTE = 1000 * 60; const queryClient = new QueryClient({ defaultOptions: { queries: { - cacheTime: 10 * MINUTE, // [!code --] + gcTime: 10 * MINUTE, // [!code ++] }, }, }) ``` ### The `useErrorBoundary` option has been renamed to `throwOnError` To make the `useErrorBoundary` option more framework-agnostic and avoid confusion with the established React function prefix "`use`" for hooks and the "ErrorBoundary" component name, it has been renamed to `throwOnError` to more accurately reflect its functionality. ### TypeScript: `Error` is now the default type for errors instead of `unknown` Even though in JavaScript, you can `throw` anything (which makes `unknown` the most correct type), almost always, `Errors` (or subclasses of `Error`) are thrown. This change makes it easier to work with the `error` field in TypeScript for most cases. If you want to throw something that isn't an Error, you'll now have to set the generic for yourself: ```ts useQuery({ queryKey: ['some-query'], queryFn: async () => { if (Math.random() > 0.5) { throw 'some error' } return 42 }, }) ``` For a way to set a different kind of Error globally, see [the TypeScript Guide](../typescript.md#registering-a-global-error). ### eslint `prefer-query-object-syntax` rule is removed Since the only supported syntax now is the object syntax, this rule is no longer needed ### Removed `keepPreviousData` in favor of `placeholderData` identity function We have removed the `keepPreviousData` option and `isPreviousData` flag as they were doing mostly the same thing as `placeholderData` and `isPlaceholderData` flag. To achieve the same functionality as `keepPreviousData`, we have added previous query `data` as an argument to `placeholderData` which accepts an identity function. Therefore you just need to provide an identity function to `placeholderData` or use the included `keepPreviousData` function from TanStack Query. > A note here is that `useQueries` would not receive `previousData` in the `placeholderData` function as argument. This is due to a dynamic nature of queries passed in the array, which may lead to a different shape of result from placeholder and queryFn. ```tsx import { useQuery, + keepPreviousData // [!code ++] } from "@tanstack/react-query"; const { data, - isPreviousData, // [!code --] + isPlaceholderData, // [!code ++] } = useQuery({ queryKey, queryFn, - keepPreviousData: true, // [!code --] + placeholderData: keepPreviousData // [!code ++] }); ``` An identity function, in the context of TanStack Query, refers to a function that always returns its provided argument (i.e. data) unchanged. ```ts useQuery({ queryKey, queryFn, placeholderData: (previousData, previousQuery) => previousData, // identity function with the same behaviour as `keepPreviousData` }) ``` There are some caveats to this change however, which you must be aware of: - `placeholderData` will always put you into `success` state, while `keepPreviousData` gave you the status of the previous query. That status could be `error` if we have data fetched successfully and then got a background refetch error. However, the error itself was not shared, so we decided to stick with behavior of `placeholderData`. - `keepPreviousData` gave you the `dataUpdatedAt` timestamp of the previous data, while with `placeholderData`, `dataUpdatedAt` will stay at `0`. This might be annoying if you want to show that timestamp continuously on screen. However you might get around it with `useEffect`. ```ts const [updatedAt, setUpdatedAt] = useState(0) const { data, dataUpdatedAt } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), }) useEffect(() => { if (dataUpdatedAt > updatedAt) { setUpdatedAt(dataUpdatedAt) } }, [dataUpdatedAt]) ``` ### Window focus refetching no longer listens to the `focus` event The `visibilitychange` event is used exclusively now. This is possible because we only support browsers that support the `visibilitychange` event. This fixes a bunch of issues [as listed here](https://github.com/TanStack/query/pull/4805). ### Network status no longer relies on the `navigator.onLine` property `navigator.onLine` doesn't work well in Chromium based browsers. There are [a lot of issues](https://bugs.chromium.org/p/chromium/issues/list?q=navigator.online) around false negatives, which lead to Queries being wrongfully marked as `offline`. To circumvent this, we now always start with `online: true` and only listen to `online` and `offline` events to update the status. This should reduce the likelihood of false negatives, however, it might mean false positives for offline apps that load via serviceWorkers, which can work even without an internet connection. ### Removed custom `context` prop in favor of custom `queryClient` instance In v4, we introduced the possibility to pass a custom `context` to all react-query hooks. This allowed for proper isolation when using MicroFrontends. However, `context` is a react-only feature. All that `context` does is give us access to the `queryClient`. We could achieve the same isolation by allowing to pass in a custom `queryClient` directly. This in turn will enable other frameworks to have the same functionality in a framework-agnostic way. ```tsx import { queryClient } from './my-client' const { data } = useQuery( { queryKey: ['users', id], queryFn: () => fetch(...), - context: customContext // [!code --] }, + queryClient, // [!code ++] ) ``` ### Removed `refetchPage` in favor of `maxPages` In v4, we introduced the possibility to define the pages to refetch for infinite queries with the `refetchPage` function. However, refetching all pages might lead to UI inconsistencies. Also, this option is available on e.g. `queryClient.refetchQueries`, but it only does something for infinite queries, not "normal" queries. The v5 includes a new `maxPages` option for infinite queries to limit the number of pages to store in the query data and to refetch. This new feature handles the use cases initially identified for the `refetchPage` page feature without the related issues. ### New `dehydrate` API The options you can pass to `dehydrate` have been simplified. Queries and Mutations are always dehydrated (according to the default function implementation). To change this behaviour, instead of using the removed boolean options `dehydrateMutations` and `dehydrateQueries` you can implement the function equivalents `shouldDehydrateQuery` or `shouldDehydrateMutation` instead. To get the old behaviour of not hydrating queries/mutations at all, pass in `() => false`. ```tsx - dehydrateMutations?: boolean // [!code --] - dehydrateQueries?: boolean // [!code --] ``` ### Infinite queries now need a `initialPageParam` Previously, we've passed `undefined` to the `queryFn` as `pageParam`, and you could assign a default value to the `pageParam` parameter in the `queryFn` function signature. This had the drawback of storing `undefined` in the `queryCache`, which is not serializable. Instead, you now have to pass an explicit `initialPageParam` to the infinite query options. This will be used as the `pageParam` for the first page: ```tsx useInfiniteQuery({ queryKey, - queryFn: ({ pageParam = 0 }) => fetchSomething(pageParam), // [!code --] + queryFn: ({ pageParam }) => fetchSomething(pageParam), // [!code ++] + initialPageParam: 0, // [!code ++] getNextPageParam: (lastPage) => lastPage.next, }) ``` ### Manual mode for infinite queries has been removed Previously, we've allowed to overwrite the `pageParams` that would be returned from `getNextPageParam` or `getPreviousPageParam` by passing a `pageParam` value directly to `fetchNextPage` or `fetchPreviousPage`. This feature didn't work at all with refetches and wasn't widely known or used. This also means that `getNextPageParam` is now required for infinite queries. ### Returning `null` from `getNextPageParam` or `getPreviousPageParam` now indicates that there is no further page available In v4, you needed to explicitly return `undefined` to indicate that there is no further page available. We've widened this check to include `null`. ### No retries on the server On the server, `retry` now defaults to `0` instead of `3`. For prefetching, we have always defaulted to `0` retries, but since queries that have `suspense` enabled can now execute directly on the server as well (since React18), we have to make sure that we don't retry on the server at all. ### `status: loading` has been changed to `status: pending` and `isLoading` has been changed to `isPending` and `isInitialLoading` has now been renamed to `isLoading` The `loading` status has been renamed to `pending`, and similarly the derived `isLoading` flag has been renamed to `isPending`. For mutations as well the `status` has been changed from `loading` to `pending` and the `isLoading` flag has been changed to `isPending`. Lastly, a new derived `isLoading` flag has been added to the queries that is implemented as `isPending && isFetching`. This means that `isLoading` and `isInitialLoading` have the same thing, but `isInitialLoading` is deprecated now and will be removed in the next major version. To understand the reasoning behind this change checkout the [v5 roadmap discussion](https://github.com/TanStack/query/discussions/4252). ### `hashQueryKey` has been renamed to `hashKey` because it also hashes mutation keys and can be used inside the `predicate` functions of `useIsMutating` and `useMutationState`, which gets mutations passed. [//]: # 'FrameworkSpecificBreakingChanges' ### The minimum required React version is now 18.0 React Query v5 requires React 18.0 or later. This is because we are using the new `useSyncExternalStore` hook, which is only available in React 18.0 and later. Previously, we have been using the shim provided by React. ### The `contextSharing` prop has been removed from QueryClientProvider You could previously use the `contextSharing` property to share the first (and at least one) instance of the query client context across the window. This ensured that if TanStack Query was used across different bundles or microfrontends then they will all use the same instance of the context, regardless of module scoping. With the removal of the custom context prop in v5, refer to the section on [Removed custom context prop in favor of custom queryClient instance](#removed-custom-context-prop-in-favor-of-custom-queryclient-instance). If you wish to share the same query client across multiple packages of an application, you can directly pass a shared custom `queryClient` instance. ### No longer using `unstable_batchedUpdates` as the batching function in React and React Native Since the function `unstable_batchedUpdates` is noop in React 18, it will no longer be automatically set as the batching function in `react-query`. If your framework supports a custom batching function, you can let TanStack Query know about it by calling `notifyManager.setBatchNotifyFunction`. For example, this is how the batch function is set in `solid-query`: ```ts import { notifyManager } from '@tanstack/query-core' import { batch } from 'solid-js' notifyManager.setBatchNotifyFunction(batch) ``` ### Hydration API changes To better support concurrent features and transitions we've made some changes to the hydration APIs. The `Hydrate` component has been renamed to `HydrationBoundary` and the `useHydrate` hook has been removed. The `HydrationBoundary` no longer hydrates mutations, only queries. To hydrate mutations, use the low level `hydrate` API or the `persistQueryClient` plugin. Finally, as a technical detail, the timing for when queries are hydrated have changed slightly. New queries are still hydrated in the render phase so that SSR works as usual, but any queries that already exist in the cache are now hydrated in an effect instead (as long as their data is fresher than what is in the cache). If you are hydrating just once at the start of your application as is common, this wont affect you, but if you are using Server Components and pass down fresh data for hydration on a page navigation, you might notice a flash of the old data before the page immediately rerenders. This last change is technically a breaking one, and was made so we don't prematurely update content on the _existing_ page before a page transition has been fully committed. No action is required on your part. ```tsx - import { Hydrate } from '@tanstack/react-query' // [!code --] + import { HydrationBoundary } from '@tanstack/react-query' // [!code ++] - // [!code --] + // [!code ++] - // [!code --] +
    // [!code ++] ``` ### Query defaults changes `queryClient.getQueryDefaults` will now merge together all matching registrations instead of returning only the first matching registration. As a result, calls to `queryClient.setQueryDefaults` should now be ordered with _increasing_ specificity. That is, registrations should be made from the **most generic key** to the **least generic one**. For example: ```ts + queryClient.setQueryDefaults(['todo'], { // [!code ++] + retry: false, // [!code ++] + staleTime: 60_000, // [!code ++] + }) // [!code ++] queryClient.setQueryDefaults(['todo', 'detail'], { + retry: true, // [!code --] retryDelay: 1_000, staleTime: 10_000, }) - queryClient.setQueryDefaults(['todo'], { // [!code --] - retry: false, // [!code --] - staleTime: 60_000, // [!code --] - }) // [!code --] ``` Note that in this specific example, `retry: true` was added to the `['todo', 'detail']` registration to counteract it now inheriting `retry: false` from the more general registration. The specific changes needed to maintain exact behavior will vary depending on your defaults. [//]: # 'FrameworkSpecificBreakingChanges' ## New Features 🚀 v5 also comes with new features: ### Simplified optimistic updates We have a new, simplified way to perform optimistic updates by leveraging the returned `variables` from `useMutation`: ```tsx const queryInfo = useTodos() const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) if (queryInfo.data) { return (
      {queryInfo.data.items.map((todo) => (
    • {todo.text}
    • ))} {addTodoMutation.isPending && (
    • {addTodoMutation.variables}
    • )}
    ) } ``` Here, we are only changing how the UI looks when the mutation is running instead of writing data directly to the cache. This works best if we only have one place where we need to show the optimistic update. For more details, have a look at the [optimistic updates documentation](./optimistic-updates.md). ### Limited, Infinite Queries with new maxPages option Infinite queries are great when infinite scroll or pagination are needed. However, the more pages you fetch, the more memory you consume, and this also slows down the query refetching process as all the pages are sequentially refetched. Version 5 has a new `maxPages` option for infinite queries, which allows developers to limit the number of pages that are stored in the query data and subsequently refetched. You can adjust the `maxPages` value according to the UX and refetching performance you want to deliver. Note that the infinite list must be bi-directional, which requires both `getNextPageParam` and `getPreviousPageParam` to be defined. ### Infinite Queries can prefetch multiple pages Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option. Read the [prefetching guide](./prefetching.md) for more information. ### New `combine` option for `useQueries` See the [useQueries docs](../reference/useQueries.md#combine) for more details. ### Experimental `fine grained storage persister` See the [experimental_createPersister docs](../plugins/createPersister.md) for more details. [//]: # 'FrameworkSpecificNewFeatures' ### Typesafe way to create Query Options See the [TypeScript docs](../typescript.md#typing-query-options) for more details. ### new hooks for suspense With v5, suspense for data fetching finally becomes "stable". We've added dedicated `useSuspenseQuery`, `useSuspenseInfiniteQuery` and `useSuspenseQueries` hooks. With these hooks, `data` will never be potentially `undefined` on type level: ```js const { data: post } = useSuspenseQuery({ // ^? const post: Post queryKey: ['post', postId], queryFn: () => fetchPost(postId), }) ``` The experimental `suspense: boolean` flag on the query hooks has been removed. You can read more about them in the [suspense docs](./suspense.md). [//]: # 'FrameworkSpecificNewFeatures' ================================================ FILE: docs/framework/react/guides/mutations.md ================================================ --- id: mutations title: Mutations --- Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a `useMutation` hook. Here's an example of a mutation that adds a new todo to the server: [//]: # 'Example' ```tsx function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return (
    {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? (
    An error occurred: {mutation.error.message}
    ) : null} {mutation.isSuccess ?
    Todo added!
    : null} )}
    ) } ``` [//]: # 'Example' A mutation can only be in one of the following states at any given moment: - `isIdle` or `status === 'idle'` - The mutation is currently idle or in a fresh/reset state - `isPending` or `status === 'pending'` - The mutation is currently running - `isError` or `status === 'error'` - The mutation encountered an error - `isSuccess` or `status === 'success'` - The mutation was successful and mutation data is available Beyond those primary states, more information is available depending on the state of the mutation: - `error` - If the mutation is in an `error` state, the error is available via the `error` property. - `data` - If the mutation is in a `success` state, the data is available via the `data` property. In the example above, you also saw that you can pass variables to your mutations function by calling the `mutate` function with a **single variable or object**. Even with just variables, mutations aren't all that special, but when used with the `onSuccess` option, the [Query Client's `invalidateQueries` method](../../../reference/QueryClient.md#queryclientinvalidatequeries) and the [Query Client's `setQueryData` method](../../../reference/QueryClient.md#queryclientsetquerydata), mutations become a very powerful tool. [//]: # 'Info1' > IMPORTANT: The `mutate` function is an asynchronous function, which means you cannot use it directly in an event callback in **React 16 and earlier**. If you need to access the event in `onSubmit` you need to wrap `mutate` in another function. This is due to [React event pooling](https://reactjs.org/docs/legacy-event-pooling.html). [//]: # 'Info1' [//]: # 'Example2' ```tsx // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return
    ...
    } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return
    ...
    } ``` [//]: # 'Example2' ## Resetting Mutation State It's sometimes the case that you need to clear the `error` or `data` of a mutation request. To do this, you can use the `reset` function to handle this: [//]: # 'Example3' ```tsx const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return (
    {mutation.error && (
    mutation.reset()}>{mutation.error}
    )} setTitle(e.target.value)} />
    ) } ``` [//]: # 'Example3' ## Mutation Side Effects `useMutation` comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both [invalidating and refetching queries after mutations](./invalidations-from-mutations.md) and even [optimistic updates](./optimistic-updates.md) [//]: # 'Example4' ```tsx useMutation({ mutationFn: addTodo, onMutate: (variables, context) => { // A mutation is about to happen! // Optionally return a result containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, onMutateResult, context) => { // An error happened! console.log(`rolling back optimistic update with id ${onMutateResult.id}`) }, onSuccess: (data, variables, onMutateResult, context) => { // Boom baby! }, onSettled: (data, error, variables, onMutateResult, context) => { // Error or success... doesn't matter! }, }) ``` [//]: # 'Example4' When returning a promise in any of the callback functions it will first be awaited before the next callback is called: [//]: # 'Example5' ```tsx useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) ``` [//]: # 'Example5' You might find that you want to **trigger additional callbacks** beyond the ones defined on `useMutation` when calling `mutate`. This can be used to trigger component-specific side effects. To do that, you can provide any of the same callback options to the `mutate` function after your mutation variable. Supported options include: `onSuccess`, `onError` and `onSettled`. Please keep in mind that those additional callbacks won't run if your component unmounts _before_ the mutation finishes. [//]: # 'Example6' ```tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, onMutateResult, context) => { // I will fire first }, onError: (error, variables, onMutateResult, context) => { // I will fire first }, onSettled: (data, error, variables, onMutateResult, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, onMutateResult, context) => { // I will fire second! }, onError: (error, variables, onMutateResult, context) => { // I will fire second! }, onSettled: (data, error, variables, onMutateResult, context) => { // I will fire second! }, }) ``` [//]: # 'Example6' ### Consecutive mutations There is a slight difference in handling `onSuccess`, `onError` and `onSettled` callbacks when it comes to consecutive mutations. When passed to the `mutate` function, they will be fired up only _once_ and only if the component is still mounted. This is due to the fact that mutation observer is removed and resubscribed every time when the `mutate` function is called. On the contrary, `useMutation` handlers execute for each `mutate` call. > Be aware that most likely, `mutationFn` passed to `useMutation` is asynchronous. In that case, the order in which mutations are fulfilled may differ from the order of `mutate` function calls. [//]: # 'Example7' ```tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, onMutateResult, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, onMutateResult, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) ``` [//]: # 'Example7' ## Promises Use `mutateAsync` instead of `mutate` to get a promise which will resolve on success or throw on an error. This can for example be used to compose side effects. [//]: # 'Example8' ```tsx const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } ``` [//]: # 'Example8' ## Retry By default, TanStack Query will not retry a mutation on error, but it is possible with the `retry` option: [//]: # 'Example9' ```tsx const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) ``` [//]: # 'Example9' If mutations fail because the device is offline, they will be retried in the same order when the device reconnects. ## Persist mutations Mutations can be persisted to storage if needed and resumed at a later point. This can be done with the hydration functions: [//]: # 'Example10' ```tsx const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables, context) => { // Cancel current queries for the todos list await context.client.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return a result with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, onMutateResult, context) => { // Replace optimistic todo in the todos list with the result context.client.setQueryData(['todos'], (old) => old.map((todo) => todo.id === onMutateResult.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, onMutateResult, context) => { // Remove optimistic todo from the todos list context.client.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() ``` [//]: # 'Example10' [//]: # 'PersistOfflineIntro' ### Persisting Offline mutations If you persist offline mutations with the [persistQueryClient plugin](../plugins/persistQueryClient.md), mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. [//]: # 'PersistOfflineIntro' This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling `resumePausedMutations` might yield an error: `No mutationFn found`. [//]: # 'Example11' ```tsx const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > ) } ``` [//]: # 'Example11' [//]: # 'OfflineExampleLink' We also have an extensive [offline example](../examples/offline) that covers both queries and mutations. [//]: # 'OfflineExampleLink' ## Mutation Scopes Per default, all mutations run in parallel - even if you invoke `.mutate()` of the same mutation multiple times. Mutations can be given a `scope` with an `id` to avoid that. All mutations with the same `scope.id` will run in serial, which means when they are triggered, they will start in `isPaused: true` state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come. [//]: # 'ExampleScopes' ```tsx const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) ``` [//]: # 'ExampleScopes' [//]: # 'Materials' ## Further reading For more information about mutations, have a look at [TkDodo's article on Mastering Mutations in React Query](https://tkdodo.eu/blog/mastering-mutations-in-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/network-mode.md ================================================ --- id: network-mode title: Network Mode --- TanStack Query provides three different network modes to distinguish how [Queries](./queries.md) and [Mutations](./mutations.md) should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is [online](#network-mode-online). ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the `state` (`pending`, `error`, `success`) it is in if the fetch cannot be made because there is no network connection. However, a [fetchStatus](./queries.md#fetchstatus) is exposed additionally. This can be either: - `fetching`: The `queryFn` is really executing - a request is in-flight. - `paused`: The query is not executing - it is `paused` until you have connection again - `idle`: The query is not fetching and not paused The flags `isFetching` and `isPaused` are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for `pending` state to show a loading spinner. Queries can be in `state: 'pending'`, but `fetchStatus: 'paused'` if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of `refetchOnReconnect` (which also defaults to `true` in this mode), because it is not a `refetch`, but rather a `continue`. If the query has been [cancelled](./query-cancellation.md) in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from `AsyncStorage`, or if you just want to return `Promise.resolve(5)` from your `queryFn`. - Queries will never be `paused` because you have no network connection. - Retries will also not pause - your Query will go to `error` state if it fails. - `refetchOnReconnect` defaults to `false` in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the `queryFn` once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an [offline-first PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers), or if you use HTTP caching via the [Cache-Control header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#the_cache-control_header). In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an `online` query - pausing retries. ## Devtools The [TanStack Query Devtools](../devtools.md) will show Queries in a `paused` state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the [OnlineManager](../../../reference/onlineManager.md) in an offline state. ## Signature - `networkMode: 'online' | 'always' | 'offlineFirst'` - optional - defaults to `'online'` ================================================ FILE: docs/framework/react/guides/optimistic-updates.md ================================================ --- id: optimistic-updates title: Optimistic Updates --- React Query provides two ways to optimistically update your UI before a mutation has completed. You can either use the `onMutate` option to update your cache directly, or leverage the returned `variables` to update your UI from the `useMutation` result. ## Via the UI This is the simpler variant, as it doesn't interact with the cache directly. [//]: # 'ExampleUI1' ```tsx const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation ``` [//]: # 'ExampleUI1' you will then have access to `addTodoMutation.variables`, which contain the added todo. In your UI list, where the query is rendered, you can append another item to the list while the mutation `isPending`: [//]: # 'ExampleUI2' ```tsx
      {todoQuery.items.map((todo) => (
    • {todo.text}
    • ))} {isPending &&
    • {variables}
    • }
    ``` [//]: # 'ExampleUI2' We're rendering a temporary item with a different `opacity` as long as the mutation is pending. Once it completes, the item will automatically no longer be rendered. Given that the refetch succeeded, we should see the item as a "normal item" in our list. If the mutation errors, the item will also disappear. But we could continue to show it, if we want, by checking for the `isError` state of the mutation. `variables` are _not_ cleared when the mutation errors, so we can still access them, maybe even show a retry button: [//]: # 'ExampleUI3' ```tsx { isError && (
  • {variables}
  • ) } ``` [//]: # 'ExampleUI3' ### If the mutation and the query don't live in the same component This approach works very well if the mutation and the query live in the same component. However, you also get access to all mutations in other components via the dedicated `useMutationState` hook. It is best combined with a `mutationKey`: [//]: # 'ExampleUI4' ```tsx // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) ``` [//]: # 'ExampleUI4' `variables` will be an `Array`, because there might be multiple mutations running at the same time. If we need a unique key for the items, we can also select `mutation.state.submittedAt`. This will even make displaying concurrent optimistic updates a breeze. ## Via the cache When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won't make it possible to refetch. In this event, you can instead choose to roll back your update. To do this, `useMutation`'s `onMutate` handler option allows you to return a value that will later be passed to both `onError` and `onSettled` handlers as the last argument. In most cases, it is most useful to pass a rollback function. ### Updating a list of todos when adding a new todo [//]: # 'Example' ```tsx const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await context.client.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = context.client.getQueryData(['todos']) // Optimistically update to the new value context.client.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a result with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the result returned from onMutate to roll back onError: (err, newTodo, onMutateResult, context) => { context.client.setQueryData(['todos'], onMutateResult.previousTodos) }, // Always refetch after error or success: onSettled: (data, error, variables, onMutateResult, context) => context.client.invalidateQueries({ queryKey: ['todos'] }), }) ``` [//]: # 'Example' ### Updating a single todo [//]: # 'Example2' ```tsx useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = context.client.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value context.client.setQueryData(['todos', newTodo.id], newTodo) // Return a result with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the result we returned above onError: (err, newTodo, onMutateResult, context) => { context.client.setQueryData( ['todos', onMutateResult.newTodo.id], onMutateResult.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo, error, variables, onMutateResult, context) => context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) ``` [//]: # 'Example2' You can also use the `onSettled` function in place of the separate `onError` and `onSuccess` handlers if you wish: [//]: # 'Example3' ```tsx useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, onMutateResult, context) => { if (error) { // do something } }, }) ``` [//]: # 'Example3' ## When to use what If you only have one place where the optimistic result should be shown, using `variables` and updating the UI directly is the approach that requires less code and is generally easier to reason about. For example, you don't need to handle rollbacks at all. However, if you have multiple places on the screen that would require to know about the update, manipulating the cache directly will take care of this for you automatically. [//]: # 'Materials' ## Further reading Have a look at the guide by TkDodo on [Concurrent Optimistic Updates](https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/paginated-queries.md ================================================ --- id: paginated-queries title: Paginated / Lagged Queries --- Rendering paginated data is a very common UI pattern and in TanStack Query, it "just works" by including the page information in the query key: [//]: # 'Example' ```tsx const result = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), }) ``` [//]: # 'Example' However, if you run this simple example, you might notice something strange: **The UI jumps in and out of the `success` and `pending` states because each new page is treated like a brand new query.** This experience is not optimal and unfortunately is how many tools today insist on working. But not TanStack Query! As you may have guessed, TanStack Query comes with an awesome feature called `placeholderData` that allows us to get around this. ## Better Paginated Queries with `placeholderData` Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use `useQuery`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `pending` states as different queries are created and destroyed for each page or cursor. By setting `placeholderData` to `(previousData) => previousData` or `keepPreviousData` function exported from TanStack Query, we get a few new things: - **The data from the last successful fetch is available while new data is being requested, even though the query key has changed**. - When the new data arrives, the previous `data` is seamlessly swapped to show the new data. - `isPlaceholderData` is made available to know what data the query is currently providing you [//]: # 'Example2' ```tsx import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return (
    {isPending ? (
    Loading...
    ) : isError ? (
    Error: {error.message}
    ) : (
    {data.projects.map((project) => (

    {project.name}

    ))}
    )} Current Page: {page + 1} {isFetching ? Loading... : null}
    ) } ``` [//]: # 'Example2' ## Lagging Infinite Query results with `placeholderData` While not as common, the `placeholderData` option also works flawlessly with the `useInfiniteQuery` hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time. ================================================ FILE: docs/framework/react/guides/parallel-queries.md ================================================ --- id: parallel-queries title: Parallel Queries --- "Parallel" queries are queries that are executed in parallel, or at the same time so as to maximize fetching concurrency. ## Manual Parallel Queries When the number of parallel queries does not change, there is **no extra effort** to use parallel queries. Just use any number of TanStack Query's `useQuery` and `useInfiniteQuery` hooks side-by-side! [//]: # 'Example' ```tsx function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } ``` [//]: # 'Example' [//]: # 'Info' > When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the `useSuspenseQueries` hook (which is suggested) or orchestrate your own parallelism with separate components for each `useSuspenseQuery` instance. [//]: # 'Info' ## Dynamic Parallel Queries with `useQueries` [//]: # 'DynamicParallelIntro' If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, TanStack Query provides a `useQueries` hook, which you can use to dynamically execute as many queries in parallel as you'd like. [//]: # 'DynamicParallelIntro' [//]: # 'DynamicParallelDescription' `useQueries` accepts an **options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: [//]: # 'DynamicParallelDescription' [//]: # 'Example2' ```tsx function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/react/guides/placeholder-query-data.md ================================================ --- id: placeholder-query-data title: Placeholder Query Data --- ## What is placeholder data? Placeholder data allows a query to behave as if it already has data, similar to the `initialData` option, but **the data is not persisted to the cache**. This comes in handy for situations where you have enough partial (or fake) data to render the query successfully while the actual data is fetched in the background. > Example: An individual blog post query could pull "preview" data from a parent list of blog posts that only include title and a small snippet of the post body. You would not want to persist this partial data to the query result of the individual query, but it is useful for showing the content layout as quickly as possible while the actual query finishes to fetch the entire object. There are a few ways to supply placeholder data for a query to the cache before you need it: - Declaratively: - Provide `placeholderData` to a query to prepopulate its cache if empty - Imperatively: - [Prefetch or fetch the data using `queryClient` and the `placeholderData` option](./prefetching.md) When we use `placeholderData`, our Query will not be in a `pending` state - it will start out as being in `success` state, because we have `data` to display - even if that data is just "placeholder" data. To distinguish it from "real" data, we will also have the `isPlaceholderData` flag set to `true` on the Query result. ## Placeholder Data as a Value [//]: # 'ExampleValue' ```tsx function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } ``` [//]: # 'ExampleValue' [//]: # 'Memoization' ### Placeholder Data Memoization If the process for accessing a query's placeholder data is intensive or just not something you want to perform on every render, you can memoize the value: ```tsx function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } ``` [//]: # 'Memoization' ## Placeholder Data as a Function `placeholderData` can also be a function, where you can get access to the data and Query meta information of a "previous" successful Query. This is useful for situations where you want to use the data from one query as the placeholder data for another query. When the QueryKey changes, e.g. from `['todos', 1]` to `['todos', 2]`, we can keep displaying "old" data instead of having to show a loading spinner while data is _transitioning_ from one Query to the next. For more information, see [Paginated Queries](./paginated-queries.md). [//]: # 'ExampleFunction' ```tsx const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) ``` [//]: # 'ExampleFunction' ### Placeholder Data from Cache In some circumstances, you may be able to provide the placeholder data for a query from the cached result of another. A good example of this would be searching the cached data from a blog post list query for a preview version of the post, then using that as the placeholder data for your individual post query: [//]: # 'ExampleCache' ```tsx function BlogPost({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } ``` [//]: # 'ExampleCache' [//]: # 'Materials' ## Further reading For a comparison between `Placeholder Data` and `Initial Data`, see the [article by TkDodo](https://tkdodo.eu/blog/placeholder-and-initial-data-in-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/prefetching.md ================================================ --- id: prefetching title: Prefetching & Router Integration --- When you know or suspect that a certain piece of data will be needed, you can use prefetching to populate the cache with that data ahead of time, leading to a faster experience. There are a few different prefetching patterns: 1. In event handlers 2. In components 3. Via router integration 4. During Server Rendering (another form of router integration) In this guide, we'll take a look at the first three, while the fourth will be covered in depth in the [Server Rendering & Hydration guide](./ssr.md) and the [Advanced Server Rendering guide](./advanced-ssr.md). One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the [Performance & Request Waterfalls guide](./request-waterfalls.md). ## prefetchQuery & prefetchInfiniteQuery Before jumping into the different specific prefetch patterns, let's look at the `prefetchQuery` and `prefetchInfiniteQuery` functions. First a few basics: - Out of the box, these functions use the default `staleTime` configured for the `queryClient` to determine whether existing data in the cache is fresh or needs to be fetched again - You can also pass a specific `staleTime` like this: `prefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` - This `staleTime` is only used for the prefetch, you still need to set it for any `useQuery` call as well - If you want to ignore `staleTime` and instead always return data if it's available in the cache, you can use the `ensureQueryData` function. - Tip: If you are prefetching on the server, set a default `staleTime` higher than `0` for that `queryClient` to avoid having to pass in a specific `staleTime` to each prefetch call - If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime` - These functions return `Promise` and thus never return query data. If that's something you need, use `fetchQuery`/`fetchInfiniteQuery` instead. - The prefetch functions never throw errors because they usually try to fetch again in a `useQuery` which is a nice graceful fallback. If you need to catch errors, use `fetchQuery`/`fetchInfiniteQuery` instead. This is how you use `prefetchQuery`: [//]: # 'ExamplePrefetchQuery' ```tsx const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } ``` [//]: # 'ExamplePrefetchQuery' Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option, in which case you also have to provide a `getNextPageParam` function: [//]: # 'ExamplePrefetchInfiniteQuery' ```tsx const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } ``` [//]: # 'ExamplePrefetchInfiniteQuery' Next, let's look at how you can use these and other ways to prefetch in different situations. ## Prefetch in event handlers A straightforward form of prefetching is doing it when the user interacts with something. In this example we'll use `queryClient.prefetchQuery` to start a prefetch on `onMouseEnter` or `onFocus`. [//]: # 'ExampleEventHandler' ```tsx function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( ) } ``` [//]: # 'ExampleEventHandler' ## Prefetch in components Prefetching during the component lifecycle is useful when we know some child or descendant will need a particular piece of data, but we can't render that until some other query has finished loading. Let's borrow an example from the Request Waterfall guide to explain: [//]: # 'ExampleComponent' ```tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } ``` [//]: # 'ExampleComponent' This results in a request waterfall looking like this: ``` 1. |> getArticleById() 2. |> getArticleCommentsById() ``` As mentioned in that guide, one way to flatten this waterfall and improve performance is to hoist the `getArticleCommentsById` query to the parent and pass down the result as a prop, but what if this is not feasible or desirable, for example when the components are unrelated and have multiple levels between them? In that case, we can instead prefetch the query in the parent. The simplest way to do this is to use a query but ignore the result: [//]: # 'ExampleParentComponent' ```tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } ``` [//]: # 'ExampleParentComponent' This starts fetching `'article-comments'` immediately and flattens the waterfall: ``` 1. |> getArticleById() 1. |> getArticleCommentsById() ``` [//]: # 'Suspense' If you want to prefetch together with Suspense, you will have to do things a bit differently. You can't use `useSuspenseQueries` to prefetch, since the prefetch would block the component from rendering. You also can not use `useQuery` for the prefetch, because that wouldn't start the prefetch until after suspenseful query had resolved. For this scenario, you can use the [`usePrefetchQuery`](../reference/usePrefetchQuery.md) or the [`usePrefetchInfiniteQuery`](../reference/usePrefetchInfiniteQuery.md) hooks available in the library. You can now use `useSuspenseQuery` in the component that actually needs the data. You _might_ want to wrap this later component in its own `` boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. ```tsx function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return (
    ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } ``` Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.prefetchQuery`: ```tsx const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) ``` Prefetching in an effect also works, but note that if you are using `useSuspenseQuery` in the same component, this effect wont run until _after_ the query finishes which might not be what you want. ```tsx const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) ``` To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best: - Prefetch before a suspense boundary using `usePrefetchQuery` or `usePrefetchInfiniteQuery` hooks - Use `useQuery` or `useSuspenseQueries` and ignore the result - Prefetch inside the query function - Prefetch in an effect Let's look at a slightly more advanced case next. [//]: # 'Suspense' ### Dependent Queries & Code Splitting Sometimes we want to prefetch conditionally, based on the result of another fetch. Consider this example borrowed from the [Performance & Request Waterfalls guide](./request-waterfalls.md): [//]: # 'ExampleConditionally1' ```tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return } return })} ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } ``` [//]: # 'ExampleConditionally1' As noted over in that guide, this example leads to the following double request waterfall: ``` 1. |> getFeed() 2. |> JS for 3. |> getGraphDataById() ``` If we can not restructure our API so `getFeed()` also returns the `getGraphDataById()` data when necessary, there is no way to get rid of the `getFeed->getGraphDataById` waterfall, but by leveraging conditional prefetching, we can at least load the code and data in parallel. Just like described above, there are multiple ways to do this, but for this example, we'll do it in the query function: [//]: # 'ExampleConditionally2' ```tsx function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } ``` [//]: # 'ExampleConditionally2' This would load the code and data in parallel: ``` 1. |> getFeed() 2. |> JS for 2. |> getGraphDataById() ``` There is a tradeoff however, in that the code for `getGraphDataById` is now included in the parent bundle instead of in `JS for ` so you'll need to determine what's the best performance tradeoff on a case by case basis. If `GraphFeedItem` are likely, it's probably worth to include the code in the parent. If they are exceedingly rare, it's probably not. [//]: # 'Router' ## Router Integration Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level. In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the [Server Rendering & Hydration guide](./ssr.md). For now, let's focus on the client side case and look at an example of how you can make this work with [TanStack Router](https://tanstack.com/router). These examples leave out a lot of setup and boilerplate to stay concise, you can check out a [full React Query example](https://tanstack.com/router/latest/docs/framework/react/examples/basic-react-query-file-based) over in the [TanStack Router docs](https://tanstack.com/router/latest/docs). When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. ```tsx const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) ``` Integration with other routers is also possible, see the [react-router](../examples/react-router) for another demonstration. [//]: # 'Router' ## Manually Priming a Query If you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the [Query Client's `setQueryData` method](../../../reference/QueryClient.md#queryclientsetquerydata) to directly add or update a query's cached result by key. [//]: # 'ExampleManualPriming' ```tsx queryClient.setQueryData(['todos'], todos) ``` [//]: # 'ExampleManualPriming' [//]: # 'Materials' ## Further reading For a deep-dive on how to get data into your Query Cache before you fetch, see the [article Seeding the Query Cache by TkDodo](https://tkdodo.eu/blog/seeding-the-query-cache). Integrating with Server Side routers and frameworks is very similar to what we just saw, with the addition that the data has to be passed from the server to the client to be hydrated into the cache there. To learn how, continue on to the [Server Rendering & Hydration guide](./ssr.md). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/queries.md ================================================ --- id: queries title: Queries --- ## Query Basics A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using [Mutations](./mutations.md) instead. [//]: # 'SubscribeDescription' To subscribe to a query in your components or custom hooks, call the `useQuery` hook with at least: [//]: # 'SubscribeDescription' - A **unique key for the query** - A function that returns a promise that: - Resolves the data, or - Throws an error [//]: # 'Example' ```tsx import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } ``` [//]: # 'Example' The **unique key** you provide is used internally for refetching, caching, and sharing your queries throughout your application. The query result returned by `useQuery` contains all of the information about the query that you'll need for templating and any other usage of the data: [//]: # 'Example2' ```tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) ``` [//]: # 'Example2' The `result` object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment: - `isPending` or `status === 'pending'` - The query has no data yet - `isError` or `status === 'error'` - The query encountered an error - `isSuccess` or `status === 'success'` - The query was successful and data is available Beyond those primary states, more information is available depending on the state of the query: - `error` - If the query is in an `isError` state, the error is available via the `error` property. - `data` - If the query is in an `isSuccess` state, the data is available via the `data` property. - `isFetching` - In any state, if the query is fetching at any time (including background refetching) `isFetching` will be `true`. For **most** queries, it's usually sufficient to check for the `isPending` state, then the `isError` state, then finally, assume that the data is available and render the successful state: [//]: # 'Example3' ```tsx function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return Loading... } if (isError) { return Error: {error.message} } // We can assume by this point that `isSuccess === true` return (
      {data.map((todo) => (
    • {todo.title}
    • ))}
    ) } ``` [//]: # 'Example3' If booleans aren't your thing, you can always use the `status` state as well: [//]: # 'Example4' ```tsx function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return Loading... } if (status === 'error') { return Error: {error.message} } // also status === 'success', but "else" logic works, too return (
      {data.map((todo) => (
    • {todo.title}
    • ))}
    ) } ``` [//]: # 'Example4' TypeScript will also narrow the type of `data` correctly if you've checked for `pending` and `error` before accessing it. ### FetchStatus In addition to the `status` field, you will also get an additional `fetchStatus` property with the following options: - `fetchStatus === 'fetching'` - The query is currently fetching. - `fetchStatus === 'paused'` - The query wanted to fetch, but it is paused. Read more about this in the [Network Mode](./network-mode.md) guide. - `fetchStatus === 'idle'` - The query is not doing anything at the moment. ### Why two different states? Background refetches and stale-while-revalidate logic make all combinations for `status` and `fetchStatus` possible. For example: - a query in `success` status will usually be in `idle` fetchStatus, but it could also be in `fetching` if a background refetch is happening. - a query that mounts and has no data will usually be in `pending` status and `fetching` fetchStatus, but it could also be `paused` if there is no network connection. So keep in mind that a query can be in `pending` state without actually fetching data. As a rule of thumb: - The `status` gives information about the `data`: Do we have any or not? - The `fetchStatus` gives information about the `queryFn`: Is it running or not? [//]: # 'Materials' ## Further Reading For an alternative way of performing status checks, have a look at [this article by TkDodo](https://tkdodo.eu/blog/status-checks-in-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/query-cancellation.md ================================================ --- id: query-cancellation title: Query Cancellation --- TanStack Query provides each query function with an [`AbortSignal` instance](https://developer.mozilla.org/docs/Web/API/AbortSignal). When a query becomes out-of-date or inactive, this `signal` will become aborted. This means that all queries are cancellable, and you can respond to the cancellation inside your query function if desired. The best part about this is that it allows you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation. The `AbortController` API is available in [most runtime environments](https://developer.mozilla.org/docs/Web/API/AbortController#browser_compatibility), but if your runtime environment does not support it, you will need to provide a polyfill. There are [several available](https://www.npmjs.com/search?q=abortcontroller%20polyfill). ## Default behavior By default, queries that unmount or become unused before their promises are resolved are _not_ cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available. However, if you consume the `AbortSignal`, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being _reverted_ to its previous state. ## Using `fetch` [//]: # 'Example' ```tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) ``` [//]: # 'Example' ## Using `axios` [v0.22.0+](https://github.com/axios/axios/releases/tag/v0.22.0) [//]: # 'Example2' ```tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) ``` [//]: # 'Example2' ### Using `axios` with version lower than v0.22.0 [//]: # 'Example3' ```tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) ``` [//]: # 'Example3' ## Using `XMLHttpRequest` [//]: # 'Example4' ```tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) ``` [//]: # 'Example4' ## Using `graphql-request` An `AbortSignal` can be set in the client `request` method. [//]: # 'Example5' ```tsx const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) ``` [//]: # 'Example5' ## Using `graphql-request` with version lower than v4.0.0 An `AbortSignal` can be set in the `GraphQLClient` constructor. [//]: # 'Example6' ```tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) ``` [//]: # 'Example6' ## Manual Cancellation You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `queryClient.cancelQueries({ queryKey })`, which will cancel the query and revert it back to its previous state. If you have consumed the `signal` passed to the query function, TanStack Query will additionally also cancel the Promise. [//]: # 'Example7' ```tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( ) ``` [//]: # 'Example7' ## `Cancel Options` Cancel options are used to control the behavior of query cancellation operations. ```tsx // Cancel specific queries silently await queryClient.cancelQueries({ queryKey: ['posts'] }, { silent: true }) ``` A cancel options object supports the following properties: - `silent?: boolean` - When set to `true`, suppresses propagation of `CancelledError` to observers (e.g., `onError` callbacks) and related notifications, and returns the retry promise instead of rejecting. - Defaults to `false` - `revert?: boolean` - When set to `true`, restores the query’s state (data and status) from immediately before the in-flight fetch, sets `fetchStatus` back to `idle`, and only throws if there was no prior data. - Defaults to `true` ## Limitations [//]: # 'Limitations' Cancellation does not work when working with `Suspense` hooks: `useSuspenseQuery`, `useSuspenseQueries` and `useSuspenseInfiniteQuery`. [//]: # 'Limitations' ================================================ FILE: docs/framework/react/guides/query-functions.md ================================================ --- id: query-functions title: Query Functions --- A query function can be literally any function that **returns a promise**. The promise that is returned should either **resolve the data** or **throw an error**. All of the following are valid query function configurations: [//]: # 'Example' ```tsx useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) ``` [//]: # 'Example' ## Handling and Throwing Errors For TanStack Query to determine a query has errored, the query function **must throw** or return a **rejected Promise**. Any error that is thrown in the query function will be persisted on the `error` state of the query. [//]: # 'Example2' ```tsx const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) ``` [//]: # 'Example2' ## Usage with `fetch` and other clients that do not throw by default While most utilities like `axios` or `graphql-request` automatically throw errors for unsuccessful HTTP calls, some utilities like `fetch` do not throw errors by default. If that's the case, you'll need to throw them on your own. Here is a simple way to do that with the popular `fetch` API: [//]: # 'Example3' ```tsx useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) ``` [//]: # 'Example3' ## Query Function Variables Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function as part of the QueryFunctionContext. While not always necessary, this makes it possible to extract your query functions if needed: [//]: # 'Example4' ```tsx function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ``` [//]: # 'Example4' ### QueryFunctionContext The `QueryFunctionContext` is the object passed to each query function. It consists of: - `queryKey: QueryKey`: [Query Keys](./query-keys.md) - `client: QueryClient`: [QueryClient](../../../reference/QueryClient.md) - `signal?: AbortSignal` - [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance provided by TanStack Query - Can be used for [Query Cancellation](./query-cancellation.md) - `meta: Record | undefined` - an optional field you can fill with additional information about your query Additionally, [Infinite Queries](./infinite-queries.md) get the following options passed: - `pageParam: TPageParam` - the page parameter used to fetch the current page - `direction: 'forward' | 'backward'` - **deprecated** - the direction of the current page fetch - To get access to the direction of the current page fetch, please add a direction to `pageParam` from `getNextPageParam` and `getPreviousPageParam`. ================================================ FILE: docs/framework/react/guides/query-invalidation.md ================================================ --- id: query-invalidation title: Query Invalidation --- Waiting for queries to become stale before they are fetched again doesn't always work, especially when you know for a fact that a query's data is out of date because of something the user has done. For that purpose, the `QueryClient` has an `invalidateQueries` method that lets you intelligently mark queries as stale and potentially refetch them too! [//]: # 'Example' ```tsx // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) ``` [//]: # 'Example' > Note: Where other libraries that use normalized caches would attempt to update local queries with the new data either imperatively or via schema inference, TanStack Query gives you the tools to avoid the manual labor that comes with maintaining normalized caches and instead prescribes **targeted invalidation, background-refetching and ultimately atomic updates**. When a query is invalidated with `invalidateQueries`, two things happen: - It is marked as stale. This stale state overrides any `staleTime` configurations being used in `useQuery` or related hooks - If the query is currently being rendered via `useQuery` or related hooks, it will also be refetched in the background ## Query Matching with `invalidateQueries` When using APIs like `invalidateQueries` and `removeQueries` (and others that support partial query matching), you can match multiple queries by their prefix, or get really specific and match an exact query. For information on the types of filters you can use, please see [Query Filters](./filters.md#query-filters). In this example, we can use the `todos` prefix to invalidate any queries that start with `todos` in their query key: [//]: # 'Example2' ```tsx import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) ``` [//]: # 'Example2' You can even invalidate queries with specific variables by passing a more specific query key to the `invalidateQueries` method: [//]: # 'Example3' ```tsx queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) ``` [//]: # 'Example3' The `invalidateQueries` API is very flexible, so even if you want to **only** invalidate `todos` queries that don't have any more variables or subkeys, you can pass an `exact: true` option to the `invalidateQueries` method: [//]: # 'Example4' ```tsx queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) ``` [//]: # 'Example4' If you find yourself wanting **even more** granularity, you can pass a predicate function to the `invalidateQueries` method. This function will receive each `Query` instance from the query cache and allow you to return `true` or `false` for whether you want to invalidate that query: [//]: # 'Example5' ```tsx queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) ``` [//]: # 'Example5' ================================================ FILE: docs/framework/react/guides/query-keys.md ================================================ --- id: query-keys title: Query Keys --- At its core, TanStack Query manages query caching for you based on query keys. Query keys have to be an Array at the top level, and can be as simple as an Array with a single string, or as complex as an array of many strings and nested objects. As long as the query key is serializable using `JSON.stringify`, and **unique to the query's data**, you can use it! ## Simple Query Keys The simplest form of a key is an array with constants values. This format is useful for: - Generic List/Index resources - Non-hierarchical resources [//]: # 'Example' ```tsx // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) ``` [//]: # 'Example' ## Array Keys with variables When a query needs more information to uniquely describe its data, you can use an array with a string and any number of serializable objects to describe it. This is useful for: - Hierarchical or nested resources - It's common to pass an ID, index, or other primitive to uniquely identify the item - Queries with additional parameters - It's common to pass an object of additional options [//]: # 'Example2' ```tsx // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) ``` [//]: # 'Example2' ## Query Keys are hashed deterministically! This means that no matter the order of keys in objects, all of the following queries are considered equal: [//]: # 'Example3' ```tsx useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) ``` [//]: # 'Example3' The following query keys, however, are not equal. Array item order matters! [//]: # 'Example4' ```tsx useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) ``` [//]: # 'Example4' ## If your query function depends on a variable, include it in your query key Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that **change**. For example: [//]: # 'Example5' ```tsx function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } ``` [//]: # 'Example5' Note that query keys act as dependencies for your query functions. Adding dependent variables to your query key will ensure that queries are cached independently, and that any time a variable changes, _queries will be refetched automatically_ (depending on your `staleTime` settings). See the [exhaustive-deps](../../../eslint/exhaustive-deps.md) section for more information and examples. [//]: # 'Materials' ## Further reading For tips on organizing Query Keys in larger applications, have a look at [Effective React Query Keys](https://tkdodo.eu/blog/effective-react-query-keys) and check the [Query Key Factory Package](https://github.com/lukemorales/query-key-factory) from the [Community Resources](../../../community-resources). [//]: # 'Materials' ================================================ FILE: docs/framework/react/guides/query-options.md ================================================ --- id: query-options title: Query Options --- One of the best ways to share `queryKey` and `queryFn` between multiple places, yet keep them co-located to one another, is to use the `queryOptions` helper. At runtime, this helper just returns whatever you pass into it, but it has a lot of advantages when using it [with TypeScript](../typescript.md#typing-query-options). You can define all possible options for a query in one place, and you'll also get type inference and type safety for all of them. [//]: # 'Example1' ```ts import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) ``` [//]: # 'Example1' For Infinite Queries, a separate [`infiniteQueryOptions`](../reference/infiniteQueryOptions.md) helper is available. [//]: # 'SelectDescription' You can still override some options at the component level. A very common and useful pattern is to create per-component [`select`](./render-optimizations.md#select) functions: [//]: # 'SelectDescription' [//]: # 'Example2' ```ts // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/react/guides/query-retries.md ================================================ --- id: query-retries title: Query Retries --- When a `useQuery` query fails (the query function throws an error), TanStack Query will automatically retry the query if that query's request has not reached the max number of consecutive retries (defaults to `3`) or a function is provided to determine if a retry is allowed. You can configure retries both on a global level and an individual query level. - Setting `retry = false` will disable retries. - Setting `retry = 6` will retry failing requests 6 times before showing the final error thrown by the function. - Setting `retry = true` will infinitely retry failing requests. - Setting `retry = (failureCount, error) => ...` allows for custom logic based on why the request failed. Note that `failureCount` starts at `0` for the first retry attempt. [//]: # 'Info' > On the server, retries default to `0` to make server rendering as fast as possible. [//]: # 'Info' [//]: # 'Example' ```tsx import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) ``` [//]: # 'Example' > Info: Contents of the `error` property will be part of `failureReason` response property of `useQuery` until the last retry attempt. So in above example any error contents will be part of `failureReason` property for first 9 retry attempts (Overall 10 attempts) and finally they will be part of `error` after last attempt if error persists after all retry attempts. ## Retry Delay By default, retries in TanStack Query do not happen immediately after a request fails. As is standard, a back-off delay is gradually applied to each retry attempt. The default `retryDelay` is set to double (starting at `1000`ms) with each attempt, but not exceed 30 seconds: [//]: # 'Example2' ```tsx // Configure for all queries import { QueryCache, QueryClient, QueryClientProvider, } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }, }, }) function App() { return ... } ``` [//]: # 'Example2' Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: [//]: # 'Example3' ```tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) ``` [//]: # 'Example3' ## Background Retry Behavior When using `refetchInterval` with `refetchIntervalInBackground: true`, retries will pause when the browser tab is inactive. This happens because retries respect the same focus behavior as regular refetches. If you need continuous retries in the background, consider disabling retries and implementing a custom refetch strategy: [//]: # 'Example4' ```tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchInterval: (query) => { // Refetch more frequently when in error state return query.state.status === 'error' ? 5000 : 30000 }, refetchIntervalInBackground: true, retry: false, // Disable built-in retries }) ``` [//]: # 'Example4' This approach lets you control retry timing manually while keeping refetches active in the background. ================================================ FILE: docs/framework/react/guides/render-optimizations.md ================================================ --- id: render-optimizations title: Render Optimizations --- React Query applies a couple of optimizations automatically to ensure that your components only re-render when they actually need to. This is done by the following means: ## structural sharing React Query uses a technique called "structural sharing" to ensure that as many references as possible will be kept intact between re-renders. If data is fetched over the network, usually, you'll get a completely new reference by json parsing the response. However, React Query will keep the original reference if _nothing_ changed in the data. If a subset changed, React Query will keep the unchanged parts and only replace the changed parts. > Note: This optimization only works if the `queryFn` returns JSON compatible data. You can turn it off by setting `structuralSharing: false` globally or on a per-query basis, or you can implement your own structural sharing by passing a function to it. ### referential identity The top level object returned from `useQuery`, `useInfiniteQuery`, `useMutation` and the Array returned from `useQueries` is **not referentially stable**. It will be a new reference on every render. However, the `data` properties returned from these hooks will be as stable as possible. ## tracked properties React Query will only trigger a re-render if one of the properties returned from `useQuery` is actually "used". This is done by using [Proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This avoids a lot of unnecessary re-renders, e.g. because properties like `isFetching` or `isStale` might change often, but are not used in the component. You can customize this feature by setting `notifyOnChangeProps` manually globally or on a per-query basis. If you want to turn that feature off, you can set `notifyOnChangeProps: 'all'`. > Note: The get trap of a proxy is invoked by accessing a property, either via destructuring or by accessing it directly. If you use object rest destructuring, you will disable this optimization. We have a [lint rule](../../../eslint/no-rest-destructuring.md) to guard against this pitfall. ## select You can use the `select` option to select a subset of the data that your component should subscribe to. This is useful for highly optimized data transformations or to avoid unnecessary re-renders. ```js export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } ``` A component using the `useTodoCount` custom hook will only re-render if the length of the todos changes. It will **not** re-render if e.g. the name of a todo changed. > Note: `select` operates on successfully cached data and is not the appropriate place to throw errors. The source of truth for errors is the `queryFn`, and a `select` function that returns an error results in `data` being `undefined` and `isSuccess` being `true`. We recommend handling errors in the `queryFn` if you wish to have a query fail on incorrect data, or outside of the query hook if you have a error case not related to caching. ### memoization The `select` function will only re-run if: - the `select` function itself changed referentially - `data` changed This means that an inlined `select` function, as shown above, will run on every render. To avoid this, you can wrap the `select` function in `useCallback`, or extract it to a stable function reference if it doesn't have any dependencies: ```js // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } ``` ```js // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } ``` ## Further Reading For an in-depth guide about these topics, read [React Query Render Optimizations](https://tkdodo.eu/blog/react-query-render-optimizations) from the TkDodo. To learn how to best optimize the `select` option, read [React Query Selectors, Supercharged](https://tkdodo.eu/blog/react-query-selectors-supercharged) ================================================ FILE: docs/framework/react/guides/request-waterfalls.md ================================================ --- id: request-waterfalls title: Performance & Request Waterfalls --- Application performance is a broad and complex area and while React Query can't make your APIs faster, there are still things to be mindful about in how you use React Query to ensure the best performance. The biggest performance footgun when using React Query, or indeed any data fetching library that lets you fetch data inside of components, is request waterfalls. The rest of this page will explain what they are, how you can spot them and how you can restructure your application or APIs to avoid them. The [Prefetching & Router Integration guide](./prefetching.md) builds on this and teaches you how to prefetch data ahead of time when it's not possible or feasible to restructure your application or APIs. The [Server Rendering & Hydration guide](./ssr.md) teaches you how to prefetch data on the server and pass that data down to the client so you don't have to fetch it again. [//]: # 'AdvancedSSRLink' The [Advanced Server Rendering guide](./advanced-ssr.md) further teaches you how to apply these patterns to Server Components and Streaming Server Rendering. [//]: # 'AdvancedSSRLink' ## What is a Request Waterfall? A request waterfall is what happens when a request for a resource (code, css, images, data) does not start until _after_ another request for a resource has finished. Consider a web page. Before you can load things like the CSS, JS etc, the browser first needs to load the markup. This is a request waterfall. ``` 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image ``` If you fetch your CSS inside a JS file, you now have a double waterfall: ``` 1. |-> Markup 2. |-> JS 3. |-> CSS ``` If that CSS uses a background image, it's a triple waterfall: ``` 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image ``` The best way to spot and analyze your request waterfalls is usually by opening your browsers devtools "Network" tab. Each waterfall represents at least one roundtrip to the server, unless the resource is locally cached (in practice, some of these waterfalls might represent more than one roundtrip because the browser needs to establish a connection which requires some back and forth, but let's ignore that here). Because of this, the negative effects of request waterfalls are highly dependent on the users latency. Consider the example of the triple waterfall, which actually represents 4 server roundtrips. With 250ms latency, which is not uncommon on 3g networks or in bad network conditions, we end up with a total time of 4\*250=1000ms **only counting latency**. If we were able to flatten that to the first example with only 2 roundtrips, we get 500ms instead, possibly loading that background image in half the time! ## Request Waterfalls & React Query Now let's consider React Query. We'll focus on the case without Server Rendering first. Before we can even start making a query, we need to load the JS, so before we can show that data on the screen, we have a double waterfall: ``` 1. |-> Markup 2. |-> JS 3. |-> Query ``` With this as a basis, let's look at a few different patterns that can lead to Request Waterfalls in React Query, and how to avoid them. - Single Component Waterfalls / Serial Queries - Nested Component Waterfalls - Code Splitting ### Single Component Waterfalls / Serial Queries When a single component first fetches one query, and then another, that's a request waterfall. This can happen when the second query is a [Dependent Query](./dependent-queries.md), that is, it depends on data from the first query when fetching: [//]: # 'DependentExample' ```tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) ``` [//]: # 'DependentExample' While not always feasible, for optimal performance it's better to restructure your API so you can fetch both of these in a single query. In the example above, instead of first fetching `getUserByEmail` to be able to `getProjectsByUser`, introducing a new `getProjectsByUserEmail` query would flatten the waterfall. [//]: # 'ServerComponentsNote1' > Another way to mitigate dependent queries without restructuring your API is to move the waterfall to the server where latency is lower. This is the idea behind Server Components which are covered in the [Advanced Server Rendering guide](./advanced-ssr.md). [//]: # 'ServerComponentsNote1' [//]: # 'SuspenseSerial' Another example of serial queries is when you use React Query with Suspense: ```tsx function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } ``` Note that with regular `useQuery` these would happen in parallel. Luckily, this is easy to fix, by always using the hook `useSuspenseQueries` when you have multiple suspenseful queries in a component. ```tsx const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) ``` [//]: # 'SuspenseSerial' ### Nested Component Waterfalls [//]: # 'NestedIntro' Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. This can happen both with `useQuery` and `useSuspenseQuery`. [//]: # 'NestedIntro' If the child renders conditionally based on the data in the parent, or if the child relies on some part of the result being passed down as a prop from the parent to make its query, we have a _dependent_ nested component waterfall. Let's first look at an example where the child is **not** dependent on the parent. [//]: # 'NestedExample' ```tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } ``` [//]: # 'NestedExample' Note that while `` takes a prop `id` from the parent, that id is already available when the `
    ` renders so there is no reason we could not fetch the comments at the same time as the article. In real world applications, the child might be nested far below the parent and these kinds of waterfalls are often trickier to spot and fix, but for our example, one way to flatten the waterfall would be to hoist the comments query to the parent instead: [//]: # 'NestedHoistedExample' ```tsx function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> {commentsPending ? ( 'Loading comments...' ) : ( )} ) } ``` [//]: # 'NestedHoistedExample' [//]: # 'NestedHoistedOutro' The two queries will now fetch in parallel. Note that if you are using suspense, you'd want to combine these two queries into a single `useSuspenseQueries` instead. [//]: # 'NestedHoistedOutro' Another way to flatten this waterfall would be to prefetch the comments in the `
    ` component, or prefetch both of these queries at the router level on page load or page navigation, read more about this in the [Prefetching & Router Integration guide](./prefetching.md). Next, let's look at a _Dependent Nested Component Waterfall_. [//]: # 'DependentNestedExample' ```tsx function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return } return })} ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } ``` [//]: # 'DependentNestedExample' The second query `getGraphDataById` is dependent on its parent in two different ways. First of all, it doesn't ever happen unless the `feedItem` is a graph, and second, it needs an `id` from the parent. ``` 1. |> getFeed() 2. |> getGraphDataById() ``` [//]: # 'ServerComponentsNote2' In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the `getFeed` query. Another more advanced solution is to leverage Server Components to move the waterfall to the server where latency is lower (read more about this in the [Advanced Server Rendering guide](./advanced-ssr.md)) but note that this can be a very big architectural change. [//]: # 'ServerComponentsNote2' You can have good performance even with a few query waterfalls here and there, just know they are a common performance concern and be mindful about them. An especially insidious version is when Code Splitting is involved, let's take a look at this next. ### Code Splitting Splitting an applications JS-code into smaller chunks and only loading the necessary parts is usually a critical step in achieving good performance. It does have a downside however, in that it often introduces request waterfalls. When that code split code also has a query inside it, this problem is worsened further. Consider this a slightly modified version of the Feed example. [//]: # 'LazyExample' ```tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return } return })} ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } ``` [//]: # 'LazyExample' This example has a double waterfall, looking like this: ``` 1. |> getFeed() 2. |> JS for 3. |> getGraphDataById() ``` But that's just looking at the code from the example, if we consider what the first page load of this page looks like, we actually have to complete 5 round trips to the server before we can render the graph! ``` 1. |> Markup 2. |> JS for 3. |> getFeed() 4. |> JS for 5. |> getGraphDataById() ``` Note that this looks a bit different when server rendering, we will explore that further in the [Server Rendering & Hydration guide](./ssr.md). Also note that it's not uncommon for the route that contains `` to also be code split, which could add yet another hop. In the code split case, it might actually help to hoist the `getGraphDataById` query to the `` component and make it conditional, or add a conditional prefetch. That query could then be fetched in parallel with the code, turning the example part into this: ``` 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for ``` This is very much a tradeoff however. You are now including the data fetching code for `getGraphDataById` in the same bundle as ``, so evaluate what is best for your case. Read more about how to do this in the [Prefetching & Router Integration guide](./prefetching.md). [//]: # 'ServerComponentsNote3' > The tradeoff between: > > - Include all data fetching code in the main bundle, even if we seldom use it > - Put the data fetching code in the code split bundle, but with a request waterfall > > is not great and has been one of the motivations for Server Components. With Server Components, it's possible to avoid both, read more about how this applies to React Query in the [Advanced Server Rendering guide](./advanced-ssr.md). [//]: # 'ServerComponentsNote3' ## Summary and takeaways Request Waterfalls are a very common and complex performance concern with many tradeoffs. There are many ways to accidentally introduce them into your application: - Adding a query to a child, not realizing a parent already has a query - Adding a query to a parent, not realizing a child already has a query - Moving a component with descendants that has a query to a new parent with an ancestor that has a query - Etc.. Because of this accidental complexity, it pays off to be mindful of waterfalls and regularly examine your application looking for them (a good way is to examine the Network tab every now and then!). You don't necessarily have to flatten them all to have good performance, but keep an eye out for the high impact ones. In the next guide, we'll look at more ways to flatten waterfalls, by leveraging [Prefetching & Router Integration](./prefetching.md). ================================================ FILE: docs/framework/react/guides/scroll-restoration.md ================================================ --- id: scroll-restoration title: Scroll Restoration --- Traditionally, when you navigate to a previously visited page on a web browser, you would find that the page would be scrolled to the exact position where you were before you navigated away from that page. This is called **scroll restoration** and has been in a bit of a regression since web applications have started moving towards client side data fetching. With TanStack Query however, that's no longer the case. TanStack Query doesn’t implement scroll restoration by itself, but it removes one of the biggest causes of broken restoration in SPA’s: refetch-induced UI resets. By keeping previously fetched data in cache (and optionally using `placeholderData`), navigation back to a page can render instantly with stable layout, making scroll restoration reliable when handled by the router (e.g. React Router’s ScrollRestoration, TanStack Router’s scroll restoration, or a small custom history-based solution). Out of the box, "scroll restoration" for all queries (including paginated and infinite queries) Just Works™️ in TanStack Query. The reason for this is that query results are cached and able to be retrieved synchronously when a query is rendered. As long as your queries are being cached long enough (the default time is 5 minutes) and have not been garbage collected, scroll restoration will work out of the box all the time. ================================================ FILE: docs/framework/react/guides/ssr.md ================================================ --- id: ssr title: Server Rendering & Hydration --- In this guide you'll learn how to use React Query with server rendering. See the guide on [Prefetching & Router Integration](./prefetching.md) for some background. You might also want to check out the [Performance & Request Waterfalls guide](./request-waterfalls.md) before that. For deeper examples on hydration + prefetching (including code splitting), see the [Dependent Queries & Code Splitting](./prefetching.md#dependent-queries-code-splitting) section. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the [Advanced Server Rendering guide](./advanced-ssr.md). If you just want to see some code, you can skip ahead to the [Full Next.js pages router example](#full-nextjs-pages-router-example) or the [Full Remix example](#full-remix-example) below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the [Performance & Request Waterfalls guide](./request-waterfalls.md), you might remember this: ``` 1. |-> Markup (without content) 2. |-> JS 3. |-> Query ``` With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: ``` 1. |-> Markup (with content AND initial data) 2. |-> JS ``` As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular `useQuery` API. While we don't necessarily recommend it, it is possible to replace this with `useSuspenseQuery` instead **as long as you always prefetch all your queries**. The upside is that you get to use `` for loading states on the client. If you do forget to prefetch a query when you are using `useSuspenseQuery`, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a `queryClient` and wrap the application in a ``. When doing server rendering, it's important to create the `queryClient` instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the `queryClient` once per component lifecycle. Next.js pages router: ```tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( ) } ``` Remix: ```tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( ) } ``` ## Get started fast with `initialData` The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the `dehydrate`/`hydrate` APIs. What you do instead is passing the raw data in as the `initialData` option to `useQuery`. Let's look at an example using Next.js pages router, using `getServerSideProps`. ```tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } ``` This also works with `getStaticProps` or even the older `getInitialProps` and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: ```tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } ``` The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: - If you are calling `useQuery` in a component deeper down in the tree you need to pass the `initialData` down to that point - If you are calling `useQuery` with the same query in multiple locations, passing `initialData` to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the `useQuery` with `initialData`, the more deeply nested `useQuery` might no longer have any data. Passing `initialData` to **all** queries that needs it can also be cumbersome. - There is no way to know at what time the query was fetched on the server, so `dataUpdatedAt` and determining if the query needs refetching is based on when the page loaded instead - If there is already data in the cache for a query, `initialData` will never overwrite this data, **even if the new data is fresher than the old one**. - To understand why this is especially bad, consider the `getServerSideProps` example above. If you navigate back and forth to a page several times, `getServerSideProps` would get called each time and fetch new data, but because we are using the `initialData` option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a `queryClient` to prefetch queries during a preload phase, pass a serialized version of that `queryClient` to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: - In the framework loader function, create a `const queryClient = new QueryClient(options)` - In the loader function, do `await queryClient.prefetchQuery(...)` for each query you want to prefetch - You want to use `await Promise.all(...)` to fetch the queries in parallel when possible - It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. - From the loader, return `dehydrate(queryClient)`, note that the exact syntax to return this differs between frameworks - Wrap your tree with `` where `dehydratedState` comes from the framework loader. How you get `dehydratedState` also differs between frameworks. - This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ `queryClient`s involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own `queryClient` that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own `queryClient`. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the [Advanced Server Rendering guide](./advanced-ssr.md). ### Full Next.js pages router example > For app router documentation, see the [Advanced Server Rendering guide](./advanced-ssr.md). Initial setup: ```tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( ) } ``` In each route: ```tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the , data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( ) } ``` ### Full Remix example Initial setup: ```tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( ) } ``` In each route, note that it's fine to do this in nested routes too: ```tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the , data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData() return ( ) } ``` ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: ```tsx export default function PostsRoute({ dehydratedState }) { return ( ) } ``` While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: ```tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } ``` With Remix, this is a little bit more involved, we recommend checking out the [use-dehydrated-state](https://github.com/maplegrove-io/use-dehydrated-state) package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to [prefetch dependent queries](./prefetching.md#dependent-queries-code-splitting), but how do we do this in framework loaders? Consider the following code, taken from the [Dependent Queries guide](./dependent-queries.md): ```tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) ``` How would we prefetch this so it can be server rendered? Here's an example: ```tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } ``` This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: - `queryClient.prefetchQuery(...)` never throws errors - `dehydrate(...)` only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use `queryClient.fetchQuery(...)` instead, which will throw errors when it fails, letting you handle things in a suitable way. ```tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here ``` If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option `shouldDehydrateQuery` to override the default function and implement your own logic: ```tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ``` ## Serialization When doing `return { props: { dehydratedState: dehydrate(queryClient) } }` in Next.js, or `return json({ dehydratedState: dehydrate(queryClient) })` in Remix, what happens is that the `dehydratedState` representation of the `queryClient` is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only support returning things that are safely serializable/parsable, and therefore do not support `undefined`, `Error`, `Date`, `Map`, `Set`, `BigInt`, `Infinity`, `NaN`, `-0`, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out [superjson](https://github.com/blitz-js/superjson) or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use `JSON.stringify(dehydratedState)`, but because this doesn't escape things like `` by default, this can easily lead to **XSS-vulnerabilities** in your application. [superjson](https://github.com/blitz-js/superjson) also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like [Serialize JavaScript](https://github.com/yahoo/serialize-javascript) or [devalue](https://github.com/Rich-Harris/devalue) which are both safe against XSS injections out of the box. ## A note about request waterfalls In the [Performance & Request Waterfalls guide](./request-waterfalls.md) we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the [specific code example](./request-waterfalls#code-splitting), but as a refresher, we have a code split `` component inside a `` component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: ``` 1. |> Markup (without content) 2. |> JS for 3. |> getFeed() 4. |> JS for 5. |> getGraphDataById() ``` The nice thing about server rendering is that we can turn the above into: ``` 1. |> Markup (with content AND initial data) 2. |> JS for 2. |> JS for ``` Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since `` was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: ``` 1. |> getFeed() 2. |> getGraphDataById() ``` We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the `/feed` page, and let's pretend we also have another page like `/posts`. If we type in `www.example.com/feed` directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in `www.example.com/posts` and then **click a link** to `/feed`, we're back to this: ``` 1. |> JS for 2. |> getFeed() 3. |> JS for 4. |> getGraphDataById() ``` This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: ``` 1. |> JS for 1. |> getFeed() + getGraphDataById() 2. |> JS for ``` This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the [Advanced Server Rendering guide](./advanced-ssr.md). ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was `dataUpdatedAt`. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because `staleTime` defaults to `0`, queries will be refetched in the background on page load by default. You might want to use a higher `staleTime` to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the `staleTime` of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the `QueryClient` for every request, React Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, `gcTime` defaults to `Infinity` which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity `gcTime` then you will be responsible for clearing the cache early. Avoid setting `gcTime` to `0` as it may result in a hydration error. This occurs because the [Hydration Boundary](../reference/hydration.md#hydrationboundary) places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter `gcTime`, we recommend setting it to `2 * 1000` to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to [`queryClient.clear()`](../../../reference/QueryClient.md#queryclientclear) after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller `gcTime`. ### Caveat for Next.js rewrites There's a catch if you're using [Next.js' rewrites feature](https://nextjs.org/docs/app/api-reference/next-config-js/rewrites) together with [Automatic Static Optimization](https://nextjs.org/docs/pages/building-your-application/rendering/automatic-static-optimization) or `getStaticProps`: It will cause a second hydration by React Query. That's because [Next.js needs to ensure that they parse the rewrites](https://nextjs.org/docs/app/api-reference/next-config-js/rewrites#rewrite-parameters) on the client and collect any params after hydration so that they can be provided in `router.query`. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of `useEffect`s/`useMemo`s. ================================================ FILE: docs/framework/react/guides/suspense.md ================================================ --- id: suspense title: Suspense --- React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: - [useSuspenseQuery](../reference/useSuspenseQuery.md) - [useSuspenseInfiniteQuery](../reference/useSuspenseInfiniteQuery.md) - [useSuspenseQueries](../reference/useSuspenseQueries.md) - Additionally, you can use the `useQuery().promise` and `React.use()` (Experimental) When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please read the [Resetting Error Boundaries](#resetting-error-boundaries) and look at the [Suspense Example](../examples/suspense) for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the `throwOnError` option to `true` as well. Enabling suspense mode for a query: ```tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) ``` This works nicely in TypeScript, because `data` is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. `placeholderData` also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into [startTransition](https://react.dev/reference/react/Suspense#preventing-unwanted-fallbacks). ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is `stale`. Thus, the default for `throwOnError` is: ``` throwOnError: (error, query) => typeof query.state.data === 'undefined' ``` Since you can't change `throwOnError` (because it would allow for `data` to become potentially `undefined`), you have to throw errors manually if you want all errors to be handled by Error Boundaries: ```tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ``` ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the `QueryErrorResetBoundary` component or with the `useQueryErrorResetBoundary` hook. When using the component it will reset any query errors within the boundaries of the component: ```tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( {({ reset }) => ( (
    There was an error!
    )} >
    )}
    ) ``` When using the hook it will reset any query errors within the closest `QueryErrorResetBoundary`. If there is no boundary defined it will reset them globally: ```tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( (
    There was an error!
    )} >
    ) } ``` ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing [Prefetching](./prefetching.md) on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using `NextJs`, you can use our **experimental** integration for Suspense on the Server: `@tanstack/react-query-next-experimental`. This package will allow you to fetch data on the server (in a client component) by just calling `useSuspenseQuery` in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the `ReactQueryStreamedHydration` component: ```tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( {props.children} ) } ``` For more information, check out the [NextJs Suspense Streaming Example](../examples/nextjs-suspense-streaming) and the [Advanced Rendering & Hydration](./advanced-ssr.md) guide. ## Using `useQuery().promise` and `React.use()` (Experimental) > To enable this feature, you need to set the `experimental_prefetchInRender` option to `true` when creating your `QueryClient` **Example code:** ```tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) ``` **Usage:** ```tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult }) { const data = React.use(query.promise) return (
      {data.map((todo) => (
    • {todo.title}
    • ))}
    ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <>

    Todos

    Loading...}> ) } ``` For a more complete example, see [suspense example on GitHub](https://github.com/TanStack/query/tree/main/examples/react/suspense). For a Next.js streaming example, see [nextjs-suspense-streaming example on GitHub](https://github.com/TanStack/query/tree/main/examples/react/nextjs-suspense-streaming). ================================================ FILE: docs/framework/react/guides/testing.md ================================================ --- id: testing title: Testing --- React Query works by means of hooks - either the ones we offer or custom ones that wrap around them. With React 17 or earlier, writing unit tests for these custom hooks can be done by means of the [React Hooks Testing Library](https://react-hooks-testing-library.com/) library. Install this by running: ```sh npm install @testing-library/react-hooks react-test-renderer --save-dev ``` (The `react-test-renderer` library is needed as a peer dependency of `@testing-library/react-hooks`, and needs to correspond to the version of React that you are using.) _Note_: when using React 18 or later, `renderHook` is available directly through the `@testing-library/react` package, and `@testing-library/react-hooks` is no longer required. ## Our First Test Once installed, a simple test can be written. Given the following custom hook: ```tsx export function useCustomHook() { return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' }) } ``` We can write a test for this as follows: ```tsx import { renderHook, waitFor } from '@testing-library/react' const queryClient = new QueryClient() const wrapper = ({ children }) => ( {children} ) const { result } = renderHook(() => useCustomHook(), { wrapper }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data).toEqual('Hello') ``` Note that we provide a custom wrapper that builds the `QueryClient` and `QueryClientProvider`. This helps to ensure that our test is completely isolated from any other tests. It is possible to write this wrapper only once, but if so we need to ensure that the `QueryClient` gets cleared before every test, and that tests don't run in parallel otherwise one test will influence the results of others. ## Turn off retries The library defaults to three retries with exponential backoff, which means that your tests are likely to timeout if you want to test an erroneous query. The easiest way to turn retries off is via the QueryClientProvider. Let's extend the above example: ```tsx const queryClient = new QueryClient({ defaultOptions: { queries: { // ✅ turns retries off retry: false, }, }, }) const wrapper = ({ children }) => ( {children} ) ``` This will set the defaults for all queries in the component tree to "no retries". It is important to know that this will only work if your actual useQuery has no explicit retries set. If you have a query that wants 5 retries, this will still take precedence, because defaults are only taken as a fallback. ## Set gcTime to Infinity with Jest If you use Jest, you can set the `gcTime` to `Infinity` to prevent "Jest did not exit one second after the test run completed" error message. This is the default behavior on the server, and is only necessary to set if you are explicitly setting a `gcTime`. ## Testing Network Calls The primary use for React Query is to cache network requests, so it's important that we can test our code is making the correct network requests in the first place. There are plenty of ways that these can be tested, but for this example we are going to use [nock](https://www.npmjs.com/package/nock). Given the following custom hook: ```tsx function useFetchData() { return useQuery({ queryKey: ['fetchData'], queryFn: () => request('/api/data'), }) } ``` We can write a test for this as follows: ```tsx const queryClient = new QueryClient() const wrapper = ({ children }) => ( {children} ) const expectation = nock('http://example.com').get('/api/data').reply(200, { answer: 42, }) const { result } = renderHook(() => useFetchData(), { wrapper }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data).toEqual({ answer: 42 }) ``` Here we are making use of `waitFor` and waiting until the query status indicates that the request has succeeded. This way we know that our hook has finished and should have the correct data. _Note_: when using React 18, the semantics of `waitFor` have changed as noted above. ## Testing Load More / Infinite Scroll First we need to mock our API response ```tsx function generateMockedResponse(page) { return { page: page, items: [...] } } ``` Then, our `nock` configuration needs to differentiate responses based on the page, and we'll be using `uri` to do this. `uri`'s value here will be something like `"/?page=1` or `/?page=2` ```tsx const expectation = nock('http://example.com') .persist() .query(true) .get('/api/data') .reply(200, (uri) => { const url = new URL(`http://example.com${uri}`) const { page } = Object.fromEntries(url.searchParams) return generateMockedResponse(page) }) ``` (Notice the `.persist()`, because we'll be calling from this endpoint multiple times) Now we can safely run our tests, the trick here is to await for the data assertion to pass: ```tsx const { result } = renderHook(() => useInfiniteQueryCustomHook(), { wrapper, }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data.pages).toStrictEqual(generateMockedResponse(1)) result.current.fetchNextPage() await waitFor(() => expect(result.current.data.pages).toStrictEqual([ ...generateMockedResponse(1), ...generateMockedResponse(2), ]), ) expectation.done() ``` _Note_: when using React 18, the semantics of `waitFor` have changed as noted above. ## Further reading For additional tips and an alternative setup using `mock-service-worker`, have a look at [this article by TkDodo on Testing React Query](https://tkdodo.eu/blog/testing-react-query). ================================================ FILE: docs/framework/react/guides/updates-from-mutation-responses.md ================================================ --- id: updates-from-mutation-responses title: Updates from Mutation Responses --- When dealing with mutations that **update** objects on the server, it's common for the new object to be automatically returned in the response of the mutation. Instead of refetching any queries for that item and wasting a network call for data we already have, we can take advantage of the object returned by the mutation function and update the existing query with the new data immediately using the [Query Client's `setQueryData`](../../../reference/QueryClient.md#queryclientsetquerydata) method: [//]: # 'Example' ```tsx const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) ``` [//]: # 'Example' You might want to tie the `onSuccess` logic into a reusable mutation, for that you can create a custom hook like this: [//]: # 'Example2' ```tsx const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } ``` [//]: # 'Example2' ## Immutability Updates via `setQueryData` must be performed in an _immutable_ way. **DO NOT** attempt to write directly to the cache by mutating data (that you retrieved from the cache) in place. It might work at first but can lead to subtle bugs along the way. [//]: # 'Example3' ```tsx queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) ``` [//]: # 'Example3' ================================================ FILE: docs/framework/react/guides/window-focus-refetching.md ================================================ --- id: window-focus-refetching title: Window Focus Refetching --- If a user leaves your application and returns and the query data is stale, **TanStack Query automatically requests fresh data for you in the background**. You can disable this globally or per-query using the `refetchOnWindowFocus` option: #### Disabling Globally [//]: # 'Example' ```tsx // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return ... } ``` [//]: # 'Example' #### Disabling Per-Query [//]: # 'Example2' ```tsx useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) ``` [//]: # 'Example2' ## Custom Window Focus Event In rare circumstances, you may want to manage your own window focus events that trigger TanStack Query to revalidate. To do this, TanStack Query provides a `focusManager.setEventListener` function that supplies you the callback that should be fired when the window is focused and allows you to set up your own events. When calling `focusManager.setEventListener`, the previously set handler is removed (which in most cases will be the default handler) and your new handler is used instead. For example, this is the default handler: [//]: # 'Example3' ```tsx focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) ``` [//]: # 'Example3' [//]: # 'ReactNative' ## Managing Focus in React Native Instead of event listeners on `window`, React Native provides focus information through the [`AppState` module](https://reactnative.dev/docs/appstate#app-states). You can use the `AppState` "change" event to trigger an update when the app state changes to "active": ```tsx import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) ``` [//]: # 'ReactNative' ## Managing focus state [//]: # 'Example4' ```tsx import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) ``` [//]: # 'Example4' ================================================ FILE: docs/framework/react/installation.md ================================================ --- id: installation title: Installation --- You can install React Query via [NPM](https://npmjs.com/), or a good ol' ` ``` > You can find instructions on how to use React without JSX [here](https://react.dev/reference/react/createElement#creating-an-element-without-jsx). [//]: # 'CDNExample' ### Requirements React Query is optimized for modern browsers. It is compatible with the following browsers config ``` Chrome >= 91 Firefox >= 90 Edge >= 91 Safari >= 15 iOS >= 15 Opera >= 77 ``` > Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from `node_modules` yourselves. ### Recommendations It is recommended to also use our [ESLint Plugin Query](../../eslint/eslint-plugin-query.md) to help you catch bugs and inconsistencies while you code. You can install it via: ```bash npm i -D @tanstack/eslint-plugin-query ``` or ```bash pnpm add -D @tanstack/eslint-plugin-query ``` or ```bash yarn add -D @tanstack/eslint-plugin-query ``` or ```bash bun add -D @tanstack/eslint-plugin-query ``` ================================================ FILE: docs/framework/react/overview.md ================================================ --- id: overview title: Overview --- TanStack Query (formerly known as React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze. ## Motivation Most core web frameworks **do not** come with an opinionated way of fetching or updating data in a holistic way. Because of this developers end up building either meta-frameworks which encapsulate strict opinions about data-fetching, or they invent their own ways of fetching data. This usually means cobbling together component-based state and side-effects, or using more general purpose state management libraries to store and provide asynchronous data throughout their apps. While most traditional state management libraries are great for working with client state, they are **not so great at working with async or server state**. This is because **server state is totally different**. For starters, server state: - Is persisted remotely in a location you may not control or own - Requires asynchronous APIs for fetching and updating - Implies shared ownership and can be changed by other people without your knowledge - Can potentially become "out of date" in your applications if you're not careful Once you grasp the nature of server state in your application, **even more challenges will arise** as you go, for example: - Caching... (possibly the hardest thing to do in programming) - Deduping multiple requests for the same data into a single request - Updating "out of date" data in the background - Knowing when data is "out of date" - Reflecting updates to data as quickly as possible - Performance optimizations like pagination and lazy loading data - Managing memory and garbage collection of server state - Memoizing query results with structural sharing If you're not overwhelmed by that list, then that must mean that you've probably solved all of your server state problems already and deserve an award. However, if you are like a vast majority of people, you either have yet to tackle all or most of these challenges and we're only scratching the surface! TanStack Query is hands down one of the _best_ libraries for managing server state. It works amazingly well **out-of-the-box, with zero-config, and can be customized** to your liking as your application grows. TanStack Query allows you to defeat and overcome the tricky challenges and hurdles of _server state_ and control your app data before it starts to control you. On a more technical note, TanStack Query will likely: - Help you remove **many** lines of complicated and misunderstood code from your application and replace with just a handful of lines of TanStack Query logic - Make your application more maintainable and easier to build new features without worrying about wiring up new server state data sources - Have a direct impact on your end-users by making your application feel faster and more responsive than ever before - Potentially help you save on bandwidth and increase memory performance [//]: # 'Example' ## Enough talk, show me some code already! In the example below, you can see TanStack Query in its most basic and simple form being used to fetch the GitHub stats for the TanStack Query GitHub project itself: [Open in StackBlitz](https://stackblitz.com/github/TanStack/query/tree/main/examples/react/simple) ```tsx import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query' const queryClient = new QueryClient() export default function App() { return ( ) } function Example() { const { isPending, error, data } = useQuery({ queryKey: ['repoData'], queryFn: () => fetch('https://api.github.com/repos/TanStack/query').then((res) => res.json(), ), }) if (isPending) return 'Loading...' if (error) return 'An error has occurred: ' + error.message return (

    {data.name}

    {data.description}

    👀 {data.subscribers_count}{' '} ✨ {data.stargazers_count}{' '} 🍴 {data.forks_count}
    ) } ``` [//]: # 'Example' [//]: # 'Materials' ## You talked me into it, so what now? - Consider taking the official [TanStack Query Course](https://query.gg?s=tanstack) (or buying it for your whole team!) - Learn TanStack Query at your own pace with our amazingly thorough [Walkthrough Guide](./installation.md) and [API Reference](./reference/useQuery.md) - See the Article [Why You Want React Query](https://tkdodo.eu/blog/why-you-want-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/react/plugins/broadcastQueryClient.md ================================================ --- id: broadcastQueryClient title: broadcastQueryClient (Experimental) --- > VERY IMPORTANT: This utility is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Use at your own risk. If you choose to rely on this in production in an experimental stage, please lock your version to a patch-level version to avoid unexpected breakages. `broadcastQueryClient` is a utility for broadcasting and syncing the state of your queryClient between browser tabs/windows with the same origin. ## Installation This utility comes as a separate package and is available under the `'@tanstack/query-broadcast-client-experimental'` import. ## Usage Import the `broadcastQueryClient` function, and pass it your `QueryClient` instance, and optionally, set a `broadcastChannel`. ```tsx import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' const queryClient = new QueryClient() broadcastQueryClient({ queryClient, broadcastChannel: 'my-app', }) ``` ## API ### `broadcastQueryClient` Pass this function a `QueryClient` instance and optionally, a `broadcastChannel`. ```tsx broadcastQueryClient({ queryClient, broadcastChannel }) ``` ### `Options` An object of options: ```tsx interface BroadcastQueryClientOptions { /** The QueryClient to sync */ queryClient: QueryClient /** This is the unique channel name that will be used * to communicate between tabs and windows */ broadcastChannel?: string /** Options for the BroadcastChannel API */ options?: BroadcastChannelOptions } ``` The default options are: ```tsx { broadcastChannel = 'tanstack-query', } ``` ================================================ FILE: docs/framework/react/plugins/createAsyncStoragePersister.md ================================================ --- id: createAsyncStoragePersister title: createAsyncStoragePersister --- ## Installation This utility comes as a separate package and is available under the `'@tanstack/query-async-storage-persister'` import. ```bash npm install @tanstack/query-async-storage-persister @tanstack/react-query-persist-client ``` or ```bash pnpm add @tanstack/query-async-storage-persister @tanstack/react-query-persist-client ``` or ```bash yarn add @tanstack/query-async-storage-persister @tanstack/react-query-persist-client ``` or ```bash bun add @tanstack/query-async-storage-persister @tanstack/react-query-persist-client ``` ## Usage - Import the `createAsyncStoragePersister` function - Create a new asyncStoragePersister - you can pass any `storage` to it that adheres to the `AsyncStorage` interface - the example below uses the async-storage from React Native. - storages that read and write synchronously, like `window.localstorage`, also adhere to the `AsyncStorage` interface and can therefore also be used with `createAsyncStoragePersister`. - Wrap your app by using [`PersistQueryClientProvider`](./persistQueryClient.md#persistqueryclientprovider) component. ```tsx import AsyncStorage from '@react-native-async-storage/async-storage' import { QueryClient } from '@tanstack/react-query' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) const asyncStoragePersister = createAsyncStoragePersister({ storage: AsyncStorage, }) const Root = () => ( ) export default Root ``` ## Retries Retries work the same as for a [SyncStoragePersister](./createSyncStoragePersister.md), except that they can also be asynchronous. You can also use all the predefined retry handlers. ## API ### `createAsyncStoragePersister` Call this function to create an asyncStoragePersister that you can use later with `persistQueryClient`. ```tsx createAsyncStoragePersister(options: CreateAsyncStoragePersisterOptions) ``` ### `Options` ```tsx interface CreateAsyncStoragePersisterOptions { /** The storage client used for setting an retrieving items from cache */ storage: AsyncStorage | undefined | null /** The key to use when storing the cache to localStorage */ key?: string /** To avoid localStorage spamming, * pass a time in ms to throttle saving the cache to disk */ throttleTime?: number /** How to serialize the data to storage */ serialize?: (client: PersistedClient) => string /** How to deserialize the data from storage */ deserialize?: (cachedString: string) => PersistedClient /** How to retry persistence on error **/ retry?: AsyncPersistRetryer } interface AsyncStorage { getItem: (key: string) => MaybePromise setItem: (key: string, value: TStorageValue) => MaybePromise removeItem: (key: string) => MaybePromise entries?: () => MaybePromise> } ``` The default options are: ```tsx { key = `REACT_QUERY_OFFLINE_CACHE`, throttleTime = 1000, serialize = JSON.stringify, deserialize = JSON.parse, } ``` ================================================ FILE: docs/framework/react/plugins/createPersister.md ================================================ --- id: createPersister title: experimental_createQueryPersister --- ## Installation This utility comes as a separate package and is available under the `'@tanstack/query-persist-client-core'` import. ```bash npm install @tanstack/query-persist-client-core ``` or ```bash pnpm add @tanstack/query-persist-client-core ``` or ```bash yarn add @tanstack/query-persist-client-core ``` or ```bash bun add @tanstack/query-persist-client-core ``` > Note: This util is also included in the `@tanstack/react-query-persist-client` package, so you do not need to install it separately if you are using that package. ## Usage - Import the `experimental_createQueryPersister` function - Create a new `experimental_createQueryPersister` - you can pass any `storage` to it that adheres to the `AsyncStorage` interface - the example below uses the async-storage from React Native. - Pass that `persister` as an option to your Query. This can be done either by passing it to the `defaultOptions` of the `QueryClient` or to any `useQuery` hook instance. - If you pass this `persister` as `defaultOptions`, all queries will be persisted to the provided `storage`. You can additionally narrow this down by passing `filters`. In contrast to the `persistClient` plugin, this will not persist the whole query client as a single item, but each query separately. As a key, the query hash is used. - If you provide this `persister` to a single `useQuery` hook, only this Query will be persisted. - Note: `queryClient.setQueryData()` operations are not persisted, this means that if you perform an optimistic update and refresh the page before the query has been invalidated, your changes to the query data will be lost. See https://github.com/TanStack/query/issues/6310 This way, you do not need to store whole `QueryClient`, but choose what is worth to be persisted in your application. Each query is lazily restored (when the Query is first used) and persisted (after each run of the `queryFn`), so it does not need to be throttled. `staleTime` is also respected after restoring the Query, so if data is considered `stale`, it will be refetched immediately after restoring. If data is `fresh`, the `queryFn` will not run. Garbage collecting a Query from memory **does not** affect the persisted data. That means Queries can be kept in memory for a shorter period of time to be more **memory efficient**. If they are used the next time, they will just be restored from the persistent storage again. ```tsx import AsyncStorage from '@react-native-async-storage/async-storage' import { QueryClient } from '@tanstack/react-query' import { experimental_createQueryPersister } from '@tanstack/query-persist-client-core' const persister = experimental_createQueryPersister({ storage: AsyncStorage, maxAge: 1000 * 60 * 60 * 12, // 12 hours }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 30, // 30 seconds persister: persister.persisterFn, }, }, }) ``` ### Adapted defaults The `createPersister` plugin technically wraps the `queryFn`, so it doesn't restore if the `queryFn` doesn't run. In that way, it acts as a caching layer between the Query and the network. Thus, the `networkMode` defaults to `'offlineFirst'` when a persister is used, so that restoring from the persistent storage can also happen even if there is no network connection. ## Additional utilities Invoking `experimental_createQueryPersister` returns additional utilities in addition to `persisterFn` for easier implementation of userland functionalities. ### `persistQueryByKey(queryKey: QueryKey, queryClient: QueryClient): Promise` This function will persist `Query` to storage and key defined when creating persister. This utility might be used along `setQueryData` to persist optimistic update to storage without waiting for invalidation. ```tsx const persister = experimental_createQueryPersister({ storage: AsyncStorage, maxAge: 1000 * 60 * 60 * 12, // 12 hours }) const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { ... // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // And persist it to storage persister.persistQueryByKey(['todos'], queryClient) ... }, }) ``` ### `retrieveQuery(queryHash: string): Promise` This function would attempt to retrieve persisted query by `queryHash`. If `query` is `expired`, `busted` or `malformed` it would be removed from the storage instead, and `undefined` would be returned. ### `persisterGc(): Promise` This function can be used to sporadically clean up stoage from `expired`, `busted` or `malformed` entries. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. ### `restoreQueries(queryClient: QueryClient, filters): Promise` This function can be used to restore queries that are currently stored by persister. For example when your app is starting up in offline mode, or you want all or only specific data from previous session to be immediately available without intermediate `loading` state. The filter object supports the following properties: - `queryKey?: QueryKey` - Set this property to define a query key to match on. - `exact?: boolean` - If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. ### `removeQueries(filters): Promise` When using `queryClient.removeQueries`, the data remains in the persister and needs to be removed separately. This function can be used to remove queries that are currently stored by persister. The filter object supports the following properties: - `queryKey?: QueryKey` - Set this property to define a query key to match on. - `exact?: boolean` - If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. ## API ### `experimental_createQueryPersister` ```tsx experimental_createQueryPersister(options: StoragePersisterOptions) ``` #### `Options` ```tsx export interface StoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. */ storage: AsyncStorage | Storage | undefined | null /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (persistedQuery: PersistedQuery) => string /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: string) => PersistedQuery /** * A unique string that can be used to forcefully invalidate existing caches, * if they do not share the same buster string */ buster?: string /** * The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be discarded * @default 24 hours */ maxAge?: number /** * Prefix to be used for storage key. * Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`. */ prefix?: string /** * If set to `true`, the query will refetch on successful query restoration if the data is stale. * If set to `false`, the query will not refetch on successful query restoration. * If set to `'always'`, the query will always refetch on successful query restoration. * Defaults to `true`. */ refetchOnRestore?: boolean | 'always' /** * Filters to narrow down which Queries should be persisted. */ filters?: QueryFilters } interface AsyncStorage { getItem: (key: string) => MaybePromise setItem: (key: string, value: TStorageValue) => MaybePromise removeItem: (key: string) => MaybePromise entries?: () => MaybePromise> } ``` The default options are: ```tsx { prefix = 'tanstack-query', maxAge = 1000 * 60 * 60 * 24, serialize = JSON.stringify, deserialize = JSON.parse, refetchOnRestore = true, } ``` ================================================ FILE: docs/framework/react/plugins/createSyncStoragePersister.md ================================================ --- id: createSyncStoragePersister title: createSyncStoragePersister --- ## Deprecated This plugin is deprecated and will be removed in the next major version. You can simply use ['@tanstack/query-async-storage-persister'](./createAsyncStoragePersister.md) instead. ## Installation This utility comes as a separate package and is available under the `'@tanstack/query-sync-storage-persister'` import. ```bash npm install @tanstack/query-sync-storage-persister @tanstack/react-query-persist-client ``` or ```bash pnpm add @tanstack/query-sync-storage-persister @tanstack/react-query-persist-client ``` or ```bash yarn add @tanstack/query-sync-storage-persister @tanstack/react-query-persist-client ``` or ```bash bun add @tanstack/query-sync-storage-persister @tanstack/react-query-persist-client ``` ## Usage - Import the `createSyncStoragePersister` function - Create a new syncStoragePersister - Pass it to the [`persistQueryClient`](./persistQueryClient.md) function ```tsx import { persistQueryClient } from '@tanstack/react-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage, }) // const sessionStoragePersister = createSyncStoragePersister({ storage: window.sessionStorage }) persistQueryClient({ queryClient, persister: localStoragePersister, }) ``` ## Retries Persistence can fail, e.g. if the size exceeds the available space on the storage. Errors can be handled gracefully by providing a `retry` function to the persister. The retry function receives the `persistedClient` it tried to save, as well as the `error` and the `errorCount` as input. It is expected to return a _new_ `PersistedClient`, with which it tries to persist again. If _undefined_ is returned, there will be no further attempt to persist. ```tsx export type PersistRetryer = (props: { persistedClient: PersistedClient error: Error errorCount: number }) => PersistedClient | undefined ``` ### Predefined strategies Per default, no retry will occur. You can use one of the predefined strategies to handle retries. They can be imported `from '@tanstack/react-query-persist-client'`: - `removeOldestQuery` - will return a new `PersistedClient` with the oldest query removed. ```tsx const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage, retry: removeOldestQuery, }) ``` ## API ### `createSyncStoragePersister` Call this function to create a syncStoragePersister that you can use later with `persistQueryClient`. ```tsx createSyncStoragePersister(options: CreateSyncStoragePersisterOptions) ``` ### `Options` ```tsx interface CreateSyncStoragePersisterOptions { /** The storage client used for setting an retrieving items from cache (window.localStorage or window.sessionStorage) */ storage: Storage | undefined | null /** The key to use when storing the cache */ key?: string /** To avoid spamming, * pass a time in ms to throttle saving the cache to disk */ throttleTime?: number /** How to serialize the data to storage */ serialize?: (client: PersistedClient) => string /** How to deserialize the data from storage */ deserialize?: (cachedString: string) => PersistedClient /** How to retry persistence on error **/ retry?: PersistRetryer } ``` The default options are: ```tsx { key = `REACT_QUERY_OFFLINE_CACHE`, throttleTime = 1000, serialize = JSON.stringify, deserialize = JSON.parse, } ``` #### `serialize` and `deserialize` options There is a limit to the amount of data which can be stored in `localStorage`. If you need to store more data in `localStorage`, you can override the `serialize` and `deserialize` functions to compress and decompress the data using a library like [lz-string](https://github.com/pieroxy/lz-string/). ```tsx import { QueryClient } from '@tanstack/react-query' import { persistQueryClient } from '@tanstack/react-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' import { compress, decompress } from 'lz-string' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity } }, }) persistQueryClient({ queryClient: queryClient, persister: createSyncStoragePersister({ storage: window.localStorage, serialize: (data) => compress(JSON.stringify(data)), deserialize: (data) => JSON.parse(decompress(data)), }), maxAge: Infinity, }) ``` ================================================ FILE: docs/framework/react/plugins/persistQueryClient.md ================================================ --- id: persistQueryClient title: persistQueryClient --- This is set of utilities for interacting with "persisters" which save your queryClient for later use. Different **persisters** can be used to store your client and cache to many different storage layers. ## Build Persisters - [createSyncStoragePersister](./createSyncStoragePersister.md) - [createAsyncStoragePersister](./createAsyncStoragePersister.md) - [create a custom persister](#persisters) ## How It Works **IMPORTANT** - for persist to work properly, you probably want to pass `QueryClient` a `gcTime` value to override the default during hydration (as shown above). If it is not set when creating the `QueryClient` instance, it will default to `300000` (5 minutes) for hydration, and the stored cache will be discarded after 5 minutes of inactivity. This is the default garbage collection behavior. It should be set as the same value or higher than persistQueryClient's `maxAge` option. E.g. if `maxAge` is 24 hours (the default) then `gcTime` should be 24 hours or higher. If lower than `maxAge`, garbage collection will kick in and discard the stored cache earlier than expected. You can also pass it `Infinity` to disable garbage collection behavior entirely. Due to a JavaScript limitation, the maximum allowed `gcTime` is about [24 days](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value), although it is possible to work around this limit using [timeoutManager.setTimeoutProvider](../../../reference/timeoutManager.md#timeoutmanagersettimeoutprovider). ```tsx const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) ``` ### Cache Busting Sometimes you may make changes to your application or data that immediately invalidate any and all cached data. If and when this happens, you can pass a `buster` string option. If the cache that is found does not also have that buster string, it will be discarded. The following several functions accept this option: ```tsx persistQueryClient({ queryClient, persister, buster: buildHash }) persistQueryClientSave({ queryClient, persister, buster: buildHash }) persistQueryClientRestore({ queryClient, persister, buster: buildHash }) ``` ### Removal If data is found to be any of the following: 1. expired (see `maxAge`) 2. busted (see `buster`) 3. error (ex: `throws ...`) 4. empty (ex: `undefined`) the persister `removeClient()` is called and the cache is immediately discarded. ## API ### `persistQueryClientSave` - Your query/mutation are [`dehydrated`](../reference/hydration.md#dehydrate) and stored by the persister you provided. - `createSyncStoragePersister` and `createAsyncStoragePersister` throttle this action to happen at most every 1 second to save on potentially expensive writes. Review their documentation to see how to customize their throttle timing. You can use this to explicitly persist the cache at the moment(s) you choose. ```tsx persistQueryClientSave({ queryClient, persister, buster = '', dehydrateOptions = undefined, }) ``` ### `persistQueryClientSubscribe` Runs `persistQueryClientSave` whenever the cache changes for your `queryClient`. For example: you might initiate the `subscribe` when a user logs-in and checks "Remember me". - It returns an `unsubscribe` function which you can use to discontinue the monitor; ending the updates to the persisted cache. - If you want to erase the persisted cache after the `unsubscribe`, you can send a new `buster` to `persistQueryClientRestore` which will trigger the persister's `removeClient` function and discard the persisted cache. ```tsx persistQueryClientSubscribe({ queryClient, persister, buster = '', dehydrateOptions = undefined, }) ``` ### `persistQueryClientRestore` - Attempts to [`hydrate`](../reference/hydration.md#hydrate) a previously persisted dehydrated query/mutation cache from the persister back into the query cache of the passed query client. - If a cache is found that is older than the `maxAge` (which by default is 24 hours), it will be discarded. This timing can be customized as you see fit. You can use this to restore the cache at moment(s) you choose. ```tsx persistQueryClientRestore({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, // 24 hours buster = '', hydrateOptions = undefined, }) ``` ### `persistQueryClient` Takes the following actions: 1. Immediately restores any persisted cache ([see `persistQueryClientRestore`](#persistqueryclientrestore)) 2. Subscribes to the query cache and returns the `unsubscribe` function ([see `persistQueryClientSubscribe`](#persistqueryclientsubscribe)). This functionality is preserved from version 3.x. ```tsx persistQueryClient({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, // 24 hours buster = '', hydrateOptions = undefined, dehydrateOptions = undefined, }) ``` ### `Options` All options available are as follows: ```tsx interface PersistQueryClientOptions { /** The QueryClient to persist */ queryClient: QueryClient /** The Persister interface for storing and restoring the cache * to/from a persisted location */ persister: Persister /** The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be **silently** discarded * (defaults to 24 hours) */ maxAge?: number /** A unique string that can be used to forcefully * invalidate existing caches if they do not share the same buster string */ buster?: string /** The options passed to the hydrate function * Not used on `persistQueryClientSave` or `persistQueryClientSubscribe` */ hydrateOptions?: HydrateOptions /** The options passed to the dehydrate function * Not used on `persistQueryClientRestore` */ dehydrateOptions?: DehydrateOptions } ``` There are actually three interfaces available: - `PersistedQueryClientSaveOptions` is used for `persistQueryClientSave` and `persistQueryClientSubscribe` (doesn't use `hydrateOptions`). - `PersistedQueryClientRestoreOptions` is used for `persistQueryClientRestore` (doesn't use `dehydrateOptions`). - `PersistQueryClientOptions` is used for `persistQueryClient` ## Usage with React [persistQueryClient](#persistQueryClient) will try to restore the cache and automatically subscribes to further changes, thus syncing your client to the provided storage. However, restoring is asynchronous, because all persisters are async by nature, which means that if you render your App while you are restoring, you might get into race conditions if a query mounts and fetches at the same time. Further, if you subscribe to changes outside of the React component lifecycle, you have no way of unsubscribing: ```tsx // 🚨 never unsubscribes from syncing persistQueryClient({ queryClient, persister: localStoragePersister, }) // 🚨 happens at the same time as restoring ReactDOM.createRoot(rootElement).render() ``` ### PersistQueryClientProvider For this use-case, you can use the `PersistQueryClientProvider`. It will make sure to subscribe / unsubscribe correctly according to the React component lifecycle, and it will also make sure that queries will not start fetching while we are still restoring. Queries will still render though, they will just be put into `fetchingState: 'idle'` until data has been restored. Then, they will refetch unless the restored data is _fresh_ enough, and _initialData_ will also be respected. It can be used _instead of_ the normal [QueryClientProvider](../reference/QueryClientProvider.md): ```tsx import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) const persister = createAsyncStoragePersister({ storage: window.localStorage, }) ReactDOM.createRoot(rootElement).render( , ) ``` #### Props `PersistQueryClientProvider` takes the same props as [QueryClientProvider](../reference/QueryClientProvider.md), and additionally: - `persistOptions: PersistQueryClientOptions` - all [options](#options) you can pass to [persistQueryClient](#persistqueryclient) minus the QueryClient itself - `onSuccess?: () => Promise | unknown` - optional - will be called when the initial restore is finished - can be used to [resumePausedMutations](../../../reference/QueryClient.md#queryclientresumepausedmutations) - if a Promise is returned, it will be awaited; restoring is seen as ongoing until then - `onError?: () => Promise | unknown` - optional - will be called when an error is thrown during restoration - if a Promise is returned, it will be awaited ### useIsRestoring If you are using the `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. ## Persisters ### Persisters Interface Persisters have the following interfaces: ```tsx export interface Persister { persistClient(persistClient: PersistedClient): Promisable restoreClient(): Promisable removeClient(): Promisable } ``` Persisted Client entries have the following interface: ```tsx export interface PersistedClient { timestamp: number buster: string clientState: DehydratedState } ``` You can import these (to build a persister): ```tsx import { PersistedClient, Persister, } from '@tanstack/react-query-persist-client' ``` ### Building A Persister You can persist however you like. Here is an example of how to build an [Indexed DB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) persister. Compared to `Web Storage API`, Indexed DB is faster, stores more than 5MB, and doesn't require serialization. That means it can readily store Javascript native types, such as `Date` and `File`. ```tsx import { get, set, del } from 'idb-keyval' import { PersistedClient, Persister, } from '@tanstack/react-query-persist-client' /** * Creates an Indexed DB persister * @see https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API */ export function createIDBPersister(idbValidKey: IDBValidKey = 'reactQuery') { return { persistClient: async (client: PersistedClient) => { await set(idbValidKey, client) }, restoreClient: async () => { return await get(idbValidKey) }, removeClient: async () => { await del(idbValidKey) }, } satisfies Persister } ``` ================================================ FILE: docs/framework/react/quick-start.md ================================================ --- id: quick-start title: Quick Start --- This code snippet very briefly illustrates the 3 core concepts of React Query: - [Queries](./guides/queries.md) - [Mutations](./guides/mutations.md) - [Query Invalidation](./guides/query-invalidation.md) [//]: # 'Example' If you're looking for a fully functioning example, please have a look at our [simple StackBlitz example](./examples/simple) ```tsx import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { getTodos, postTodo } from '../my-api' // Create a client const queryClient = new QueryClient() function App() { return ( // Provide the client to your App ) } function Todos() { // Access the client const queryClient = useQueryClient() // Queries const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }) // Mutations const mutation = useMutation({ mutationFn: postTodo, onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) return (
      {query.data?.map((todo) => (
    • {todo.title}
    • ))}
    ) } render(, document.getElementById('root')) ``` [//]: # 'Example' These three concepts make up most of the core functionality of React Query. The next sections of the documentation will go over each of these core concepts in great detail. ================================================ FILE: docs/framework/react/react-native.md ================================================ --- id: react-native title: React Native --- React Query is designed to work out of the box with React Native. ## DevTools Support There are several options available for React Native DevTools integration: 1. **Native macOS App**: A 3rd party app for debugging React Query in any js-based application: https://github.com/LovesWorking/rn-better-dev-tools 2. **Flipper Plugin**: A 3rd party plugin for Flipper users: https://github.com/bgaleotti/react-query-native-devtools 3. **Reactotron Plugin**: A 3rd party plugin for Reactotron users: https://github.com/hsndmr/reactotron-react-query ## Online status management React Query already supports auto refetch on reconnect in web browser. To add this behavior in React Native you have to use React Query `onlineManager` as in the example below: ```tsx import NetInfo from '@react-native-community/netinfo' import { onlineManager } from '@tanstack/react-query' onlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { setOnline(!!state.isConnected) }) }) ``` or ```tsx import { onlineManager } from '@tanstack/react-query' import * as Network from 'expo-network' onlineManager.setEventListener((setOnline) => { let initialised = false const eventSubscription = Network.addNetworkStateListener((state) => { initialised = true setOnline(!!state.isConnected) }) Network.getNetworkStateAsync() .then((state) => { if (!initialised) { setOnline(!!state.isConnected) } }) .catch(() => { // getNetworkStateAsync can reject on some platforms/SDK versions }) return eventSubscription.remove }) ``` ## Refetch on App focus Instead of event listeners on `window`, React Native provides focus information through the [`AppState` module](https://reactnative.dev/docs/appstate#app-states). You can use the `AppState` "change" event to trigger an update when the app state changes to "active": ```tsx import { useEffect } from 'react' import { AppState, Platform } from 'react-native' import type { AppStateStatus } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) ``` ## Refresh on Screen focus In some situations, you may want to refetch the query when a React Native Screen is focused again. This custom hook will refetch **all active stale queries** when the screen is focused again. ```tsx import React from 'react' import { useFocusEffect } from '@react-navigation/native' import { useQueryClient } from '@tanstack/react-query' export function useRefreshOnFocus() { const queryClient = useQueryClient() const firstTimeRef = React.useRef(true) useFocusEffect( React.useCallback(() => { if (firstTimeRef.current) { firstTimeRef.current = false return } // refetch all stale active queries queryClient.refetchQueries({ queryKey: ['posts'], stale: true, type: 'active', }) }, [queryClient]), ) } ``` In the above code, the first focus (when the screen is initially mounted) is skipped because `useFocusEffect` calls our callback on mount in addition to screen focus. ## Disable queries on out of focus screens If you don’t want certain queries to remain “live” while a screen is out of focus, you can use the subscribed prop on useQuery. This prop lets you control whether a query stays subscribed to updates. Combined with React Navigation’s useIsFocused, it allows you to seamlessly unsubscribe from queries when a screen isn’t in focus: Example usage: ```tsx import React from 'react' import { useIsFocused } from '@react-navigation/native' import { useQuery } from '@tanstack/react-query' import { Text } from 'react-native' function MyComponent() { const isFocused = useIsFocused() const { dataUpdatedAt } = useQuery({ queryKey: ['key'], queryFn: () => fetch(...), subscribed: isFocused, }) return DataUpdatedAt: {dataUpdatedAt} } ``` When subscribed is false, the query unsubscribes from updates and won’t trigger re-renders or fetch new data for that screen. Once it becomes true again (e.g., when the screen regains focus), the query re-subscribes and stays up to date. ================================================ FILE: docs/framework/react/reference/QueryClientProvider.md ================================================ --- id: QueryClientProvider title: QueryClientProvider --- Use the `QueryClientProvider` component to connect and provide a `QueryClient` to your application: ```tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() function App() { return ... } ``` **Options** - `client: QueryClient` - **Required** - the QueryClient instance to provide ================================================ FILE: docs/framework/react/reference/QueryErrorResetBoundary.md ================================================ --- id: QueryErrorResetBoundary title: QueryErrorResetBoundary --- When using **suspense** or **throwOnError** in your queries, you need a way to let queries know that you want to try again when re-rendering after some error occurred. With the `QueryErrorResetBoundary` component you can reset any query errors within the boundaries of the component. ```tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( {({ reset }) => ( (
    There was an error!
    )} >
    )}
    ) ``` ================================================ FILE: docs/framework/react/reference/hydration.md ================================================ --- id: hydration title: hydration --- ## `dehydrate` `dehydrate` creates a frozen representation of a `cache` that can later be hydrated with `HydrationBoundary` or `hydrate`. This is useful for passing prefetched queries from server to client or persisting queries to localStorage or other persistent locations. It only includes currently successful queries by default. ```tsx import { dehydrate } from '@tanstack/react-query' const dehydratedState = dehydrate(queryClient, { shouldDehydrateQuery, shouldDehydrateMutation, }) ``` **Options** - `client: QueryClient` - **Required** - The `queryClient` that should be dehydrated - `options: DehydrateOptions` - Optional - `shouldDehydrateMutation: (mutation: Mutation) => boolean` - Optional - Whether to dehydrate mutations. - The function is called for each mutation in the cache - Return `true` to include this mutation in dehydration, or `false` otherwise - Defaults to only including paused mutations - If you would like to extend the function while retaining the default behavior, import and execute `defaultShouldDehydrateMutation` as part of the return statement - `shouldDehydrateQuery: (query: Query) => boolean` - Optional - Whether to dehydrate queries. - The function is called for each query in the cache - Return `true` to include this query in dehydration, or `false` otherwise - Defaults to only including successful queries - If you would like to extend the function while retaining the default behavior, import and execute `defaultShouldDehydrateQuery` as part of the return statement - `serializeData?: (data: any) => any` A function to transform (serialize) data during dehydration. - `shouldRedactErrors?: (error: unknown) => boolean` - Optional - Whether to redact errors from the server during dehydration. - The function is called for each error in the cache - Return `true` to redact this error, or `false` otherwise - Defaults to redacting all errors **Returns** - `dehydratedState: DehydratedState` - This includes everything that is needed to hydrate the `queryClient` at a later point - You **should not** rely on the exact format of this response, it is not part of the public API and can change at any time - This result is not in serialized form, you need to do that yourself if desired ### Limitations Some storage systems (such as browser [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)) require values to be JSON serializable. If you need to dehydrate values that are not automatically serializable to JSON (like `Error` or `undefined`), you have to serialize them for yourself. Since only successful queries are included per default, to also include `Errors`, you have to provide `shouldDehydrateQuery`, e.g.: ```tsx // server const state = dehydrate(client, { shouldDehydrateQuery: () => true }) // to also include Errors const serializedState = mySerialize(state) // transform Error instances to objects // client const state = myDeserialize(serializedState) // transform objects back to Error instances hydrate(client, state) ``` ## `hydrate` `hydrate` adds a previously dehydrated state into a `cache`. ```tsx import { hydrate } from '@tanstack/react-query' hydrate(queryClient, dehydratedState, options) ``` **Options** - `client: QueryClient` - **Required** - The `queryClient` to hydrate the state into - `dehydratedState: DehydratedState` - **Required** - The state to hydrate into the client - `options: HydrateOptions` - Optional - `defaultOptions: DefaultOptions` - Optional - `mutations: MutationOptions` The default mutation options to use for the hydrated mutations. - `queries: QueryOptions` The default query options to use for the hydrated queries. - `deserializeData?: (data: any) => any` A function to transform (deserialize) data before it is put into the cache. - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. ### Limitations If the queries you're trying to hydrate already exist in the queryCache, `hydrate` will only overwrite them if the data is newer than the data present in the cache. Otherwise, it will **not** get applied. [//]: # 'HydrationBoundary' ## `HydrationBoundary` `HydrationBoundary` adds a previously dehydrated state into the `queryClient` that would be returned by `useQueryClient()`. If the client already contains data, the new queries will be intelligently merged based on update timestamp. ```tsx import { HydrationBoundary } from '@tanstack/react-query' function App() { return ... } ``` > Note: Only `queries` can be dehydrated with an `HydrationBoundary`. **Options** - `state: DehydratedState` - The state to hydrate - `options: HydrateOptions` - Optional - `defaultOptions: QueryOptions` - The default query options to use for the hydrated queries. - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. [//]: # 'HydrationBoundary' ================================================ FILE: docs/framework/react/reference/infiniteQueryOptions.md ================================================ --- id: infiniteQueryOptions title: infiniteQueryOptions --- ```tsx infiniteQueryOptions({ queryKey, ...options, }) ``` **Options** You can generally pass everything to `infiniteQueryOptions` that you can also pass to [`useInfiniteQuery`](./useInfiniteQuery.md). Some options will have no effect when then forwarded to a function like `queryClient.prefetchInfiniteQuery`, but TypeScript will still be fine with those excess properties. - `queryKey: QueryKey` - **Required** - The query key to generate options for. See [useInfiniteQuery](./useInfiniteQuery.md) for more information. ================================================ FILE: docs/framework/react/reference/mutationOptions.md ================================================ --- id: mutationOptions title: mutationOptions --- ```tsx mutationOptions({ mutationFn, ...options, }) ``` **Options** You can generally pass everything to `mutationOptions` that you can also pass to [`useMutation`](./useMutation.md). ================================================ FILE: docs/framework/react/reference/queryOptions.md ================================================ --- id: queryOptions title: queryOptions --- ```tsx queryOptions({ queryKey, ...options, }) ``` **Options** You can generally pass everything to `queryOptions` that you can also pass to [`useQuery`](./useQuery.md). Some options will have no effect when then forwarded to a function like `queryClient.prefetchQuery`, but TypeScript will still be fine with those excess properties. - `queryKey: QueryKey` - **Required** - The query key to generate options for. - `experimental_prefetchInRender?: boolean` - Optional - Defaults to `false` - When set to `true`, queries will be prefetched during render, which can be useful for certain optimization scenarios - Needs to be turned on for the experimental `useQuery().promise` functionality [//]: # 'Materials' ## Further reading To learn more about `QueryOptions`, have a look at [this article by TkDodo The Query Options API](https://tkdodo.eu/blog/the-query-options-api). [//]: # 'Materials' ================================================ FILE: docs/framework/react/reference/useInfiniteQuery.md ================================================ --- id: useInfiniteQuery title: useInfiniteQuery --- ```tsx const { fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, promise, ...result } = useInfiniteQuery({ queryKey, queryFn: ({ pageParam }) => fetchPage(pageParam), initialPageParam: 1, ...options, getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => lastPage.nextCursor, getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => firstPage.prevCursor, }) ``` **Options** The options for `useInfiniteQuery` are identical to the [`useQuery` hook](../reference/useQuery.md) with the addition of the following: - `queryFn: (context: QueryFunctionContext) => Promise` - **Required, but only if no default query function has been defined** [`defaultQueryFn`](../guides/default-query-function.md) - The function that the query will use to request data. - Receives a [QueryFunctionContext](../guides/query-functions.md#queryfunctioncontext) - Must return a promise that will either resolve data or throw an error. - `initialPageParam: TPageParam` - **Required** - The default page param to use when fetching the first page. - `getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null` - **Required** - When new data is received for this query, this function receives both the last page of the infinite list of data and the full array of all pages, as well as pageParam information. - It should return a **single variable** that will be passed as the last optional parameter to your query function. - Return `undefined` or `null` to indicate there is no next page available. - `getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => TPageParam | undefined | null` - When new data is received for this query, this function receives both the first page of the infinite list of data and the full array of all pages, as well as pageParam information. - It should return a **single variable** that will be passed as the last optional parameter to your query function. - Return `undefined` or `null`to indicate there is no previous page available. - `maxPages: number | undefined` - The maximum number of pages to store in the infinite query data. - When the maximum number of pages is reached, fetching a new page will result in the removal of either the first or last page from the pages array, depending on the specified direction. - If `undefined` or equals `0`, the number of pages is unlimited - Default value is `undefined` - `getNextPageParam` and `getPreviousPageParam` must be properly defined if `maxPages` value is greater than `0` to allow fetching a page in both directions when needed. **Returns** The returned properties for `useInfiniteQuery` are identical to the [`useQuery` hook](../reference/useQuery.md), with the addition of the following properties and a small difference in `isRefetching` and `isRefetchError`: - `data.pages: TData[]` - Array containing all pages. - `data.pageParams: unknown[]` - Array containing all page params. - `isFetchingNextPage: boolean` - Will be `true` while fetching the next page with `fetchNextPage`. - `isFetchingPreviousPage: boolean` - Will be `true` while fetching the previous page with `fetchPreviousPage`. - `fetchNextPage: (options?: FetchNextPageOptions) => Promise` - This function allows you to fetch the next "page" of results. - `options.cancelRefetch: boolean` if set to `true`, calling `fetchNextPage` repeatedly will invoke `queryFn` every time, whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored. If set to `false`, calling `fetchNextPage` repeatedly won't have any effect until the first invocation has resolved. Default is `true`. - `fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise` - This function allows you to fetch the previous "page" of results. - `options.cancelRefetch: boolean` same as for `fetchNextPage`. - `hasNextPage: boolean` - Will be `true` if there is a next page to be fetched (known via the `getNextPageParam` option). - `hasPreviousPage: boolean` - Will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option). - `isFetchNextPageError: boolean` - Will be `true` if the query failed while fetching the next page. - `isFetchPreviousPageError: boolean` - Will be `true` if the query failed while fetching the previous page. - `isRefetching: boolean` - Will be `true` whenever a background refetch is in-flight, which _does not_ include initial `pending` or fetching of next or previous page - Is the same as `isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage` - `isRefetchError: boolean` - Will be `true` if the query failed while refetching a page. - `promise: Promise` - A stable promise that resolves to the query result. - This can be used with `React.use()` to fetch data - Requires the `experimental_prefetchInRender` feature flag to be enabled on the `QueryClient`. Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default refetch behaviour, resulting in outdated data. Make sure to call these functions only in response to user actions, or add conditions like `hasNextPage && !isFetching`. ================================================ FILE: docs/framework/react/reference/useIsFetching.md ================================================ --- id: useIsFetching title: useIsFetching --- `useIsFetching` is an optional hook that returns the `number` of the queries that your application is loading or fetching in the background (useful for app-wide loading indicators). ```tsx import { useIsFetching } from '@tanstack/react-query' // How many queries are fetching? const isFetching = useIsFetching() // How many queries matching the posts prefix are fetching? const isFetchingPosts = useIsFetching({ queryKey: ['posts'] }) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../guides/filters.md#query-filters) - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. **Returns** - `isFetching: number` - Will be the `number` of the queries that your application is currently loading or fetching in the background. ================================================ FILE: docs/framework/react/reference/useIsMutating.md ================================================ --- id: useIsMutating title: useIsMutating --- `useIsMutating` is an optional hook that returns the `number` of mutations that your application is fetching (useful for app-wide loading indicators). ```tsx import { useIsMutating } from '@tanstack/react-query' // How many mutations are fetching? const isMutating = useIsMutating() // How many mutations matching the posts prefix are fetching? const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] }) ``` **Options** - `filters?: MutationFilters`: [Mutation Filters](../guides/filters.md#mutation-filters) - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. **Returns** - `isMutating: number` - Will be the `number` of the mutations that your application is currently fetching. ================================================ FILE: docs/framework/react/reference/useMutation.md ================================================ --- id: useMutation title: useMutation --- ```tsx const { data, error, isError, isIdle, isPending, isPaused, isSuccess, failureCount, failureReason, mutate, mutateAsync, reset, status, submittedAt, variables, } = useMutation( { mutationFn, gcTime, meta, mutationKey, networkMode, onError, onMutate, onSettled, onSuccess, retry, retryDelay, scope, throwOnError, }, queryClient, ) mutate(variables, { onError, onSettled, onSuccess, }) ``` **Parameter1 (Options)** - `mutationFn: (variables: TVariables, context: MutationFunctionContext) => Promise` - **Required, but only if no default mutation function has been defined** - A function that performs an asynchronous task and returns a promise. - `variables` is an object that `mutate` will pass to your `mutationFn` - `context` is an object that `mutate` will pass to your `mutationFn`. Contains reference to `QueryClient`, `mutationKey` and optional `meta` object. - `gcTime: number | Infinity` - The time in milliseconds that unused/inactive cache data remains in memory. When a mutation's cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different cache times are specified, the longest one will be used. - If set to `Infinity`, will disable garbage collection - Note: the maximum allowed time is about [24 days](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value), although it is possible to work around this limit using [timeoutManager.setTimeoutProvider](../../../reference/timeoutManager.md#timeoutmanagersettimeoutprovider). - `mutationKey: unknown[]` - Optional - A mutation key can be set to inherit defaults set with `queryClient.setMutationDefaults`. - `networkMode: 'online' | 'always' | 'offlineFirst'` - Optional - defaults to `'online'` - see [Network Mode](../guides/network-mode.md) for more information. - `onMutate: (variables: TVariables, context: MutationFunctionContext) => Promise | TOnMutateResult | void` - Optional - This function will fire before the mutation function is fired and is passed the same variables the mutation function would receive - Useful to perform optimistic updates to a resource in hopes that the mutation succeeds - The value returned from this function will be passed to both the `onError` and `onSettled` functions in the event of a mutation failure and can be useful for rolling back optimistic updates. - `onSuccess: (data: TData, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext) => Promise | unknown` - Optional - This function will fire when the mutation is successful and will be passed the mutation's result. - If a promise is returned, it will be awaited and resolved before proceeding - `onError: (err: TError, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext) => Promise | unknown` - Optional - This function will fire if the mutation encounters an error and will be passed the error. - If a promise is returned, it will be awaited and resolved before proceeding - `onSettled: (data: TData, error: TError, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext) => Promise | unknown` - Optional - This function will fire when the mutation is either successfully fetched or encounters an error and be passed either the data or error - If a promise is returned, it will be awaited and resolved before proceeding - `retry: boolean | number | (failureCount: number, error: TError) => boolean` - Defaults to `0`. - If `false`, failed mutations will not retry. - If `true`, failed mutations will retry infinitely. - If set to an `number`, e.g. `3`, failed mutations will retry until the failed mutations count meets that number. - `retryDelay: number | (retryAttempt: number, error: TError) => number` - This function receives a `retryAttempt` integer and the actual Error and returns the delay to apply before the next attempt in milliseconds. - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff. - A function like `attempt => attempt * 1000` applies linear backoff. - `scope: { id: string }` - Optional - Defaults to a unique id (so that all mutations run in parallel) - Mutations with the same scope id will run in serial - `throwOnError: undefined | boolean | (error: TError) => boolean` - Set this to `true` if you want mutation errors to be thrown in the render phase and propagate to the nearest error boundary - Set this to `false` to disable the behavior of throwing errors to the error boundary. - If set to a function, it will be passed the error and should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`) - `meta: Record` - Optional - If set, stores additional information on the mutation cache entry that can be used as needed. It will be accessible wherever the `mutation` is available (eg. `onError`, `onSuccess` functions of the `MutationCache`). **Parameter2 (QueryClient)** - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. **Returns** - `mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void` - The mutation function you can call with variables to trigger the mutation and optionally hooks on additional callback options. - `variables: TVariables` - Optional - The variables object to pass to the `mutationFn`. - `onSuccess: (data: TData, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext) => void` - Optional - This function will fire when the mutation is successful and will be passed the mutation's result. - Void function, the returned value will be ignored - `onError: (err: TError, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext) => void` - Optional - This function will fire if the mutation encounters an error and will be passed the error. - Void function, the returned value will be ignored - `onSettled: (data: TData | undefined, error: TError | null, variables: TVariables, onMutateResult: TOnMutateResult | undefined, context: MutationFunctionContext) => void` - Optional - This function will fire when the mutation is either successfully fetched or encounters an error and be passed either the data or error - Void function, the returned value will be ignored - If you make multiple requests, `onSuccess` will fire only after the latest call you've made. - `mutateAsync: (variables: TVariables, { onSuccess, onSettled, onError }) => Promise` - Similar to `mutate` but returns a promise which can be awaited. - `status: MutationStatus` - Will be: - `idle` initial status prior to the mutation function executing. - `pending` if the mutation is currently executing. - `error` if the last mutation attempt resulted in an error. - `success` if the last mutation attempt was successful. - `isIdle`, `isPending`, `isSuccess`, `isError`: boolean variables derived from `status` - `isPaused: boolean` - will be `true` if the mutation has been `paused` - see [Network Mode](../guides/network-mode.md) for more information. - `data: undefined | unknown` - Defaults to `undefined` - The last successfully resolved data for the mutation. - `error: null | TError` - The error object for the query, if an error was encountered. - `reset: () => void` - A function to clean the mutation internal state (i.e., it resets the mutation to its initial state). - `failureCount: number` - The failure count for the mutation. - Incremented every time the mutation fails. - Reset to `0` when the mutation succeeds. - `failureReason: null | TError` - The failure reason for the mutation retry. - Reset to `null` when the mutation succeeds. - `submittedAt: number` - The timestamp for when the mutation was submitted. - Defaults to `0`. - `variables: undefined | TVariables` - The `variables` object passed to the `mutationFn`. - Defaults to `undefined`. ================================================ FILE: docs/framework/react/reference/useMutationState.md ================================================ --- id: useMutationState title: useMutationState --- `useMutationState` is a hook that gives you access to all mutations in the `MutationCache`. You can pass `filters` to it to narrow down your mutations, and `select` to transform the mutation state. **Example 1: Get all variables of all running mutations** ```tsx import { useMutationState } from '@tanstack/react-query' const variables = useMutationState({ filters: { status: 'pending' }, select: (mutation) => mutation.state.variables, }) ``` **Example 2: Get all data for specific mutations via the `mutationKey`** ```tsx import { useMutation, useMutationState } from '@tanstack/react-query' const mutationKey = ['posts'] // Some mutation that we want to get the state for const mutation = useMutation({ mutationKey, mutationFn: (newPost) => { return axios.post('/posts', newPost) }, }) const data = useMutationState({ // this mutation key needs to match the mutation key of the given mutation (see above) filters: { mutationKey }, select: (mutation) => mutation.state.data, }) ``` **Example 3: Access the latest mutation data via the `mutationKey`**. Each invocation of `mutate` adds a new entry to the mutation cache for `gcTime` milliseconds. To access the latest invocation, you can check for the last item that `useMutationState` returns. ```tsx import { useMutation, useMutationState } from '@tanstack/react-query' const mutationKey = ['posts'] // Some mutation that we want to get the state for const mutation = useMutation({ mutationKey, mutationFn: (newPost) => { return axios.post('/posts', newPost) }, }) const data = useMutationState({ // this mutation key needs to match the mutation key of the given mutation (see above) filters: { mutationKey }, select: (mutation) => mutation.state.data, }) // Latest mutation data const latest = data[data.length - 1] ``` **Options** - `options` - `filters?: MutationFilters`: [Mutation Filters](../guides/filters.md#mutation-filters) - `select?: (mutation: Mutation) => TResult` - Use this to transform the mutation state. - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. **Returns** - `Array` - Will be an Array of whatever `select` returns for each matching mutation. ================================================ FILE: docs/framework/react/reference/usePrefetchInfiniteQuery.md ================================================ --- id: usePrefetchInfiniteQuery title: usePrefetchInfiniteQuery --- ```tsx usePrefetchInfiniteQuery(options) ``` **Options** You can pass everything to `usePrefetchInfiniteQuery` that you can pass to [`queryClient.prefetchInfiniteQuery`](../../../reference/QueryClient.md#queryclientprefetchinfinitequery). Remember that some of them are required as below: - `queryKey: QueryKey` - **Required** - The query key to prefetch during render - `queryFn: (context: QueryFunctionContext) => Promise` - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function.md) for more information. - `initialPageParam: TPageParam` - **Required** - The default page param to use when fetching the first page. - `getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null` - **Required** - When new data is received for this query, this function receives both the last page of the infinite list of data and the full array of all pages, as well as pageParam information. - It should return a **single variable** that will be passed as the last optional parameter to your query function. - Return `undefined` or `null` to indicate there is no next page available. - **Returns** The `usePrefetchInfiniteQuery` does not return anything, it should be used just to fire a prefetch during render, before a suspense boundary that wraps a component that uses [`useSuspenseInfiniteQuery`](./useSuspenseInfiniteQuery.md) ================================================ FILE: docs/framework/react/reference/usePrefetchQuery.md ================================================ --- id: usePrefetchQuery title: usePrefetchQuery --- ```tsx usePrefetchQuery(options) ``` **Options** You can pass everything to `usePrefetchQuery` that you can pass to [`queryClient.prefetchQuery`](../../../reference/QueryClient.md#queryclientprefetchquery). Remember that some of them are required as below: - `queryKey: QueryKey` - **Required** - The query key to prefetch during render - `queryFn: (context: QueryFunctionContext) => Promise` - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function.md) for more information. **Returns** The `usePrefetchQuery` does not return anything, it should be used just to fire a prefetch during render, before a suspense boundary that wraps a component that uses [`useSuspenseQuery`](./useSuspenseQuery.md). ================================================ FILE: docs/framework/react/reference/useQueries.md ================================================ --- id: useQueries title: useQueries --- The `useQueries` hook can be used to fetch a variable number of queries: ```tsx const ids = [1, 2, 3] const results = useQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), staleTime: Infinity, })), }) ``` **Options** The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`useQuery` hook](./useQuery.md) (excluding the `queryClient` option - because the `QueryClient` can be passed in on the top level). - `queryClient?: QueryClient` - Use this to provide a custom QueryClient. Otherwise, the one from the nearest context will be used. - `combine?: (result: UseQueriesResults) => TCombinedResult` - Use this to combine the results of the queries into a single value. > 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. **placeholderData** The `placeholderData` option exists for `useQueries` as well, but it doesn't get information passed from previously rendered Queries like `useQuery` does, because the input to `useQueries` can be a different number of Queries on each render. **Returns** The `useQueries` hook returns an array with all the query results. The order returned is the same as the input order. ## Combine If you want to combine `data` (or other Query information) from the results into a single value, you can use the `combine` option. The result will be structurally shared to be as referentially stable as possible. ```tsx const ids = [1, 2, 3] const combinedQueries = useQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), })), combine: (results) => { return { data: results.map((result) => result.data), pending: results.some((result) => result.isPending), } }, }) ``` In the above example, `combinedQueries` will be an object with a `data` and a `pending` property. Note that all other properties of the Query results will be lost. ### Memoization The `combine` function will only re-run if: - the `combine` function itself changed referentially - any of the query results changed This means that an inlined `combine` function, as shown above, will run on every render. To avoid this, you can wrap the `combine` function in `useCallback`, or extract it to a stable function reference if it doesn't have any dependencies. ================================================ FILE: docs/framework/react/reference/useQuery.md ================================================ --- id: useQuery title: useQuery --- ```tsx const { data, dataUpdatedAt, error, errorUpdatedAt, failureCount, failureReason, fetchStatus, isError, isFetched, isFetchedAfterMount, isFetching, isInitialLoading, isLoading, isLoadingError, isPaused, isPending, isPlaceholderData, isRefetchError, isRefetching, isStale, isSuccess, isEnabled, promise, refetch, status, } = useQuery( { queryKey, queryFn, gcTime, enabled, networkMode, initialData, initialDataUpdatedAt, meta, notifyOnChangeProps, placeholderData, queryKeyHashFn, refetchInterval, refetchIntervalInBackground, refetchOnMount, refetchOnReconnect, refetchOnWindowFocus, retry, retryOnMount, retryDelay, select, staleTime, structuralSharing, subscribed, throwOnError, }, queryClient, ) ``` **Parameter1 (Options)** - `queryKey: unknown[]` - **Required** - The query key to use for this query. - The query key will be hashed into a stable hash. See [Query Keys](../guides/query-keys.md) for more information. - The query will automatically update when this key changes (as long as `enabled` is not set to `false`). - `queryFn: (context: QueryFunctionContext) => Promise` - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function.md) for more information. - The function that the query will use to request data. - Receives a [QueryFunctionContext](../guides/query-functions.md#queryfunctioncontext) - Must return a promise that will either resolve data or throw an error. The data cannot be `undefined`. - `enabled: boolean | (query: Query) => boolean` - Set this to `false` to disable this query from automatically running. - Can be used for [Dependent Queries](../guides/dependent-queries.md). - `networkMode: 'online' | 'always' | 'offlineFirst'` - optional - defaults to `'online'` - see [Network Mode](../guides/network-mode.md) for more information. - `retry: boolean | number | (failureCount: number, error: TError) => boolean` - If `false`, failed queries will not retry by default. - If `true`, failed queries will retry infinitely. - If set to a `number`, e.g. `3`, failed queries will retry until the failed query count meets that number. - If set to a function, it will be called with `failureCount` (starting at `0` for the first retry) and `error` to determine if a retry should be attempted. - defaults to `3` on the client and `0` on the server - `retryOnMount: boolean` - If set to `false`, the query will not be retried on mount if it contains an error. Defaults to `true`. - `retryDelay: number | (retryAttempt: number, error: TError) => number` - This function receives a `retryAttempt` integer and the actual Error and returns the delay to apply before the next attempt in milliseconds. - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff. - A function like `attempt => attempt * 1000` applies linear backoff. - `staleTime: number | 'static' | ((query: Query) => number | 'static')` - Optional - Defaults to `0` - The time in milliseconds after which data is considered stale. This value only applies to the hook it is defined on. - If set to `Infinity`, the data will not be considered stale unless manually invalidated - If set to a function, the function will be executed with the query to compute a `staleTime`. - If set to `'static'`, the data will never be considered stale - `gcTime: number | Infinity` - Defaults to `5 * 60 * 1000` (5 minutes) or `Infinity` during SSR - The time in milliseconds that unused/inactive cache data remains in memory. When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different garbage collection times are specified, the longest one will be used. - Note: the maximum allowed time is about [24 days](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value), although it is possible to work around this limit using [timeoutManager.setTimeoutProvider](../../../reference/timeoutManager.md#timeoutmanagersettimeoutprovider). - If set to `Infinity`, will disable garbage collection - `queryKeyHashFn: (queryKey: QueryKey) => string` - Optional - If specified, this function is used to hash the `queryKey` to a string. - `refetchInterval: number | false | ((query: Query) => number | false | undefined)` - Optional - If set to a number, all queries will continuously refetch at this frequency in milliseconds - If set to a function, the function will be executed with the query to compute a frequency - `refetchIntervalInBackground: boolean` - Optional - If set to `true`, queries that are set to continuously refetch with a `refetchInterval` will continue to refetch while their tab/window is in the background - `refetchOnMount: boolean | "always" | ((query: Query) => boolean | "always")` - Optional - Defaults to `true` - If set to `true`, the query will refetch on mount if the data is stale. - If set to `false`, the query will not refetch on mount. - If set to `"always"`, the query will always refetch on mount (except when `staleTime: 'static'` is used). - If set to a function, the function will be executed with the query to compute the value - `refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")` - Optional - Defaults to `true` - If set to `true`, the query will refetch on window focus if the data is stale. - If set to `false`, the query will not refetch on window focus. - If set to `"always"`, the query will always refetch on window focus (except when `staleTime: 'static'` is used). - If set to a function, the function will be executed with the query to compute the value - `refetchOnReconnect: boolean | "always" | ((query: Query) => boolean | "always")` - Optional - Defaults to `true` - If set to `true`, the query will refetch on reconnect if the data is stale. - If set to `false`, the query will not refetch on reconnect. - If set to `"always"`, the query will always refetch on reconnect (except when `staleTime: 'static'` is used). - If set to a function, the function will be executed with the query to compute the value - `notifyOnChangeProps: string[] | "all" | (() => string[] | "all" | undefined)` - Optional - If set, the component will only re-render if any of the listed properties change. - If set to `['data', 'error']` for example, the component will only re-render when the `data` or `error` properties change. - If set to `"all"`, the component will opt-out of smart tracking and re-render whenever a query is updated. - If set to a function, the function will be executed to compute the list of properties. - By default, access to properties will be tracked, and the component will only re-render when one of the tracked properties change. - `select: (data: TData) => unknown` - Optional - This option can be used to transform or select a part of the data returned by the query function. It affects the returned `data` value, but does not affect what gets stored in the query cache. - The `select` function will only run if `data` changed, or if the reference to the `select` function itself changes. To optimize, wrap the function in `useCallback`. - `initialData: TData | () => TData` - Optional - If set, this value will be used as the initial data for the query cache (as long as the query hasn't been created or cached yet) - If set to a function, the function will be called **once** during the shared/root query initialization, and be expected to synchronously return the initialData - Initial data is considered stale by default unless a `staleTime` has been set. - `initialData` **is persisted** to the cache - `initialDataUpdatedAt: number | (() => number | undefined)` - Optional - If set, this value will be used as the time (in milliseconds) of when the `initialData` itself was last updated. - `placeholderData: TData | (previousValue: TData | undefined, previousQuery: Query | undefined) => TData` - Optional - If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `pending` state. - `placeholderData` is **not persisted** to the cache - If you provide a function for `placeholderData`, as a first argument you will receive previously watched query data if available, and the second argument will be the complete previousQuery instance. - `structuralSharing: boolean | (oldData: unknown | undefined, newData: unknown) => unknown` - Optional - Defaults to `true` - If set to `false`, structural sharing between query results will be disabled. - If set to a function, the old and new data values will be passed through this function, which should combine them into resolved data for the query. This way, you can retain references from the old data to improve performance even when that data contains non-serializable values. - `subscribed: boolean` - Optional - Defaults to `true` - If set to `false`, this instance of `useQuery` will not be subscribed to the cache. This means it won't trigger the `queryFn` on its own, and it won't receive updates if data gets into cache by other means. - `throwOnError: undefined | boolean | (error: TError, query: Query) => boolean` - Set this to `true` if you want errors to be thrown in the render phase and propagate to the nearest error boundary - Set this to `false` to disable `suspense`'s default behavior of throwing errors to the error boundary. - If set to a function, it will be passed the error and the query, and it should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`) - `meta: Record` - Optional - If set, stores additional information on the query cache entry that can be used as needed. It will be accessible wherever the `query` is available, and is also part of the `QueryFunctionContext` provided to the `queryFn`. **Parameter2 (QueryClient)** - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. **Returns** - `status: QueryStatus` - Will be: - `pending` if there's no cached data and no query attempt was finished yet. - `error` if the query attempt resulted in an error. The corresponding `error` property has the error received from the attempted fetch - `success` if the query has received a response with no errors and is ready to display its data. The corresponding `data` property on the query is the data received from the successful fetch or if the query's `enabled` property is set to `false` and has not been fetched yet `data` is the first `initialData` supplied to the query on initialization. - `isPending: boolean` - A derived boolean from the `status` variable above, provided for convenience. - `isSuccess: boolean` - A derived boolean from the `status` variable above, provided for convenience. - `isError: boolean` - A derived boolean from the `status` variable above, provided for convenience. - `isLoadingError: boolean` - Will be `true` if the query failed while fetching for the first time. - `isRefetchError: boolean` - Will be `true` if the query failed while refetching. - `data: TData` - Defaults to `undefined`. - The last successfully resolved data for the query. - `dataUpdatedAt: number` - The timestamp for when the query most recently returned the `status` as `"success"`. - `error: null | TError` - Defaults to `null` - The error object for the query, if an error was thrown. - `errorUpdatedAt: number` - The timestamp for when the query most recently returned the `status` as `"error"`. - `isStale: boolean` - Will be `true` if the data in the cache is invalidated or if the data is older than the given `staleTime`. - `isPlaceholderData: boolean` - Will be `true` if the data shown is the placeholder data. - `isFetched: boolean` - Will be `true` if the query has been fetched. - `isFetchedAfterMount: boolean` - Will be `true` if the query has been fetched after the component mounted. - This property can be used to not show any previously cached data. - `fetchStatus: FetchStatus` - `fetching`: Is `true` whenever the queryFn is executing, which includes initial `pending` as well as background refetches. - `paused`: The query wanted to fetch, but has been `paused`. - `idle`: The query is not fetching. - see [Network Mode](../guides/network-mode.md) for more information. - `isFetching: boolean` - A derived boolean from the `fetchStatus` variable above, provided for convenience. - `isPaused: boolean` - A derived boolean from the `fetchStatus` variable above, provided for convenience. - `isRefetching: boolean` - Is `true` whenever a background refetch is in-flight, which _does not_ include initial `pending` - Is the same as `isFetching && !isPending` - `isLoading: boolean` - Is `true` whenever the first fetch for a query is in-flight - Is the same as `isFetching && isPending` - `isInitialLoading: boolean` - **deprecated** - An alias for `isLoading`, will be removed in the next major version. - `isEnabled: boolean` - Is `true` if this query observer is enabled, `false` otherwise. - `failureCount: number` - The failure count for the query. - Incremented every time the query fails. - Reset to `0` when the query succeeds. - `failureReason: null | TError` - The failure reason for the query retry. - Reset to `null` when the query succeeds. - `errorUpdateCount: number` - The sum of all errors. - `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise` - A function to manually refetch the query. - If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option - `cancelRefetch?: boolean` - Defaults to `true` - Per default, a currently running request will be cancelled before a new request is made - When set to `false`, no refetch will be made if there is already a request running. - `promise: Promise` - A stable promise that will be resolved with the data of the query. - Requires the `experimental_prefetchInRender` feature flag to be enabled on the `QueryClient`. ================================================ FILE: docs/framework/react/reference/useQueryClient.md ================================================ --- id: useQueryClient title: useQueryClient --- The `useQueryClient` hook returns the current `QueryClient` instance. ```tsx import { useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient(queryClient?: QueryClient) ``` **Options** - `queryClient?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. ================================================ FILE: docs/framework/react/reference/useQueryErrorResetBoundary.md ================================================ --- id: useQueryErrorResetBoundary title: useQueryErrorResetBoundary --- This hook will reset any query errors within the closest `QueryErrorResetBoundary`. If there is no boundary defined it will reset them globally: ```tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( (
    There was an error!
    )} >
    ) } ``` ================================================ FILE: docs/framework/react/reference/useSuspenseInfiniteQuery.md ================================================ --- id: useSuspenseInfiniteQuery title: useSuspenseInfiniteQuery --- ```tsx const result = useSuspenseInfiniteQuery(options) ``` **Options** The same as for [useInfiniteQuery](./useInfiniteQuery.md), except for: - `suspense` - `throwOnError` - `enabled` - `placeholderData` **Returns** Same object as [useInfiniteQuery](./useInfiniteQuery.md), except that: - `data` is guaranteed to be defined - `isPlaceholderData` is missing - `status` is either `success` or `error` - the derived flags are set accordingly. **Caveat** [Cancellation](../guides/query-cancellation.md) does not work. ================================================ FILE: docs/framework/react/reference/useSuspenseQueries.md ================================================ --- id: useSuspenseQueries title: useSuspenseQueries --- ```tsx const result = useSuspenseQueries(options) ``` **Options** The same as for [useQueries](./useQueries.md), except that each `query` can't have: - `suspense` - `throwOnError` - `enabled` - `placeholderData` **Returns** Same structure as [useQueries](./useQueries.md), except that for each `query`: - `data` is guaranteed to be defined - `isPlaceholderData` is missing - `status` is either `success` or `error` - the derived flags are set accordingly. **Caveats** Keep in mind that the component will only re-mount after **all queries** have finished loading. Hence, if a query has gone stale in the time it took for all the queries to complete, it will be fetched again at re-mount. To avoid this, make sure to set a high enough `staleTime`. [Cancellation](../guides/query-cancellation.md) does not work. ================================================ FILE: docs/framework/react/reference/useSuspenseQuery.md ================================================ --- id: useSuspenseQuery title: useSuspenseQuery --- ```tsx const result = useSuspenseQuery(options) ``` **Options** The same as for [useQuery](./useQuery.md), except for: - `throwOnError` - `enabled` - `placeholderData` **Returns** Same object as [useQuery](./useQuery.md), except that: - `data` is guaranteed to be defined - `isPlaceholderData` is missing - `status` is either `success` or `error` - the derived flags are set accordingly. **Caveat** [Cancellation](../guides/query-cancellation.md) does not work. ================================================ FILE: docs/framework/react/typescript.md ================================================ --- id: typescript title: TypeScript --- React Query is now written in **TypeScript** to make sure the library and your projects are type-safe! Things to keep in mind: - TanStack Query follows [DefinitelyTyped's support window](https://github.com/DefinitelyTyped/DefinitelyTyped#support-window) and supports TypeScript versions released within the last 2 years. At the moment, that means TypeScript **5.4** and newer. - Changes to types in this repository are considered **non-breaking** and are usually released as **patch** semver changes (otherwise every type enhancement would be a major version!). - It is **highly recommended that you lock your react-query package version to a specific patch release and upgrade with the expectation that types may be fixed or upgraded between any release** - The non-type-related public API of React Query still follows semver very strictly. ## Type Inference Types in React Query generally flow through very well so that you don't have to provide type annotations for yourself [//]: # 'TypeInference1' ```tsx const { data } = useQuery({ // ^? const data: number | undefined queryKey: ['test'], queryFn: () => Promise.resolve(5), }) ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAORToCGAxjALQCOO+VAsAFC8MQAdqnhIAJnRh0icALwoM2XHgAUAbSqDkIAEa4qAXQA0cFQEo5APjgAFciGAYAdLVQQANgDd0KgKxmzXgB6ILgw8IA9AH5eIA) [//]: # 'TypeInference1' [//]: # 'TypeInference2' ```tsx const { data } = useQuery({ // ^? const data: string | undefined queryKey: ['test'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAORToCGAxjALQCOO+VAsAFC8MQAdqnhIAJnRh0icALwoM2XHgAUAbSox0IqgF0ANHBUBKOQD44ABXIhgGAHS1UEADYA3dCoCsxw0gwu6EwAXHASUuZhknT2MBAAyjBQwIIA5iaExrwA9Nlw+QUAegD8vEA) [//]: # 'TypeInference2' This works best if your `queryFn` has a well-defined returned type. Keep in mind that most data fetching libraries return `any` per default, so make sure to extract it to a properly typed function: [//]: # 'TypeInference3' ```tsx const fetchGroups = (): Promise => axios.get('/groups').then((response) => response.data) const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const data: Group[] | undefined ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAORToCGAxjALQCOO+VAsAFCiSw4dAB7AIqUuUpURY1Nx68YeMOjgBxcsjBwAvIjjAAJgC44AO2QgARriK9eDCOdTwS6GAwAWmiNon6ABQAlGYAClLAGAA8vtoA2gC6AHx6qbLiAHQA5h6BVAD02Vpg8sGZMF7o5oG0qJAuarqpdQ0YmUZ0MHTBDjxOLvBInd1EeigY2Lh4gfFUxX6lVIkANKQe3nGlvTwFBXAHhwB6APxwA65wI3RmW0lwAD4o5kboJMDm6Ea8QA) [//]: # 'TypeInference3' ## Type Narrowing React Query uses a [discriminated union type](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions) for the query result, discriminated by the `status` field and the derived status boolean flags. This will allow you to check for e.g. `success` status to make `data` defined: [//]: # 'TypeNarrowing' ```tsx const { data, isSuccess } = useQuery({ queryKey: ['test'], queryFn: () => Promise.resolve(5), }) if (isSuccess) { data // ^? const data: number } ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAORToCGAxjALQCOO+VAsAFC8MQAdqnhIAJnRh0ANHGCoAysgYN0qVETgBeFBmy48ACgDaVGGphUAurMMBKbQD44ABXIh56AHS1UEADYAbuiGAKx2dry8wCRwhvJKKmqoDgi8cBlwElK8APS5GQB6APy8hLxAA) [//]: # 'TypeNarrowing' ## Typing the error field The type for error defaults to `Error`, because that is what most users expect. [//]: # 'TypingError' ```tsx const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: Error ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPRTr2swBaAI458VALAAoUJFhx6AD2ARUpcpSqLlqCZKkw8YdHADi5ZGDgBeRHGAATAFxxGyEACNcRKVNYRm8CToMKwAFmYQFqo2ABQAlM4ACurAGAA8ERYA2gC6AHzWBVoqAHQA5sExVJxl5mA6cSUwoeiMMTyokMzGVgUdXRgl9vQMcT6SfgG2uORQRNYoGNi4eDFZVLWR9VQ5ADSkwWGZ9WOSnJxwl1cAegD8QA) [//]: # 'TypingError' If you want to throw a custom error, or something that isn't an `Error` at all, you can specify the type of the error field: [//]: # 'TypingError2' ```tsx const { error } = useQuery(['groups'], fetchGroups) // ^? const error: string | null ``` [//]: # 'TypingError2' However, this has the drawback that type inference for all other generics of `useQuery` will not work anymore. It is generally not considered a good practice to throw something that isn't an `Error`, so if you have a subclass like `AxiosError` you can use _type narrowing_ to make the error field more specific: [//]: # 'TypingError3' ```tsx import axios from 'axios' const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: Error | null if (axios.isAxiosError(error)) { error // ^? const error: AxiosError } ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPRTr2swBaAI458VALAAoUJFhx6AD2ARUpcpSqLlqCZKkw8YdHADi5ZGDgBeRHGAATAFxxGyEACNcRKVNYRm8CToMKwAFmYQFqo2ABQAlM4ACurAGAA8ERYA2gC6AHzWBVoqAHQA5sExVJxl5mA6cSUwoeiMMTyokMzGVgUdXRgl9vQMcT6SfgG2uORQRNYoGNi4eDFIIisA0uh4zllUtZH1VDkANHAb+ABijM5BIeF1qoRjkpyccJ9fAHoA-OPAEhwGLFVAlVIAQSUKgAolBZjEZtA4nFEFJPkioOi4O84H8pIQgA) [//]: # 'TypingError3' ### Registering a global Error TanStack Query v5 allows for a way to set a global Error type for everything, without having to specify generics on call-sides, by amending the `Register` interface. This will make sure inference still works, but the error field will be of the specified type. If you want to enforce that call-sides must do explicit type-narrowing, set `defaultError` to `unknown`: [//]: # 'RegisterErrorType' ```tsx import '@tanstack/react-query' declare module '@tanstack/react-query' { interface Register { // Use unknown so call sites must narrow explicitly. defaultError: unknown } } const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: unknown | null ``` [//]: # 'RegisterErrorType' [//]: # 'TypingMeta' ## Typing meta ### Registering global Meta Similarly to registering a [global error type](#registering-a-global-error) you can also register a global `Meta` type. This ensures the optional `meta` field on [queries](./reference/useQuery.md) and [mutations](./reference/useMutation.md) stays consistent and is type-safe. Note that the registered type must extend `Record` so that `meta` remains an object. ```ts import '@tanstack/react-query' interface MyMeta extends Record { // Your meta type definition. } declare module '@tanstack/react-query' { interface Register { queryMeta: MyMeta mutationMeta: MyMeta } } ``` [//]: # 'TypingMeta' [//]: # 'TypingQueryAndMutationKeys' ## Typing query and mutation keys ### Registering the query and mutation key types Also similarly to registering a [global error type](#registering-a-global-error), you can also register a global `QueryKey` and `MutationKey` type. This allows you to provide more structure to your keys, that matches your application's hierarchy, and have them be typed across all of the library's surface area. Note that the registered type must extend the `Array` type, so that your keys remain an array. ```ts import '@tanstack/react-query' type QueryKey = ['dashboard' | 'marketing', ...ReadonlyArray] declare module '@tanstack/react-query' { interface Register { queryKey: QueryKey mutationKey: QueryKey } } ``` [//]: # 'TypingQueryAndMutationKeys' [//]: # 'TypingQueryOptions' ## Typing Query Options If you inline query options into `useQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `useQuery` and e.g. `prefetchQuery`. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: ```ts import { queryOptions } from '@tanstack/react-query' function groupOptions() { return queryOptions({ queryKey: ['groups'], queryFn: fetchGroups, staleTime: 5 * 1000, }) } useQuery(groupOptions()) queryClient.prefetchQuery(groupOptions()) ``` Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: ```ts function groupOptions() { return queryOptions({ queryKey: ['groups'], queryFn: fetchGroups, staleTime: 5 * 1000, }) } const data = queryClient.getQueryData(groupOptions().queryKey) // ^? const data: Group[] | undefined ``` Without `queryOptions`, the type of `data` would be `unknown`, unless we'd pass a generic to it: ```ts const data = queryClient.getQueryData(['groups']) ``` Note that type inference via `queryOptions` does _not_ work for `queryClient.getQueriesData`, because it returns an array of tuples with heterogeneous, `unknown` data. If you are sure of the type of data that your query will return, specify it explicitly: ```ts const entries = queryClient.getQueriesData(groupOptions().queryKey) // ^? const entries: Array<[QueryKey, Group[] | undefined]> ``` ## Typing Mutation Options Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function: ```ts function groupMutationOptions() { return mutationOptions({ mutationKey: ['addGroup'], mutationFn: addGroup, }) } useMutation({ ...groupMutationOptions(), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['groups'] }), }) useIsMutating(groupMutationOptions()) queryClient.isMutating(groupMutationOptions()) ``` [//]: # 'TypingQueryOptions' ## Typesafe disabling of queries using `skipToken` If you are using TypeScript, you can use the `skipToken` to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. Read more about it in the [Disabling Queries](./guides/disabling-queries.md) guide. [//]: # 'Materials' ## Further Reading For tips and tricks around type inference, see the article [React Query and TypeScript](https://tkdodo.eu/blog/react-query-and-type-script). To find out how to get the best possible type-safety, you can read [Type-safe React Query](https://tkdodo.eu/blog/type-safe-react-query). [The Query Options API](https://tkdodo.eu/blog/the-query-options-api) outlines how type inference works with the `queryOptions` helper function. [//]: # 'Materials' ================================================ FILE: docs/framework/solid/devtools.md ================================================ --- id: devtools title: Devtools --- Wave your hands in the air and shout hooray because Solid Query comes with dedicated devtools! 🥳 When you begin your Solid Query journey, you'll want these devtools by your side. They help visualize all of the inner workings of Solid Query and will likely save you hours of debugging if you find yourself in a pinch! > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > > - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) > - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) > - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) ## Install and Import the Devtools The devtools are a separate package that you need to install: ```bash npm i @tanstack/solid-query-devtools ``` or ```bash pnpm add @tanstack/solid-query-devtools ``` or ```bash yarn add @tanstack/solid-query-devtools ``` or ```bash bun add @tanstack/solid-query-devtools ``` You can import the devtools like this: ```tsx import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' ``` By default, Solid Query Devtools are only included in bundles when `isServer === true` ([`isServer`](https://github.com/solidjs/solid/blob/a72d393a07b22f9b7496e5eb93712188ccce0d28/packages/solid/web/src/index.ts#L37) comes from the `solid-js/web` package), so you don't need to worry about excluding them during a production build. ## Floating Mode Floating Mode will mount the devtools as a fixed, floating element in your app and provide a toggle in the corner of the screen to show and hide the devtools. This toggle state will be stored and remembered in localStorage across reloads. Place the following code as high in your Solid app as you can. The closer it is to the root of the page, the better it will work! ```tsx import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' function App() { return ( {/* The rest of your application */} ) } ``` ### Options - `initialIsOpen: boolean` - Set this `true` if you want the dev tools to default to being open - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right"` - Defaults to `bottom-right` - The position of the Solid Query logo to open and close the devtools panel - `position?: "top" | "bottom" | "left" | "right"` - Defaults to `bottom` - The position of the Solid Query devtools panel - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ================================================ FILE: docs/framework/solid/guides/advanced-ssr.md ================================================ --- id: advanced-ssr title: Advanced Server Rendering --- Will come soon ================================================ FILE: docs/framework/solid/guides/background-fetching-indicators.md ================================================ --- id: background-fetching-indicators title: Background Fetching Indicators ref: docs/framework/react/guides/background-fetching-indicators.md replace: { 'hook': 'function' } --- [//]: # 'Example' ```tsx import { Switch, Match, Show, For } from 'solid-js' function Todos() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, })) return ( Loading... Error: {todosQuery.error.message}
    Refreshing...
    {(todo) => }
    ) } ``` [//]: # 'Example' [//]: # 'Example2' ```tsx import { useIsFetching } from '@tanstack/solid-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return (
    Queries are fetching in the background...
    ) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/solid/guides/caching.md ================================================ --- id: caching title: Caching Examples ref: docs/framework/react/guides/caching.md replace: { 'useQuery[(][{] queryKey': 'useQuery(() => ({ queryKey', 'fetchTodos [}][)]': 'fetchTodos }))', } --- ================================================ FILE: docs/framework/solid/guides/default-query-function.md ================================================ --- id: default-query-function title: Default Query Function ref: docs/framework/react/guides/default-query-function.md --- [//]: # 'Example' ```tsx // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( ) } // All you have to do now is pass a key! function Posts() { const postsQuery = useQuery(() => ({ queryKey: ['/posts'] })) // ... } // You can even leave out the queryFn and just go straight into options function Post(props) { const postQuery = useQuery(() => ({ queryKey: [`/posts/${props.postId}`], enabled: !!props.postId, })) // ... } ``` [//]: # 'Example' ================================================ FILE: docs/framework/solid/guides/dependent-queries.md ================================================ --- id: dependent-queries title: Dependent Queries ref: docs/framework/react/guides/dependent-queries.md --- [//]: # 'Example' ```tsx // Get the user const userQuery = useQuery(() => ({ queryKey: ['user', email], queryFn: getUserByEmail, })) const userId = () => userQuery.data?.id // Then get the user's projects const projectsQuery = useQuery(() => ({ queryKey: ['projects', userId()], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId(), })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx // Get the users ids const usersQuery = useQuery(() => ({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), })) // Then get the users messages const usersMessages = useQueries(() => ({ queries: usersQuery.data ? usersQuery.data.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if usersQuery.data is undefined, an empty array will be returned })) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/solid/guides/disabling-queries.md ================================================ --- id: disabling-queries title: Disabling/Pausing Queries ref: docs/framework/react/guides/disabling-queries.md --- [//]: # 'Example' ```tsx import { Switch, Match, Show, For } from 'solid-js' function Todos() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, })) return (
      {(todo) =>
    • {todo.title}
    • }
    Error: {todosQuery.error.message} Loading... Not ready ...
    {todosQuery.isFetching ? 'Fetching...' : null}
    ) } ``` [//]: # 'Example' [//]: # 'Example2' ```tsx import { createSignal } from 'solid-js' function Todos() { const [filter, setFilter] = createSignal('') const todosQuery = useQuery(() => ({ queryKey: ['todos', filter()], queryFn: () => fetchTodos(filter()), // ⬇️ disabled as long as the filter is empty enabled: !!filter(), })) return (
    {/* 🚀 applying the filter will enable and execute the query */}
    ) } ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx import { createSignal } from 'solid-js' import { skipToken, useQuery } from '@tanstack/solid-query' function Todos() { const [filter, setFilter] = createSignal() const todosQuery = useQuery(() => ({ queryKey: ['todos', filter()], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter() ? () => fetchTodos(filter()!) : skipToken, })) return (
    {/* 🚀 applying the filter will enable and execute the query */}
    ) } ``` [//]: # 'Example3' ================================================ FILE: docs/framework/solid/guides/does-this-replace-client-state.md ================================================ --- id: does-this-replace-client-state title: Does TanStack Query replace global state managers? ref: docs/framework/react/guides/does-this-replace-client-state.md replace: { 'hook': 'function' } --- ================================================ FILE: docs/framework/solid/guides/filters.md ================================================ --- id: filters title: Filters ref: docs/framework/react/guides/filters.md --- ================================================ FILE: docs/framework/solid/guides/important-defaults.md ================================================ --- id: important-defaults title: Important Defaults ref: docs/framework/react/guides/important-defaults.md --- [//]: # 'StructuralSharing' - Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. [//]: # 'StructuralSharing' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/solid/guides/infinite-queries.md ================================================ --- id: infinite-queries title: Infinite Queries ref: docs/framework/react/guides/infinite-queries.md --- [//]: # 'Example' ```tsx import { Switch, Match, For, Show } from 'solid-js' import { useInfiniteQuery } from '@tanstack/solid-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const projectsQuery = useInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, })) return (

    Loading...

    Error: {projectsQuery.error.message}

    {(group) => ( {(project) =>

    {project.name}

    }
    )}
    Fetching...
    ) } ``` [//]: # 'Example' [//]: # 'Example1' ```jsx projectsQuery.hasNextPage && !projectsQuery.isFetching && projectsQuery.fetchNextPage() } /> ``` [//]: # 'Example1' [//]: # 'Example3' ```tsx useInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx useInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), })) ``` [//]: # 'Example4' [//]: # 'Example8' ```tsx useInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, })) ``` [//]: # 'Example8' [//]: # 'Example9' ```tsx return useInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, })) ``` [//]: # 'Example9' ================================================ FILE: docs/framework/solid/guides/initial-query-data.md ================================================ --- id: initial-query-data title: Initial Query Data ref: docs/framework/react/guides/initial-query-data.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', } --- [//]: # 'Example' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx // Will show initialTodos immediately, but also immediately refetch todos after mount const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, })) ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 })) ``` [//]: # 'Example4' [//]: # 'Example5' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), })) ``` [//]: # 'Example5' [//]: # 'Example6' ```tsx const todoQuery = useQuery(() => ({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, })) ``` [//]: # 'Example6' [//]: # 'Example7' ```tsx const todoQuery = useQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, })) ``` [//]: # 'Example7' [//]: # 'Example8' ```tsx const todoQuery = useQuery(() => ({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, })) ``` [//]: # 'Example8' ================================================ FILE: docs/framework/solid/guides/invalidations-from-mutations.md ================================================ --- id: invalidations-from-mutations title: Invalidations from Mutations ref: docs/framework/react/guides/invalidations-from-mutations.md replace: { 'React': 'Solid', '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', 'hook': 'function', } --- ================================================ FILE: docs/framework/solid/guides/mutations.md ================================================ --- id: mutations title: Mutations ref: docs/framework/react/guides/mutations.md replace: { 'hook': 'function' } --- [//]: # 'Example' ```tsx import { Switch, Match, Show } from 'solid-js' function App() { const mutation = useMutation(() => ({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, })) return (
    Adding todo...
    An error occurred: {mutation.error.message}
    Todo added!
    ) } ``` [//]: # 'Example' [//]: # 'Info1' [//]: # 'Info1' [//]: # 'Example2' ```tsx const CreateTodo = () => { const mutation = useMutation(() => ({ mutationFn: (formData) => { return fetch('/api', formData) }, })) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return
    ...
    } ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx import { createSignal, Show } from 'solid-js' const CreateTodo = () => { const [title, setTitle] = createSignal('') const mutation = useMutation(() => ({ mutationFn: createTodo })) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title: title() }) } return (
    mutation.reset()}>{mutation.error}
    setTitle(e.currentTarget.value)} />
    ) } ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx useMutation(() => ({ mutationFn: addTodo, onMutate: (variables, context) => { // A mutation is about to happen! // Optionally return a result containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, onMutateResult, context) => { // An error happened! console.log(`rolling back optimistic update with id ${onMutateResult.id}`) }, onSuccess: (data, variables, onMutateResult, context) => { // Boom baby! }, onSettled: (data, error, variables, onMutateResult, context) => { // Error or success... doesn't matter! }, })) ``` [//]: # 'Example4' [//]: # 'Example5' ```tsx useMutation(() => ({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, })) ``` [//]: # 'Example5' [//]: # 'Example6' ```tsx useMutation(() => ({ mutationFn: addTodo, onSuccess: (data, variables, onMutateResult, context) => { // I will fire first }, onError: (error, variables, onMutateResult, context) => { // I will fire first }, onSettled: (data, error, variables, onMutateResult, context) => { // I will fire first }, })) mutate(todo, { onSuccess: (data, variables, onMutateResult, context) => { // I will fire second! }, onError: (error, variables, onMutateResult, context) => { // I will fire second! }, onSettled: (data, error, variables, onMutateResult, context) => { // I will fire second! }, }) ``` [//]: # 'Example6' [//]: # 'Example7' ```tsx useMutation(() => ({ mutationFn: addTodo, onSuccess: (data, variables, onMutateResult, context) => { // Will be called 3 times }, })) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, onMutateResult, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) ``` [//]: # 'Example7' [//]: # 'Example8' ```tsx const mutation = useMutation(() => ({ mutationFn: addTodo })) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } ``` [//]: # 'Example8' [//]: # 'Example9' ```tsx const mutation = useMutation(() => ({ mutationFn: addTodo, retry: 3, })) ``` [//]: # 'Example9' [//]: # 'Example10' ```tsx const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables, context) => { // Cancel current queries for the todos list await context.client.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return a result with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, onMutateResult, context) => { // Replace optimistic todo in the todos list with the result context.client.setQueryData(['todos'], (old) => old.map((todo) => todo.id === onMutateResult.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, onMutateResult, context) => { // Remove optimistic todo from the todos list context.client.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation(() => ({ mutationKey: ['addTodo'] })) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() ``` [//]: # 'Example10' [//]: # 'PersistOfflineIntro' ### Persisting Offline mutations If you persist offline mutations with the `persistQueryClient` plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. [//]: # 'PersistOfflineIntro' [//]: # 'OfflineExampleLink' [//]: # 'OfflineExampleLink' [//]: # 'ExampleScopes' ```tsx const mutation = useMutation(() => ({ mutationFn: addTodo, scope: { id: 'todo', }, })) ``` [//]: # 'ExampleScopes' [//]: # 'Materials' ## Further reading For more information about mutations, have a look at [TkDodo's article on Mastering Mutations in TanStack Query](https://tkdodo.eu/blog/mastering-mutations-in-react-query). [//]: # 'Materials' ================================================ FILE: docs/framework/solid/guides/network-mode.md ================================================ --- id: network-mode title: Network Mode ref: docs/framework/react/guides/network-mode.md --- ================================================ FILE: docs/framework/solid/guides/optimistic-updates.md ================================================ --- id: optimistic-updates title: Optimistic Updates ref: docs/framework/react/guides/optimistic-updates.md replace: { 'React Query': 'Solid Query', 'hook': 'function' } --- [//]: # 'ExampleUI1' ```tsx const addTodoMutation = useMutation(() => ({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), })) ``` [//]: # 'ExampleUI1' [//]: # 'ExampleUI2' ```tsx
      {(todo) =>
    • {todo.text}
    • }
    • {addTodoMutation.variables}
    ``` [//]: # 'ExampleUI2' [//]: # 'ExampleUI3' ```tsx
  • {addTodoMutation.variables}
  • ``` [//]: # 'ExampleUI3' [//]: # 'ExampleUI4' ```tsx // somewhere in your app const mutation = useMutation(() => ({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], })) // access variables somewhere else const variables = useMutationState(() => ({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, })) ``` [//]: # 'ExampleUI4' [//]: # 'Example' ```tsx const queryClient = useQueryClient() useMutation(() => ({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await context.client.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = context.client.getQueryData(['todos']) // Optimistically update to the new value context.client.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a result with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the result returned from onMutate to roll back onError: (err, newTodo, onMutateResult, context) => { context.client.setQueryData(['todos'], onMutateResult.previousTodos) }, // Always refetch after error or success: onSettled: (data, error, variables, onMutateResult, context) => context.client.invalidateQueries({ queryKey: ['todos'] }), })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx useMutation(() => ({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo, context) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = context.client.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value context.client.setQueryData(['todos', newTodo.id], newTodo) // Return a result with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the result we returned above onError: (err, newTodo, onMutateResult, context) => { context.client.setQueryData( ['todos', onMutateResult.newTodo.id], onMutateResult.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo, error, variables, onMutateResult, context) => context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }), })) ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx useMutation(() => ({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, onMutateResult, context) => { if (error) { // do something } }, })) ``` [//]: # 'Example3' ================================================ FILE: docs/framework/solid/guides/paginated-queries.md ================================================ --- id: paginated-queries title: Paginated / Lagged Queries ref: docs/framework/react/guides/paginated-queries.md replace: { 'hook': 'function' } --- [//]: # 'Example' ```tsx const projectsQuery = useQuery(() => ({ queryKey: ['projects', page()], queryFn: () => fetchProjects(page()), })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx import { createSignal, Switch, Match, Show, For } from 'solid-js' import { keepPreviousData, useQuery } from '@tanstack/solid-query' function Todos() { const [page, setPage] = createSignal(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const projectsQuery = useQuery(() => ({ queryKey: ['projects', page()], queryFn: () => fetchProjects(page()), placeholderData: keepPreviousData, })) return (
    Loading...
    Error: {projectsQuery.error.message}
    {(project) =>

    {project.name}

    }
    Current Page: {page() + 1} Loading...
    ) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/solid/guides/parallel-queries.md ================================================ --- id: parallel-queries title: Parallel Queries ref: docs/framework/react/guides/parallel-queries.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', 'hooks': 'functions', } --- [//]: # 'Example' ```tsx function App () { // The following queries will execute in parallel const usersQuery = useQuery(() => ({ queryKey: ['users'], queryFn: fetchUsers })) const teamsQuery = useQuery(() => ({ queryKey: ['teams'], queryFn: fetchTeams })) const projectsQuery = useQuery(() => ({ queryKey: ['projects'], queryFn: fetchProjects })) ... } ``` [//]: # 'Example' [//]: # 'Info' [//]: # 'Info' [//]: # 'DynamicParallelIntro' If the number of queries you need to execute changes, you cannot use manual querying since that would break reactivity. Instead, TanStack Query provides a `useQueries` function, which you can use to dynamically execute as many queries in parallel as you'd like. [//]: # 'DynamicParallelIntro' [//]: # 'DynamicParallelDescription' `useQueries` accepts an **accessor that returns an options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: [//]: # 'DynamicParallelDescription' [//]: # 'Example2' ```tsx function App(props) { const userQueries = useQueries(() => ({ queries: props.users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), })) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/solid/guides/placeholder-query-data.md ================================================ --- id: placeholder-query-data title: Placeholder Query Data ref: docs/framework/react/guides/placeholder-query-data.md --- [//]: # 'ExampleValue' ```tsx function Todos() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, })) } ``` [//]: # 'ExampleValue' [//]: # 'Memoization' ### Placeholder Data Memoization If the process for accessing a query's placeholder data is intensive or just not something you want to perform on every render, you can memoize the value: ```tsx function Todos() { const placeholderData = createMemo(() => generateFakeTodos()) const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderData(), })) } ``` [//]: # 'Memoization' [//]: # 'ExampleFunction' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, })) ``` [//]: # 'ExampleFunction' [//]: # 'ExampleCache' ```tsx function BlogPost(props) { const queryClient = useQueryClient() const blogPostQuery = useQuery(() => ({ queryKey: ['blogPost', props.blogPostId], queryFn: () => fetch(`/blogPosts/${props.blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === props.blogPostId) }, })) } ``` [//]: # 'ExampleCache' ================================================ FILE: docs/framework/solid/guides/prefetching.md ================================================ --- id: prefetching title: Prefetching & Router Integration ref: docs/framework/react/guides/prefetching.md --- [//]: # 'ExampleComponent' ```tsx import { Switch, Match } from 'solid-js' function Article(props) { const articleQuery = useQuery(() => ({ queryKey: ['article', props.id], queryFn: getArticleById, })) return ( Loading article... ) } function Comments(props) { const commentsQuery = useQuery(() => ({ queryKey: ['article-comments', props.id], queryFn: getArticleCommentsById, })) ... } ``` [//]: # 'ExampleComponent' [//]: # 'ExampleParentComponent' ```tsx import { Switch, Match } from 'solid-js' function Article(props) { const articleQuery = useQuery(() => ({ queryKey: ['article', props.id], queryFn: getArticleById, })) // Prefetch useQuery(() => ({ queryKey: ['article-comments', props.id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], })) return ( Loading article... ) } function Comments(props) { const commentsQuery = useQuery(() => ({ queryKey: ['article-comments', props.id], queryFn: getArticleCommentsById, })) ... } ``` [//]: # 'ExampleParentComponent' [//]: # 'Suspense' Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.prefetchQuery`: ```tsx const queryClient = useQueryClient() const articleQuery = useQuery(() => ({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, })) ``` Prefetching in an effect also works: ```tsx import { createEffect } from 'solid-js' const queryClient = useQueryClient() createEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }) ``` To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best: - Use `useQuery` and ignore the result - Prefetch inside the query function - Prefetch in an effect Let's look at a slightly more advanced case next. [//]: # 'Suspense' [//]: # 'ExampleConditionally1' ```tsx import { lazy, Switch, Match, For } from 'solid-js' // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = lazy(() => import('./GraphFeedItem')) function Feed() { const feedQuery = useQuery(() => ({ queryKey: ['feed'], queryFn: getFeed, })) return ( Loading feed... {(feedItem) => { if (feedItem.type === 'GRAPH') { return } return }} ) } // GraphFeedItem.tsx function GraphFeedItem(props) { const graphQuery = useQuery(() => ({ queryKey: ['graph', props.feedItem.id], queryFn: getGraphDataById, })) ... } ``` [//]: # 'ExampleConditionally1' [//]: # 'ExampleConditionally2' ```tsx function Feed() { const queryClient = useQueryClient() const feedQuery = useQuery(() => ({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } })) ... } ``` [//]: # 'ExampleConditionally2' [//]: # 'Router' ## Router Integration Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level. In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the [Server Rendering & Hydration guide](./ssr.md). For now, let's focus on the client side case and look at an example of how you can make this work with [TanStack Router](https://tanstack.com/router). These examples leave out a lot of setup and boilerplate to stay concise, you can check out a [full Solid Query example](https://tanstack.com/router/latest/docs/framework/solid/examples/basic-solid-query-file-based) over in the [TanStack Router docs](https://tanstack.com/router/latest/docs). When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. ```tsx const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(() => articleQueryOptions) const commentsQuery = useQuery(() => commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) ``` Integration with other routers is also possible. [//]: # 'Router' ================================================ FILE: docs/framework/solid/guides/queries.md ================================================ --- id: queries title: Queries ref: docs/framework/react/guides/queries.md --- [//]: # 'SubscribeDescription' To subscribe to a query in your components, call the `useQuery` function with at least: [//]: # 'SubscribeDescription' [//]: # 'Example' ```tsx import { useQuery } from '@tanstack/solid-query' function App() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) } ``` [//]: # 'Example' [//]: # 'Example2' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx import { Switch, Match, For } from 'solid-js' function Todos() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) return ( Loading... Error: {todosQuery.error.message}
      {(todo) =>
    • {todo.title}
    • }
    ) } ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx import { Switch, Match, For } from 'solid-js' function Todos() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList, })) return ( Loading... Error: {todosQuery.error.message}
      {(todo) =>
    • {todo.title}
    • }
    ) } ``` [//]: # 'Example4' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/solid/guides/query-cancellation.md ================================================ --- id: query-cancellation title: Query Cancellation ref: docs/framework/react/guides/query-cancellation.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', } --- [//]: # 'Example' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx import axios from 'axios' const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), })) ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx import axios from 'axios' const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, })) ``` [//]: # 'Example4' [//]: # 'Example5' ```tsx const client = new GraphQLClient(endpoint) const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, })) ``` [//]: # 'Example5' [//]: # 'Example6' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, })) ``` [//]: # 'Example6' [//]: # 'Example7' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, })) const queryClient = useQueryClient() return ( ) ``` [//]: # 'Example7' [//]: # 'Limitations' [//]: # 'Limitations' ================================================ FILE: docs/framework/solid/guides/query-functions.md ================================================ --- id: query-functions title: Query Functions ref: docs/framework/react/guides/query-functions.md --- [//]: # 'Example' ```tsx useQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx const todosQuery = useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, })) ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, })) ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx function Todos(props) { const todosQuery = useQuery(() => ({ queryKey: ['todos', { status: props.status, page: props.page }], queryFn: fetchTodoList, })) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ``` [//]: # 'Example4' ================================================ FILE: docs/framework/solid/guides/query-invalidation.md ================================================ --- id: query-invalidation title: Query Invalidation ref: docs/framework/react/guides/query-invalidation.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', 'hooks': 'functions', } --- ================================================ FILE: docs/framework/solid/guides/query-keys.md ================================================ --- id: query-keys title: Query Keys ref: docs/framework/react/guides/query-keys.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', 'React Query Keys': 'TanStack Query Keys', } --- [//]: # 'Example' ```tsx // A list of todos useQuery(() => ({ queryKey: ['todos'], ... })) // Something else, whatever! useQuery(() => ({ queryKey: ['something', 'special'], ... })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx // An individual todo useQuery(() => ({ queryKey: ['todo', 5], ... })) // An individual todo in a "preview" format useQuery(() => ({ queryKey: ['todo', 5, { preview: true }], ...})) // A list of todos that are "done" useQuery(() => ({ queryKey: ['todos', { type: 'done' }], ... })) ``` [//]: # 'Example2' [//]: # 'Example3' ```tsx useQuery(() => ({ queryKey: ['todos', { status, page }], ... })) useQuery(() => ({ queryKey: ['todos', { page, status }], ...})) useQuery(() => ({ queryKey: ['todos', { page, status, other: undefined }], ... })) ``` [//]: # 'Example3' [//]: # 'Example4' ```tsx useQuery(() => ({ queryKey: ['todos', status, page], ... })) useQuery(() => ({ queryKey: ['todos', page, status], ...})) useQuery(() => ({ queryKey: ['todos', undefined, page, status], ...})) ``` [//]: # 'Example4' [//]: # 'Example5' ```tsx function Todos(props) { const todosQuery = useQuery(() => ({ queryKey: ['todos', props.todoId], queryFn: () => fetchTodoById(props.todoId), })) } ``` [//]: # 'Example5' ================================================ FILE: docs/framework/solid/guides/query-options.md ================================================ --- id: query-options title: Query Options ref: docs/framework/react/guides/query-options.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', } --- [//]: # 'Example1' ```ts import { queryOptions } from '@tanstack/solid-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(() => groupOptions(1)) useQueries(() => ({ queries: [groupOptions(1), groupOptions(2)], })) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) ``` [//]: # 'Example1' [//]: # 'SelectDescription' You can still override some options at the component level. A very common and useful pattern is to create per-component `select` functions: [//]: # 'SelectDescription' [//]: # 'Example2' ```ts // Type inference still works, so query.data will be the return type of select instead of queryFn const groupQuery = useQuery(() => ({ ...groupOptions(1), select: (data) => data.groupName, })) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/solid/guides/query-retries.md ================================================ --- id: query-retries title: Query Retries ref: docs/framework/react/guides/query-retries.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', 'useQuery[(]': 'useQuery(() => ', 'useQueries[(]': 'useQueries(() => ', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', } --- ================================================ FILE: docs/framework/solid/guides/request-waterfalls.md ================================================ --- id: request-waterfalls title: Performance & Request Waterfalls ref: docs/framework/react/guides/request-waterfalls.md replace: { 'React Query': 'Solid Query' } --- [//]: # 'AdvancedSSRLink' [//]: # 'AdvancedSSRLink' [//]: # 'DependentExample' ```tsx // Get the user const userQuery = useQuery(() => ({ queryKey: ['user', email], queryFn: getUserByEmail, })) const userId = () => userQuery.data?.id // Then get the user's projects const projectsQuery = useQuery(() => ({ queryKey: ['projects', userId()], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId(), })) ``` [//]: # 'DependentExample' [//]: # 'ServerComponentsNote1' [//]: # 'ServerComponentsNote1' [//]: # 'SuspenseSerial' [//]: # 'SuspenseSerial' [//]: # 'NestedIntro' Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. [//]: # 'NestedIntro' [//]: # 'NestedExample' ```tsx import { Switch, Match } from 'solid-js' function Article(props) { const articleQuery = useQuery(() => ({ queryKey: ['article', props.id], queryFn: getArticleById, })) return ( Loading article... ) } function Comments(props) { const commentsQuery = useQuery(() => ({ queryKey: ['article-comments', props.id], queryFn: getArticleCommentsById, })) ... } ``` [//]: # 'NestedExample' [//]: # 'NestedHoistedExample' ```tsx import { Switch, Match, Show } from 'solid-js' function Article(props) { const articleQuery = useQuery(() => ({ queryKey: ['article', props.id], queryFn: getArticleById, })) const commentsQuery = useQuery(() => ({ queryKey: ['article-comments', props.id], queryFn: getArticleCommentsById, })) return ( Loading article... Loading comments... ) } ``` [//]: # 'NestedHoistedExample' [//]: # 'NestedHoistedOutro' The two queries will now fetch in parallel. [//]: # 'NestedHoistedOutro' [//]: # 'DependentNestedExample' ```tsx import { Switch, Match, For } from 'solid-js' function Feed() { const feedQuery = useQuery(() => ({ queryKey: ['feed'], queryFn: getFeed, })) return ( Loading feed... {(feedItem) => { if (feedItem.type === 'GRAPH') { return } return }} ) } function GraphFeedItem(props) { const graphQuery = useQuery(() => ({ queryKey: ['graph', props.feedItem.id], queryFn: getGraphDataById, })) ... } ``` [//]: # 'DependentNestedExample' [//]: # 'ServerComponentsNote2' In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the `getFeed` query. [//]: # 'ServerComponentsNote2' [//]: # 'LazyExample' ```tsx import { lazy, Switch, Match, For } from 'solid-js' // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = lazy(() => import('./GraphFeedItem')) function Feed() { const feedQuery = useQuery(() => ({ queryKey: ['feed'], queryFn: getFeed, })) return ( Loading feed... {(feedItem) => { if (feedItem.type === 'GRAPH') { return } return }} ) } // GraphFeedItem.tsx function GraphFeedItem(props) { const graphQuery = useQuery(() => ({ queryKey: ['graph', props.feedItem.id], queryFn: getGraphDataById, })) ... } ``` [//]: # 'LazyExample' [//]: # 'ServerComponentsNote3' [//]: # 'ServerComponentsNote3' ================================================ FILE: docs/framework/solid/guides/scroll-restoration.md ================================================ --- id: scroll-restoration title: Scroll Restoration ref: docs/framework/react/guides/scroll-restoration.md replace: { 'React Router’s ScrollRestoration, ': '' } --- ================================================ FILE: docs/framework/solid/guides/ssr.md ================================================ --- id: ssr title: SSR --- Will come soon ================================================ FILE: docs/framework/solid/guides/suspense.md ================================================ --- id: suspense title: Suspense --- Solid Query can also be used with Solid's [Suspense](https://docs.solidjs.com/reference/components/suspense) API's. To do that you need to wrap your suspendable component with `Suspense` component provided by Solid ```tsx import { Suspense } from 'solid-js' ;}> ``` You can use async `suspense` function that is provided by `solid-query`. ```tsx import { useQuery } from '@tanstack/solid-query' const todoFetcher = async () => await fetch('https://jsonplaceholder.cypress.io/todos').then((response) => response.json(), ) function SuspendableComponent() { const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: todoFetcher, })) // Accessing todosQuery.data directly inside a boundary // automatically triggers suspension until data is ready return
    Data: {JSON.stringify(todosQuery.data)}
    } ``` ## Fetch-on-render vs Render-as-you-fetch Out of the box, Solid Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing [Prefetching](./prefetching) on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ================================================ FILE: docs/framework/solid/guides/testing.md ================================================ --- id: testing title: Testing --- ================================================ FILE: docs/framework/solid/guides/updates-from-mutation-responses.md ================================================ --- id: updates-from-mutation-responses title: Updates from Mutation Responses ref: docs/framework/react/guides/updates-from-mutation-responses.md replace: { 'hook': 'function' } --- [//]: # 'Example' ```tsx const queryClient = useQueryClient() const mutation = useMutation(() => ({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, })) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const todoQuery = useQuery(() => ({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, })) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation(() => ({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, })) } ``` [//]: # 'Example2' ================================================ FILE: docs/framework/solid/guides/window-focus-refetching.md ================================================ --- id: window-focus-refetching title: Window Focus Refetching ref: docs/framework/react/guides/window-focus-refetching.md replace: { '@tanstack/react-query': '@tanstack/solid-query' } --- [//]: # 'Example2' ```tsx useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, })) ``` [//]: # 'Example2' [//]: # 'ReactNative' [//]: # 'ReactNative' ================================================ FILE: docs/framework/solid/installation.md ================================================ --- id: installation title: Installation --- You can install Solid Query via [NPM](https://npmjs.com/), or a good ol' ` ``` ### Requirements Solid Query is optimized for modern browsers. It is compatible with the following browsers config ``` Chrome >= 91 Firefox >= 90 Edge >= 91 Safari >= 15 iOS >= 15 Opera >= 77 ``` > Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from `node_modules` yourselves. ================================================ FILE: docs/framework/solid/overview.md ================================================ --- id: overview title: Overview --- Solid Query is the official SolidJS adapter of TanStack Query that makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze. ## Motivation SolidJS has been gaining popularity as a fast, reactive, and declarative library for building user interfaces. It comes packed with a lot of features out of the box. Primitives like `createSignal`, `createStore` are great for managing client state. And, unlike other UI libraries, SolidJS has strong opinions about managing asynchronous data. The `createResource` API is a great primitive for handling server state in SolidJS apps. A `resource` is a special kind of signal that can be used to trigger `Suspense` boundaries when the data is in a loading state. ```tsx import { createResource, ErrorBoundary, Suspense } from 'solid-js' import { render } from 'solid-js/web' function App() { const [repository] = createResource(async () => { const result = await fetch('https://api.github.com/repos/TanStack/query') if (!result.ok) throw new Error('Failed to fetch data') return result.json() }) return (
    Static Content
    {/* An error while fetching will be caught by the ErrorBoundary */} Something went wrong!
    }> {/* Suspense will trigger a loading state while the data is being fetched */} Loading...}>
    {repository()?.updated_at}
    ) } const root = document.getElementById('root') render(() => , root!) ``` This is amazing! In a few lines of code, you can fetch data from an API and handle loading and error states. But, as your application grows in complexity, you will need more features to manage server state effectively. This is because **server state is totally different from client state**. For starters, server state: - Is persisted remotely in a location you do not control or own - Requires asynchronous APIs for fetching and updating - Implies shared ownership and can be changed by other people without your knowledge - Can potentially become "out of date" in your applications if you're not careful Once you grasp the nature of server state in your application, **even more challenges will arise** as you go, for example: - Caching... (possibly the hardest thing to do in programming) - Deduping multiple requests for the same data into a single request - Updating "out of date" data in the background - Knowing when data is "out of date" - Reflecting updates to data as quickly as possible - Performance optimizations like pagination and lazy loading data - Managing memory and garbage collection of server state - Memoizing query results with structural sharing This is where **Solid Query** comes in. The library wraps around `createResource` and provides a set of hooks and utilities to manage server state effectively. It works amazingly well **out-of-the-box, with zero-config, and can be customized** to your liking as your application grows. On a more technical note, Solid Query will likely: - Help you remove **many** lines of complicated and misunderstood code from your application and replace with just a handful of lines of Solid Query logic. - Make your application more maintainable and easier to build new features without worrying about wiring up new server state data sources - Have a direct impact on your end-users by making your application feel faster and more responsive than ever before. - Potentially help you save on bandwidth and increase memory performance ## Enough talk, show me some code already! In the example below, you can see Solid Query in its most basic and simple form being used to fetch the GitHub stats for the TanStack Query GitHub project itself: ```tsx import { ErrorBoundary, Suspense } from 'solid-js' import { useQuery, QueryClient, QueryClientProvider, } from '@tanstack/solid-query' function App() { const repositoryQuery = useQuery(() => ({ queryKey: ['TanStack Query'], queryFn: async () => { const result = await fetch('https://api.github.com/repos/TanStack/query') if (!result.ok) throw new Error('Failed to fetch data') return result.json() }, staleTime: 1000 * 60 * 5, // 5 minutes throwOnError: true, // Throw an error if the query fails })) return (
    Static Content
    {/* An error while fetching will be caught by the ErrorBoundary */} Something went wrong!
    }> {/* Suspense will trigger a loading state while the data is being fetched */} Loading...}> {/* The `data` property on a query is a SolidJS resource so it will work with Suspense and transitions out of the box! */}
    {repositoryQuery.data?.updated_at}
    ) } const root = document.getElementById('root') const client = new QueryClient() render( () => ( ), root!, ) ``` ## Well, that seems like more lines of code to do the same thing? Yes it is! But, these few lines of code unlock a whole new world of possibilities. In the example above, your query is cached for 5 minutes, meaning that if a new component mounts anywhere in your app that uses the same query within 5 minutes, it will not re-fetch the data but instead use the cached data. This is just one of the many features that Solid Query provides out of the box. Some other features include: - **Automatic Refetching**: Queries automatically refetch in the background when they become "stale" (out of date according to the `staleTime` option) - **Automatic Caching**: Queries are cached by default and shared across your application - **Request Deduplication**: Multiple components can share the same query and make one request - **Automatic Garbage Collection**: Queries are garbage collected when they are no longer needed - **Window Focus Refetching**: Queries automatically refetch when the application comes back into focus - **Pagination**: Built-in support for pagination - **Request Cancellation**: Automatically cancels outdated or unwanted requests - **Polling/Realtime**: It's easy to add polling or realtime updates to your queries with a simple `refetchInterval` option - **SSR Support**: Solid Query works great with server-side rendering - **Optimistic Updates**: Easily update your cache with optimistic updates - **And much more...** ================================================ FILE: docs/framework/solid/plugins/broadcastQueryClient.md ================================================ --- id: broadcastQueryClient title: broadcastQueryClient (Experimental) ref: docs/framework/react/plugins/broadcastQueryClient.md --- ================================================ FILE: docs/framework/solid/plugins/createPersister.md ================================================ --- id: createPersister title: experimental_createPersister ref: docs/framework/react/plugins/createPersister.md replace: { 'react-query': 'solid-query' } --- ================================================ FILE: docs/framework/solid/quick-start.md ================================================ --- id: quick-start title: Quick Start --- The `@tanstack/solid-query` package provides a 1st-class API for using TanStack Query with SolidJS. ## Example ```tsx import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/solid-query' import { Switch, Match, For } from 'solid-js' const queryClient = new QueryClient() function Example() { const query = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, })) return (

    Loading...

    Error: {query.error.message}

    {(todo) =>

    {todo.title}

    }
    ) } function App() { return ( ) } ``` ## Important Differences between Solid Query & React Query Solid Query offers an API similar to React Query, but there are some key differences to be mindful of. - Arguments to `solid-query` primitives (like `useQuery`, `useMutation`, `useIsFetching`) are functions, so that they can be tracked in a reactive scope. ```tsx // ❌ react version useQuery({ queryKey: ['todos', todo], queryFn: fetchTodos, }) // ✅ solid version useQuery(() => ({ queryKey: ['todos', todo], queryFn: fetchTodos, })) ``` - Suspense works for queries out of the box if you access the query data inside a `` boundary. ```tsx import { For, Suspense } from 'solid-js' function Example() { const query = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, })) return (
    {/* ✅ Will trigger loading fallback, data accessed in a suspense boundary. */} {(todo) =>
    {todo.title}
    }
    {/* ❌ Will not trigger loading fallback, data not accessed in a suspense boundary. */} {(todo) =>
    {todo.title}
    }
    ) } ``` - Solid Query primitives do not support destructuring. The return value from these functions is a store, and their properties are only tracked in a reactive context. ```tsx import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/solid-query' import { Match, Switch } from 'solid-js' const queryClient = new QueryClient() export default function App() { return ( ) } function Example() { // ❌ react version -- supports destructing outside reactive context // const { isPending, error, data } = useQuery({ // queryKey: ['repoData'], // queryFn: () => // fetch('https://api.github.com/repos/tannerlinsley/react-query').then( // (res) => res.json() // ), // }) // ✅ solid version -- does not support destructuring outside reactive context const query = useQuery(() => ({ queryKey: ['repoData'], queryFn: () => fetch('https://api.github.com/repos/tannerlinsley/react-query').then( (res) => res.json(), ), })) // ✅ access query properties in JSX reactive context return ( Loading... Error: {query.error.message}

    {query.data.name}

    {query.data.description}

    👀 {query.data.subscribers_count}{' '} ✨ {query.data.stargazers_count}{' '} 🍴 {query.data.forks_count}
    ) } ``` - Signals and store values can be passed in directly to function arguments. Solid Query will update the query `store` automatically. ```tsx import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/solid-query' import { createSignal, For } from 'solid-js' const queryClient = new QueryClient() function Example() { const [enabled, setEnabled] = createSignal(false) const [todo, setTodo] = createSignal(0) // ✅ passing a signal directly is safe and observers update // automatically when the value of a signal changes const todosQuery = useQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos, enabled: enabled(), })) const todoDetailsQuery = useQuery(() => ({ queryKey: ['todo', todo()], queryFn: fetchTodo, enabled: todo() > 0, })) return (

    Loading...

    Error: {todosQuery.error.message}

    {(todo) => ( )}
    ) } function App() { return ( ) } ``` - Errors can be caught and reset using SolidJS' native `ErrorBoundary` component. Set `throwOnError` or the `suspense` option to `true` to make sure errors are thrown to the `ErrorBoundary` - Since Property tracking is handled through Solid's fine grained reactivity, options like `notifyOnChangeProps` are not needed ================================================ FILE: docs/framework/solid/reference/hydration.md ================================================ --- id: hydration title: hydration ref: docs/framework/react/reference/hydration.md replace: { '@tanstack/react-query': '@tanstack/solid-query' } --- [//]: # 'HydrationBoundary' [//]: # 'HydrationBoundary' ================================================ FILE: docs/framework/solid/reference/infiniteQueryOptions.md ================================================ --- id: infiniteQueryOptions title: infiniteQueryOptions ref: docs/framework/react/reference/infiniteQueryOptions.md --- ================================================ FILE: docs/framework/solid/reference/mutationOptions.md ================================================ --- id: mutationOptions title: mutationOptions ref: docs/framework/react/reference/mutationOptions.md --- ================================================ FILE: docs/framework/solid/reference/queryOptions.md ================================================ --- id: queryOptions title: queryOptions ref: docs/framework/react/reference/queryOptions.md --- ================================================ FILE: docs/framework/solid/reference/useInfiniteQuery.md ================================================ --- id: useInfiniteQuery title: useInfiniteQuery ref: docs/framework/react/reference/useInfiniteQuery.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useInfiniteQuery[(]': 'useInfiniteQuery(() => ', 'useMutation[(]': 'useMutation(() => ', } --- ================================================ FILE: docs/framework/solid/reference/useIsFetching.md ================================================ --- id: useIsFetching title: useIsFetching ref: docs/framework/react/reference/useIsFetching.md replace: { '@tanstack/react-query': '@tanstack/solid-query' } --- ================================================ FILE: docs/framework/solid/reference/useIsMutating.md ================================================ --- id: useIsMutating title: useIsMutating ref: docs/framework/react/reference/useIsMutating.md replace: { '@tanstack/react-query': '@tanstack/solid-query' } --- ================================================ FILE: docs/framework/solid/reference/useMutation.md ================================================ --- id: useMutation title: useMutation ref: docs/framework/react/reference/useMutation.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', } --- ================================================ FILE: docs/framework/solid/reference/useMutationState.md ================================================ --- id: useMutationState title: useMutationState ref: docs/framework/react/reference/useMutationState.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useMutationState[(]': 'useMutationState(() => ', 'useMutation[(]': 'useMutation(() => ', } --- ================================================ FILE: docs/framework/solid/reference/useQueries.md ================================================ --- id: useQueries title: useQueries ref: docs/framework/react/reference/useQueries.md replace: { '@tanstack/react-query': '@tanstack/solid-query', 'useQueries[(]': 'useQueries(() => ', } --- ================================================ FILE: docs/framework/solid/reference/useQuery.md ================================================ --- id: useQuery title: useQuery --- ```tsx const { data, dataUpdatedAt, error, errorUpdatedAt, failureCount, failureReason, fetchStatus, isError, isFetched, isFetchedAfterMount, isFetching, isInitialLoading, isLoading, isLoadingError, isPaused, isPending, isPlaceholderData, isRefetchError, isRefetching, isStale, isSuccess, refetch, status, } = useQuery( () => ({ queryKey, queryFn, enabled, select, placeholderData, deferStream, reconcile, gcTime, networkMode, initialData, initialDataUpdatedAt, meta, queryKeyHashFn, refetchInterval, refetchIntervalInBackground, refetchOnMount, refetchOnReconnect, refetchOnWindowFocus, retry, retryOnMount, retryDelay, staleTime, throwOnError, }), () => queryClient, ) ``` ## Usage example Here are some examples of how to use the `useQuery` primitive in Solid Query. ### Basic The most basic usage of `useQuery` is to create a query that fetches data from an API. ```tsx import { useQuery } from '@tanstack/solid-query' function App() { const todos = useQuery(() => ({ queryKey: 'todos', queryFn: async () => { const response = await fetch('/api/todos') if (!response.ok) { throw new Error('Failed to fetch todos') } return response.json() }, })) return (
    Error: {todos.error.message}
    Loading...
    Todos:
      {(todo) =>
    • {todo.title}
    • }
    ) } ``` ### Reactive Options The reason why `useQuery` accepts a function that returns an object is to allow for reactive options. This is useful when query options depend on other values/signals that might change over time. Solid Query can track the passed function in a reactive scope and re-run it whenever the dependencies change. ```tsx import { useQuery } from '@tanstack/solid-query' function App() { const [filter, setFilter] = createSignal('all') const todos = useQuery(() => ({ queryKey: ['todos', filter()], queryFn: async () => { const response = await fetch(`/api/todos?filter=${filter()}`) if (!response.ok) { throw new Error('Failed to fetch todos') } return response.json() }, })) return (
    Error: {todos.error.message}
    Loading...
    Todos:
      {(todo) =>
    • {todo.title}
    • }
    ) } ``` ### Usage with `Suspense` `useQuery` supports triggering SolidJS `Suspense` and `ErrorBoundary` components when the query is in a pending or error state. This allows you to easily handle loading and error states in your components. ```tsx import { useQuery } from '@tanstack/solid-query' function App() { const todos = useQuery(() => ({ queryKey: 'todos', queryFn: async () => { const response = await fetch('/api/todos') if (!response.ok) { throw new Error('Failed to fetch todos') } return response.json() }, throwOnError: true, })) return ( Error: {todos.error.message}}> Loading...}>
    Todos:
      {(todo) =>
    • {todo.title}
    • }
    ) } ``` ## `useQuery` Parameters - ### Query Options - `Accessor` - ##### `queryKey: unknown[]` - **Required** - The query key to use for this query. - The query key will be hashed into a stable hash. See [Query Keys](../guides/query-keys.md) for more information. - The query will automatically update when this key changes (as long as `enabled` is not set to `false`). - ##### `queryFn: (context: QueryFunctionContext) => Promise` - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function.md) for more information. - The function that the query will use to request data. - Receives a [QueryFunctionContext](../guides/query-functions.md#queryfunctioncontext) - Must return a promise that will either resolve data or throw an error. The data cannot be `undefined`. - ##### `enabled: boolean` - Set this to `false` to disable this query from automatically running. - Can be used for [Dependent Queries](../guides/dependent-queries.md) for more information. - ##### `select: (data: TData) => unknown` - Optional - This option can be used to transform or select a part of the data returned by the query function. It affects the returned `data` value, but does not affect what gets stored in the query cache. - The `select` function will only run if `data` changed, or if the reference to the `select` function itself changes. To optimize, wrap the function in `useCallback`. - ##### `placeholderData: TData | (previousValue: TData | undefined; previousQuery: Query | undefined,) => TData` - Optional - If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `pending` state. - `placeholderData` is **not persisted** to the cache - If you provide a function for `placeholderData`, as a first argument you will receive previously watched query data if available, and the second argument will be the complete previousQuery instance. - ##### `deferStream: boolean` - Optional - Defaults to `false` - 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. - ##### `reconcile: false | string | ((oldData: TData | undefined, newData: TData) => TData)` - Optional - Defaults to `false` - Set this to a string to enable reconciliation between query results based on the string key. - 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. - ##### `gcTime: number | Infinity` - Defaults to `5 * 60 * 1000` (5 minutes) or `Infinity` during SSR - The time in milliseconds that unused/inactive cache data remains in memory. When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different garbage collection times are specified, the longest one will be used. - Note: the maximum allowed time is about [24 days](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value), although it is possible to work around this limit using [timeoutManager.setTimeoutProvider](../../../reference/timeoutManager.md#timeoutmanagersettimeoutprovider). - If set to `Infinity`, will disable garbage collection - ##### `networkMode: 'online' | 'always' | 'offlineFirst'` - optional - defaults to `'online'` - see [Network Mode](../guides/network-mode.md) for more information. - ##### `initialData: TData | () => TData` - Optional - If set, this value will be used as the initial data for the query cache (as long as the query hasn't been created or cached yet) - If set to a function, the function will be called **once** during the shared/root query initialization, and be expected to synchronously return the initialData - Initial data is considered stale by default unless a `staleTime` has been set. - `initialData` **is persisted** to the cache - ##### `initialDataUpdatedAt: number | (() => number | undefined)` - Optional - If set, this value will be used as the time (in milliseconds) of when the `initialData` itself was last updated. - ##### `meta: Record` - Optional - If set, stores additional information on the query cache entry that can be used as needed. It will be accessible wherever the `query` is available, and is also part of the `QueryFunctionContext` provided to the `queryFn`. - ##### `queryKeyHashFn: (queryKey: QueryKey) => string` - Optional - If specified, this function is used to hash the `queryKey` to a string. - ##### `refetchInterval: number | false | ((query: Query) => number | false | undefined)` - Optional - If set to a number, all queries will continuously refetch at this frequency in milliseconds - If set to a function, the function will be executed with the query to compute a frequency - ##### `refetchIntervalInBackground: boolean` - Optional - If set to `true`, queries that are set to continuously refetch with a `refetchInterval` will continue to refetch while their tab/window is in the background - ##### `refetchOnMount: boolean | "always" | ((query: Query) => boolean | "always")` - Optional - Defaults to `true` - If set to `true`, the query will refetch on mount if the data is stale. - If set to `false`, the query will not refetch on mount. - If set to `"always"`, the query will always refetch on mount. - If set to a function, the function will be executed with the query to compute the value - ##### `refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")` - Optional - Defaults to `true` - If set to `true`, the query will refetch on window focus if the data is stale. - If set to `false`, the query will not refetch on window focus. - If set to `"always"`, the query will always refetch on window focus. - If set to a function, the function will be executed with the query to compute the value - ##### `refetchOnReconnect: boolean | "always" | ((query: Query) => boolean | "always")` - Optional - Defaults to `true` - If set to `true`, the query will refetch on reconnect if the data is stale. - If set to `false`, the query will not refetch on reconnect. - If set to `"always"`, the query will always refetch on reconnect. - If set to a function, the function will be executed with the query to compute the value - ##### `retry: boolean | number | (failureCount: number, error: TError) => boolean` - If `false`, failed queries will not retry by default. - If `true`, failed queries will retry infinitely. - If set to a `number`, e.g. `3`, failed queries will retry until the failed query count meets that number. - defaults to `3` on the client and `0` on the server - ##### `retryOnMount: boolean` - If set to `false`, the query will not be retried on mount if it contains an error. Defaults to `true`. - ##### `retryDelay: number | (retryAttempt: number, error: TError) => number` - This function receives a `retryAttempt` integer and the actual Error and returns the delay to apply before the next attempt in milliseconds. - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff. - A function like `attempt => attempt * 1000` applies linear backoff. - ##### `staleTime: number | Infinity` - Optional - Defaults to `0` - The time in milliseconds after data is considered stale. This value only applies to the hook it is defined on. - If set to `Infinity`, the data will never be considered stale - ##### `throwOnError: undefined | boolean | (error: TError, query: Query) => boolean` - Set this to `true` if you want errors to be thrown in the render phase and propagate to the nearest error boundary - Set this to `false` to disable `suspense`'s default behavior of throwing errors to the error boundary. - If set to a function, it will be passed the error and the query, and it should return a boolean indicating whether to show the error in an error boundary (`true`) or return the error as state (`false`) - ### Query Client - `Accessor` - Optional - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. ## `useQuery` Return Value - `Store>` `useQuery` returns a SolidJS store with the following properties: - ##### `status: QueryStatus` - Will be: - `pending` if there's no cached data and no query attempt was finished yet. - `error` if the query attempt resulted in an error. The corresponding `error` property has the error received from the attempted fetch - `success` if the query has received a response with no errors and is ready to display its data. The corresponding `data` property on the query is the data received from the successful fetch or if the query's `enabled` property is set to `false` and has not been fetched yet `data` is the first `initialData` supplied to the query on initialization. - ##### `isPending: boolean` - A derived boolean from the `status` variable above, provided for convenience. - ##### `isSuccess: boolean` - A derived boolean from the `status` variable above, provided for convenience. - ##### `isError: boolean` - A derived boolean from the `status` variable above, provided for convenience. - ##### `isLoadingError: boolean` - Will be `true` if the query failed while fetching for the first time. - ##### `isRefetchError: boolean` - Will be `true` if the query failed while refetching. - ##### `data: Resource` - Defaults to `undefined`. - The last successfully resolved data for the query. - **Important**: The `data` property is a SolidJS resource. This means that if the data is accessed underneath a `` component, it will trigger the Suspense boundary if the data is not available yet. - ##### `dataUpdatedAt: number` - The timestamp for when the query most recently returned the `status` as `"success"`. - ##### `error: null | TError` - Defaults to `null` - The error object for the query, if an error was thrown. - ##### `errorUpdatedAt: number` - The timestamp for when the query most recently returned the `status` as `"error"`. - ##### `isStale: boolean` - Will be `true` if the data in the cache is invalidated or if the data is older than the given `staleTime`. - ##### `isPlaceholderData: boolean` - Will be `true` if the data shown is the placeholder data. - ##### `isFetched: boolean` - Will be `true` if the query has been fetched. - ##### `isFetchedAfterMount: boolean` - Will be `true` if the query has been fetched after the component mounted. - This property can be used to not show any previously cached data. - ##### `fetchStatus: FetchStatus` - `fetching`: Is `true` whenever the queryFn is executing, which includes initial `pending` as well as background refetches. - `paused`: The query wanted to fetch, but has been `paused`. - `idle`: The query is not fetching. - see [Network Mode](../guides/network-mode.md) for more information. - ##### `isFetching: boolean` - A derived boolean from the `fetchStatus` variable above, provided for convenience. - ##### `isPaused: boolean` - A derived boolean from the `fetchStatus` variable above, provided for convenience. - ##### `isRefetching: boolean` - Is `true` whenever a background refetch is in-flight, which _does not_ include initial `pending` - Is the same as `isFetching && !isPending` - ##### `isLoading: boolean` - Is `true` whenever the first fetch for a query is in-flight - Is the same as `isFetching && isPending` - ##### `isInitialLoading: boolean` - **deprecated** - An alias for `isLoading`, will be removed in the next major version. - ##### `failureCount: number` - The failure count for the query. - Incremented every time the query fails. - Reset to `0` when the query succeeds. - ##### `failureReason: null | TError` - The failure reason for the query retry. - Reset to `null` when the query succeeds. - ##### `errorUpdateCount: number` - The sum of all errors. - ##### `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise` - A function to manually refetch the query. - If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option - `cancelRefetch?: boolean` - Defaults to `true` - Per default, a currently running request will be cancelled before a new request is made - When set to `false`, no refetch will be made if there is already a request running. ================================================ FILE: docs/framework/solid/typescript.md ================================================ --- id: typescript title: TypeScript --- Solid Query is written in **TypeScript** to make sure the library and your projects are type-safe! Things to keep in mind: - TanStack Query follows [DefinitelyTyped's support window](https://github.com/DefinitelyTyped/DefinitelyTyped#support-window) and supports TypeScript versions released within the last 2 years. At the moment, that means TypeScript **5.4** and newer. - Changes to types in this repository are considered **non-breaking** and are usually released as **patch** semver changes (otherwise every type enhancement would be a major version!). - It is **highly recommended that you lock your solid-query package version to a specific patch release and upgrade with the expectation that types may be fixed or upgraded between any release** - The non-type-related public API of Solid Query still follows semver very strictly. ## Type Inference Types in Solid Query generally flow through very well so that you don't have to provide type annotations for yourself ```tsx import { useQuery } from '@tanstack/solid-query' const query = useQuery(() => ({ queryKey: ['number'], queryFn: () => Promise.resolve(5), })) query.data // ^? (property) data: number | undefined ``` [typescript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgYygUwIYzQRQK5pQCecAvnAGZQQhwDkAAjBgHYDOzyA1gPRsQAbYABMAtAEcCxOgFgAUPOQR28SYRIBeFOiy4pRABQGAlHA0A+OAYTy4duGuIBpNEQBccANp0WeEACNCOgBdABo4W3tHIgAxFg8TM0sABWoQYDY0ADp0fgEANzQDAFZjeVJjMoU5aKzhLAx5Hh57OAA9AH55brkgA) ```tsx import { useQuery } from '@tanstack/solid-query' const query = useQuery(() => ({ queryKey: ['test'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), })) query.data // ^? (property) data: string | undefined ``` [typescript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgYygUwIYzQRQK5pQCecAvnAGZQQhwDkAAjBgHYDOzyA1gPRsQAbYABMAtAEcCxOgFgAUPOQR28SYRIBeFOiy4pRABQGAlHA0A+OAYTy4duGuIBpNEQBccANp1sHOgF0AGjhbe0ciADEWDxMzSwAFahBgNjQAOnR+AQA3NAMAVmNA0LtUgTRkGBjhLAxTCzga5jSYCABlGChgFgBzE2K5UmNjeXlwtKaMeR4eezgAPQB+UYU5IA) This works best if your `queryFn` has a well-defined returned type. Keep in mind that most data fetching libraries return `any` per default, so make sure to extract it to a properly typed function: ```tsx const fetchGroups = (): Promise => axios.get('/groups').then((response) => response.data) const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.data // ^? (property) data: Group[] | undefined ``` [typescript playground](https://www.typescriptlang.org/play/?ssl=11&ssc=4&pln=6&pc=1#code/JYWwDg9gTgLgBAbzgYygUwIYzQRQK5pQCecAvnAGZQQhwDkAAjBgHYDOzyA1gPRsQAbYABMAtAEcCxOgFgAUKEiw4GAB7AIbStVp01GtrLnyYRMGjgBxanjBwAvIjgiAXHBZ4QAI0Jl585Ah2eAo0GGQAC2sIWy1HAAoASjcABR1gNjQAHmjbAG0AXQA+BxL9TQA6AHMw+LoeKpswQ0SKmAi0Fnj0Nkh2C3sSnr7MiuEsDET-OUDguElCEkdUTGx8Rfik0rh4hHk4A-mpIgBpNCI3PLpGmOa6AoAaOH3DheIAMRY3UPCoprYHvJSIkpsY5G8iGMJvIeDxDnAAHoAfmm8iAA) ## Type Narrowing Solid Query uses a [discriminated union type](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions) for the query result, discriminated by the `status` field and the derived status boolean flags. This will allow you to check for e.g. `success` status to make `data` defined: ```tsx const query = useQuery(() => ({ queryKey: ['number'], queryFn: () => Promise.resolve(5), })) if (query.isSuccess) { const data = query.data // ^? const data: number } ``` [typescript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgYygUwIYzQRQK5pQCecAvnAGZQQhwDkAAjBgHYDOzyA1gPRsQAbYABMAtAEcCxOgFgAUKEixEKdFjQBRChTTJ45KjXr8hYgFZtZc+cgjt4kwiQC8qzNnxOAFF4CUcZwA+OC8EeTg4R2IAaTQiAC44AG06FjwQACNCOgBdABpwyKkiADEWRL8A4IAFahBgNjQAOnQTADc0LwBWXwK5Ul9feXlgChCooiaGgGU8ZGQ0NjZ-MLkIiNt7OGEsDACipyad5kKInh51iIA9AH55UmHrOSA) ## Typing the error field The type for error defaults to `Error`, because that is what most users expect. ```tsx const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: Error | null ``` [typescript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgYygUwIYzQRQK5pQCecAvnAGZQQhwDkAAjBgHYDOzyA1gPRsQAbYABMAtAEcCxOgFgAUKEiw4GAB7AIbStVp01GtrLnyYRMGjgBxanjBwAvIjgiAXHBZ4QAI0Jl585Ah2eAo0GGQAC2sIWy1HAAoASjcABR1gNjQAHmjbAG0AXQA+BxL9TQA6AHMw+LoeKpswQ0SKmAi0Fnj0Nkh2C3sSnr7MiuEsDET-OUDguElCEkdUTGx8Rfik0rh4hHk4A-mpIgBpNCI3PLpGmOa6AoAaOH3DheIAMRY3UPCoprYHvJSIkpsY5G8iBVCNQoPIeDxDnAAHoAfmm8iAA) If you want to throw a custom error, or something that isn't an `Error` at all, you can specify the type of the error field: ```tsx const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: string | null ``` However, this has the drawback that type inference for all other generics of `useQuery` will not work anymore. It is generally not considered a good practice to throw something that isn't an `Error`, so if you have a subclass like `AxiosError` you can use _type narrowing_ to make the error field more specific: ```tsx import axios from 'axios' const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: Error | null if (axios.isAxiosError(query.error)) { query.error // ^? (property) error: AxiosError } ``` [typescript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgYygUwIYzQRQK5pQCecAvnAGZQQhwDkAAjBgHYDOzyA1gPRsQAbYABMAtAEcCxOgFgAUKEiw4GAB7AIbStVp01GtrLnyYRMGjgBxanjBwAvIjgiAXHBZ4QAI0Jl585Ah2eAo0GGQAC2sIWy1HAAoASjcABR1gNjQAHmjbAG0AXQA+BxL9TQA6AHMw+LoeKpswQ0SKmAi0Fnj0Nkh2C3sSnr7MiuEsDET-OUDguElCEkdUTGx8Rfik0rh4hHk4A-mpIgBpNCI3PLpGmOa6AoAaOH3DheIAMRY3UPCoprYHvJSIkpsY5G8iBVCNQoPIeDxDnAAHoAfmmwAoO3KbAqGQAgupNABRKAw+IQqGk6AgxAvA4U6HQOlweGI1FA+RAA) [//]: # 'RegisterErrorType' ```tsx import '@tanstack/solid-query' declare module '@tanstack/solid-query' { interface Register { // Use unknown so call sites must narrow explicitly. defaultError: unknown } } const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: unknown | null ``` [//]: # 'RegisterErrorType' ## Registering global `Meta` Similarly to registering a [global error type](#registering-a-global-error) you can also register a global `Meta` type. This ensures the optional `meta` field on [queries](./reference/useQuery.md) and [mutations](./reference/useMutation.md) stays consistent and is type-safe. Note that the registered type must extend `Record` so that `meta` remains an object. ```ts import '@tanstack/solid-query' interface MyMeta extends Record { // Your meta type definition. } declare module '@tanstack/solid-query' { interface Register { queryMeta: MyMeta mutationMeta: MyMeta } } ``` ## Typing Query Options If you inline query options into `useQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `useQuery` and e.g. `prefetchQuery`. In that case, you'd lose type inference. To get it back, you can use `queryOptions` helper: ```ts import { queryOptions } from '@tanstack/solid-query' function groupOptions() { return queryOptions({ queryKey: ['groups'], queryFn: fetchGroups, staleTime: 5 * 1000, }) } useQuery(groupOptions) queryClient.prefetchQuery(groupOptions()) ``` Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: ```ts function groupOptions() { return queryOptions({ queryKey: ['groups'], queryFn: fetchGroups, staleTime: 5 * 1000, }) } const data = queryClient.getQueryData(groupOptions().queryKey) // ^? const data: Group[] | undefined ``` Without `queryOptions`, the type of `data` would be `unknown`, unless we'd pass a generic to it: ```ts const data = queryClient.getQueryData(['groups']) ``` ## Typesafe disabling of queries using `skipToken` If you are using TypeScript, you can use the `skipToken` to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. Read more about it in the [Disabling Queries](./guides/disabling-queries.md) guide. ================================================ FILE: docs/framework/svelte/devtools.md ================================================ --- id: devtools title: Devtools --- > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > > - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) > - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) > - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) ## Install and Import the Devtools The devtools are a separate package that you need to install: ```bash npm i @tanstack/svelte-query-devtools ``` or ```bash pnpm add @tanstack/svelte-query-devtools ``` or ```bash yarn add @tanstack/svelte-query-devtools ``` or ```bash bun add @tanstack/svelte-query-devtools ``` You can import the devtools like this: ```ts import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools' ``` ## Floating Mode Floating Mode will mount the devtools as a fixed, floating element in your app and provide a toggle in the corner of the screen to show and hide the devtools. This toggle state will be stored and remembered in localStorage across reloads. Place the following code as high in your Svelte app as you can. The closer it is to the root of the page, the better it will work! ```ts {/* The rest of your application */} ``` ### Options - `initialIsOpen: boolean` - Set this `true` if you want the dev tools to default to being open - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative"` - Defaults to `bottom-right` - The position of the TanStack logo to open and close the devtools panel - If `relative`, the button is placed in the location that you render the devtools. - `position?: "top" | "bottom" | "left" | "right"` - Defaults to `bottom` - The position of the Svelte Query devtools panel - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ================================================ FILE: docs/framework/svelte/installation.md ================================================ --- id: installation title: Installation --- You can install Svelte Query via [NPM](https://npmjs.com). ### NPM ```bash npm i @tanstack/svelte-query ``` or ```bash pnpm add @tanstack/svelte-query ``` or ```bash yarn add @tanstack/svelte-query ``` or ```bash bun add @tanstack/svelte-query ``` > Wanna give it a spin before you download? Try out the [basic](./examples/basic) example! ================================================ FILE: docs/framework/svelte/migrate-from-v5-to-v6.md ================================================ While Svelte v5 has legacy compatibility with the stores syntax from Svelte v3/v4, it has been somewhat buggy and unreliable for this adapter. The `@tanstack/svelte-query` v6 adapter fully migrates to the runes syntax, which relies on signals. This rewrite should also simplify the code required to ensure your query inputs remain reactive. ## Installation Please ensure your project has [Svelte v5.25.0](https://github.com/sveltejs/svelte/releases/tag/svelte%405.25.0) or newer. Run `pnpm add @tanstack/svelte-query@latest` (or your package manager's equivalent). > Note that `@tanstack/svelte-query` v6 depends on `@tanstack/query-core` v5. ## Function Inputs Like the Angular and Solid adapters, most functions for the Svelte adapter now require options to be provided as a "thunk" (`() => options`) to provide reactivity. TypeScript will be your friend here and warn you if you're missing this notation. ```ts - const query = createQuery({ // [!code --] + const query = createQuery(() => ({ // [!code ++] queryKey: ['todos'], queryFn: () => fetchTodos(), - }) // [!code --] + })) // [!code ++] ``` ## Accessing Properties Given the adapter no longer uses stores, it is no longer necessary to prefix with `$`. ```svelte - {#if $todos.isSuccess} // [!code --] + {#if todos.isSuccess} // [!code ++]
      - {#each $todos.data.items as item} // [!code --] + {#each todos.data.items as item} // [!code ++]
    • {item}
    • {/each}
    {/if} ``` ## Reactivity You previously needed to do some funky things with stores to achieve reactivity for inputs. This is no longer the case! You don't even need to wrap your query in a `$derived`. ```ts - const intervalMs = writable(1000) // [!code --] + let intervalMs = $state(1000) // [!code ++] - const query = createQuery(derived(intervalMs, ($intervalMs) => ({ // [!code --] + const query = createQuery(() => ({ // [!code ++] queryKey: ['refetch'], queryFn: async () => await fetch('/api/data').then((r) => r.json()), - refetchInterval: $intervalMs, // [!code --] + refetchInterval: intervalMs, // [!code ++] - }))) // [!code --] + })) // [!code ++] ``` ## Disabling Legacy Mode If your component has any stores, it might not properly switch to runes mode. You can ensure your application is using runes in two ways: ### On a per-file basis In each `.svelte` file, once you have migrated to runes, add ``. This is better for large applications requiring gradual migration. ### On an project-wide basis In your `svelte.config.js`, add the following to config: ```json compilerOptions: { runes: true, }, ``` This can be added once you've 100% eradicated stores syntax from your app. ================================================ FILE: docs/framework/svelte/overview.md ================================================ --- id: overview title: Overview --- The `@tanstack/svelte-query` package offers a 1st-class API for using TanStack Query via Svelte. > Migrating from stores to the runes syntax? See the [migration guide](./migrate-from-v5-to-v6). ## Example Include the QueryClientProvider near the root of your project: ```svelte ``` Then call any function (e.g. createQuery) from any component: ```svelte
    {#if query.isLoading}

    Loading...

    {:else if query.isError}

    Error: {query.error.message}

    {:else if query.isSuccess} {#each query.data as todo}

    {todo.title}

    {/each} {/if}
    ``` ## SvelteKit If you are using SvelteKit, please have a look at [SSR & SvelteKit](./ssr). ## Available Functions Svelte Query offers useful functions and components that will make managing server state in Svelte apps easier. - `createQuery` - `createQueries` - `createInfiniteQuery` - `createMutation` - `useQueryClient` - `useIsFetching` - `useIsMutating` - `useMutationState` - `useIsRestoring` - `useHydrate` - `` - `` ## Important Differences between Svelte Query & React Query Svelte Query offers an API similar to React Query, but there are some key differences to be mindful of. - The arguments to the `create*` functions must be wrapped in a function to preserve reactivity. ================================================ FILE: docs/framework/svelte/reference/functions/createInfiniteQuery.md ================================================ --- id: createInfiniteQuery title: createInfiniteQuery --- # Function: createInfiniteQuery() ```ts function createInfiniteQuery(options, queryClient?): CreateInfiniteQueryResult; ``` Defined in: [packages/svelte-query/src/createInfiniteQuery.ts:16](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createInfiniteQuery.ts#L16) ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `Error` ### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> ### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### TPageParam `TPageParam` = `unknown` ## Parameters ### options [`Accessor`](../type-aliases/Accessor.md)\<[`CreateInfiniteQueryOptions`](../type-aliases/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>\> ### queryClient? [`Accessor`](../type-aliases/Accessor.md)\<`QueryClient`\> ## Returns [`CreateInfiniteQueryResult`](../type-aliases/CreateInfiniteQueryResult.md)\<`TData`, `TError`\> ================================================ FILE: docs/framework/svelte/reference/functions/createMutation.md ================================================ --- id: createMutation title: createMutation --- # Function: createMutation() ```ts function createMutation(options, queryClient?): CreateMutationResult; ``` Defined in: [packages/svelte-query/src/createMutation.svelte.ts:17](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createMutation.svelte.ts#L17) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `Error` ### TVariables `TVariables` = `void` ### TContext `TContext` = `unknown` ## Parameters ### options [`Accessor`](../type-aliases/Accessor.md)\<[`CreateMutationOptions`](../type-aliases/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TContext`\>\> A function that returns mutation options ### queryClient? [`Accessor`](../type-aliases/Accessor.md)\<`QueryClient`\> Custom query client which overrides provider ## Returns [`CreateMutationResult`](../type-aliases/CreateMutationResult.md)\<`TData`, `TError`, `TVariables`, `TContext`\> ================================================ FILE: docs/framework/svelte/reference/functions/createQueries.md ================================================ --- id: createQueries title: createQueries --- # Function: createQueries() ```ts function createQueries(createQueriesOptions, queryClient?): TCombinedResult; ``` Defined in: [packages/svelte-query/src/createQueries.svelte.ts:189](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createQueries.svelte.ts#L189) ## Type Parameters ### T `T` *extends* `any`[] ### TCombinedResult `TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetCreateQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetCreateQueryResult`\<`Head`\>, `GetCreateQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetCreateQueryResult`\<`Head`\>, `GetCreateQueryResult`\<`Head`\>, `GetCreateQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetCreateQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetCreateQueryResult\\]\> \} ## Parameters ### createQueriesOptions [`Accessor`](../type-aliases/Accessor.md)\<\{ `combine?`: (`result`) => `TCombinedResult`; `queries`: \| readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetCreateQueryOptionsForCreateQueries`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetCreateQueryOptionsForCreateQueries`\<...\>, `GetCreateQueryOptionsForCreateQueries`\<...\>\] : \[`...(...)[]`\] *extends* \[..., `...(...)[]`\] ? ... *extends* ... ? ... : ... : ... *extends* ... ? ... : ... : readonly `unknown`[] *extends* `T` ? `T` : `T` *extends* `CreateQueryOptionsForCreateQueries`\<..., ..., ..., ...\>[] ? `CreateQueryOptionsForCreateQueries`\<..., ..., ..., ...\>[] : `CreateQueryOptionsForCreateQueries`\<..., ..., ..., ...\>[]\] \| readonly \[\{ \[K in string \| number \| symbol\]: GetCreateQueryOptionsForCreateQueries\\]\> \}\]; \}\> ### queryClient? [`Accessor`](../type-aliases/Accessor.md)\<`QueryClient`\> ## Returns `TCombinedResult` ================================================ FILE: docs/framework/svelte/reference/functions/createQuery.md ================================================ --- id: createQuery title: createQuery --- # Function: createQuery() ## Call Signature ```ts function createQuery(options, queryClient?): CreateQueryResult; ``` Defined in: [packages/svelte-query/src/createQuery.ts:15](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createQuery.ts#L15) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`Accessor`](../type-aliases/Accessor.md)\<[`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>\> #### queryClient? [`Accessor`](../type-aliases/Accessor.md)\<`QueryClient`\> ### Returns [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`TData`, `TError`\> ## Call Signature ```ts function createQuery(options, queryClient?): DefinedCreateQueryResult; ``` Defined in: [packages/svelte-query/src/createQuery.ts:27](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createQuery.ts#L27) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`Accessor`](../type-aliases/Accessor.md)\<[`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>\> #### queryClient? [`Accessor`](../type-aliases/Accessor.md)\<`QueryClient`\> ### Returns [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`TData`, `TError`\> ## Call Signature ```ts function createQuery(options, queryClient?): CreateQueryResult; ``` Defined in: [packages/svelte-query/src/createQuery.ts:39](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createQuery.ts#L39) ### Type Parameters #### TQueryFnData `TQueryFnData` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`Accessor`](../type-aliases/Accessor.md)\<[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>\> #### queryClient? [`Accessor`](../type-aliases/Accessor.md)\<`QueryClient`\> ### Returns [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`TData`, `TError`\> ================================================ FILE: docs/framework/svelte/reference/functions/getIsRestoringContext.md ================================================ --- id: getIsRestoringContext title: getIsRestoringContext --- # Function: getIsRestoringContext() ```ts function getIsRestoringContext(): Box; ``` Defined in: [packages/svelte-query/src/context.ts:27](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/context.ts#L27) Retrieves a `isRestoring` from Svelte's context ## Returns `Box`\<`boolean`\> ================================================ FILE: docs/framework/svelte/reference/functions/getQueryClientContext.md ================================================ --- id: getQueryClientContext title: getQueryClientContext --- # Function: getQueryClientContext() ```ts function getQueryClientContext(): QueryClient; ``` Defined in: [packages/svelte-query/src/context.ts:8](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/context.ts#L8) Retrieves a Client from Svelte's context ## Returns `QueryClient` ================================================ FILE: docs/framework/svelte/reference/functions/infiniteQueryOptions.md ================================================ --- id: infiniteQueryOptions title: infiniteQueryOptions --- # Function: infiniteQueryOptions() ```ts function infiniteQueryOptions(options): CreateInfiniteQueryOptions; ``` Defined in: [packages/svelte-query/src/infiniteQueryOptions.ts:4](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/infiniteQueryOptions.ts#L4) ## Type Parameters ### TQueryFnData `TQueryFnData` ### TError `TError` = `Error` ### TData `TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> ### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### TPageParam `TPageParam` = `unknown` ## Parameters ### options [`CreateInfiniteQueryOptions`](../type-aliases/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ## Returns [`CreateInfiniteQueryOptions`](../type-aliases/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> ================================================ FILE: docs/framework/svelte/reference/functions/mutationOptions.md ================================================ --- id: mutationOptions title: mutationOptions --- # Function: mutationOptions() ## Call Signature ```ts function mutationOptions(options): WithRequired, 'mutationKey'> ``` Defined in: [packages/svelte-query/src/mutationOptions.ts](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/mutationOptions.ts) ### Type Parameters #### TData `TData` = `unknown` #### TError `TError` = `Error` #### TVariables `TVariables` = `void` #### TOnMutateResult `TOnMutateResult` = `unknown` ### Parameters #### options `WithRequired`\<[`CreateMutationOptions`](../type-aliases/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `'mutationKey'`\> ### Returns `WithRequired`\<[`CreateMutationOptions`](../type-aliases/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `'mutationKey'`\> ## Call Signature ```ts function mutationOptions(options): Omit, 'mutationKey'> ``` Defined in: [packages/svelte-query/src/mutationOptions.ts](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/mutationOptions.ts) ### Type Parameters #### TData `TData` = `unknown` #### TError `TError` = `Error` #### TVariables `TVariables` = `void` #### TOnMutateResult `TOnMutateResult` = `unknown` ### Parameters #### options `Omit`\<[`CreateMutationOptions`](../type-aliases/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `'mutationKey'`\> ### Returns `Omit`\<[`CreateMutationOptions`](../type-aliases/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `'mutationKey'`\> ================================================ FILE: docs/framework/svelte/reference/functions/queryOptions.md ================================================ --- id: queryOptions title: queryOptions --- # Function: queryOptions() ## Call Signature ```ts function queryOptions(options): CreateQueryOptions & object & object; ``` Defined in: [packages/svelte-query/src/queryOptions.ts:30](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/queryOptions.ts#L30) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### Returns [`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` ## Call Signature ```ts function queryOptions(options): CreateQueryOptions & object & object; ``` Defined in: [packages/svelte-query/src/queryOptions.ts:41](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/queryOptions.ts#L41) ### Type Parameters #### TQueryFnData `TQueryFnData` = `unknown` #### TError `TError` = `Error` #### TData `TData` = `TQueryFnData` #### TQueryKey `TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] ### Parameters #### options [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### Returns [`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` ================================================ FILE: docs/framework/svelte/reference/functions/setIsRestoringContext.md ================================================ --- id: setIsRestoringContext title: setIsRestoringContext --- # Function: setIsRestoringContext() ```ts function setIsRestoringContext(isRestoring): void; ``` Defined in: [packages/svelte-query/src/context.ts:39](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/context.ts#L39) Sets a `isRestoring` on Svelte's context ## Parameters ### isRestoring `Box`\<`boolean`\> ## Returns `void` ================================================ FILE: docs/framework/svelte/reference/functions/setQueryClientContext.md ================================================ --- id: setQueryClientContext title: setQueryClientContext --- # Function: setQueryClientContext() ```ts function setQueryClientContext(client): void; ``` Defined in: [packages/svelte-query/src/context.ts:20](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/context.ts#L20) Sets a QueryClient on Svelte's context ## Parameters ### client `QueryClient` ## Returns `void` ================================================ FILE: docs/framework/svelte/reference/functions/useHydrate.md ================================================ --- id: useHydrate title: useHydrate --- # Function: useHydrate() ```ts function useHydrate( state?, options?, queryClient?): void; ``` Defined in: [packages/svelte-query/src/useHydrate.ts:5](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/useHydrate.ts#L5) ## Parameters ### state? `unknown` ### options? `HydrateOptions` ### queryClient? `QueryClient` ## Returns `void` ================================================ FILE: docs/framework/svelte/reference/functions/useIsFetching.md ================================================ --- id: useIsFetching title: useIsFetching --- # Function: useIsFetching() ```ts function useIsFetching(filters?, queryClient?): ReactiveValue; ``` Defined in: [packages/svelte-query/src/useIsFetching.svelte.ts:5](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/useIsFetching.svelte.ts#L5) ## Parameters ### filters? `QueryFilters`\ ### queryClient? `QueryClient` ## Returns `ReactiveValue`\<`number`\> ================================================ FILE: docs/framework/svelte/reference/functions/useIsMutating.md ================================================ --- id: useIsMutating title: useIsMutating --- # Function: useIsMutating() ```ts function useIsMutating(filters?, queryClient?): ReactiveValue; ``` Defined in: [packages/svelte-query/src/useIsMutating.svelte.ts:5](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/useIsMutating.svelte.ts#L5) ## Parameters ### filters? `MutationFilters`\<`unknown`, `Error`, `unknown`, `unknown`\> ### queryClient? `QueryClient` ## Returns `ReactiveValue`\<`number`\> ================================================ FILE: docs/framework/svelte/reference/functions/useIsRestoring.md ================================================ --- id: useIsRestoring title: useIsRestoring --- # Function: useIsRestoring() ```ts function useIsRestoring(): Box; ``` Defined in: [packages/svelte-query/src/useIsRestoring.ts:4](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/useIsRestoring.ts#L4) ## Returns `Box`\<`boolean`\> ================================================ FILE: docs/framework/svelte/reference/functions/useMutationState.md ================================================ --- id: useMutationState title: useMutationState --- # Function: useMutationState() ```ts function useMutationState(options, queryClient?): TResult[]; ``` Defined in: [packages/svelte-query/src/useMutationState.svelte.ts:22](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/useMutationState.svelte.ts#L22) ## Type Parameters ### TResult `TResult` = `MutationState`\<`unknown`, `Error`, `unknown`, `unknown`\> ## Parameters ### options [`MutationStateOptions`](../type-aliases/MutationStateOptions.md)\<`TResult`\> = `{}` ### queryClient? `QueryClient` ## Returns `TResult`[] ================================================ FILE: docs/framework/svelte/reference/functions/useQueryClient.md ================================================ --- id: useQueryClient title: useQueryClient --- # Function: useQueryClient() ```ts function useQueryClient(queryClient?): QueryClient; ``` Defined in: [packages/svelte-query/src/useQueryClient.ts:4](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/useQueryClient.ts#L4) ## Parameters ### queryClient? `QueryClient` ## Returns `QueryClient` ================================================ FILE: docs/framework/svelte/reference/index.md ================================================ --- id: "@tanstack/svelte-query" title: "@tanstack/svelte-query" --- # @tanstack/svelte-query ## Type Aliases - [Accessor](type-aliases/Accessor.md) - [CreateBaseMutationResult](type-aliases/CreateBaseMutationResult.md) - [CreateBaseQueryOptions](type-aliases/CreateBaseQueryOptions.md) - [CreateBaseQueryResult](type-aliases/CreateBaseQueryResult.md) - [CreateInfiniteQueryOptions](type-aliases/CreateInfiniteQueryOptions.md) - [CreateInfiniteQueryResult](type-aliases/CreateInfiniteQueryResult.md) - [CreateMutateAsyncFunction](type-aliases/CreateMutateAsyncFunction.md) - [CreateMutateFunction](type-aliases/CreateMutateFunction.md) - [CreateMutationOptions](type-aliases/CreateMutationOptions.md) - [CreateMutationResult](type-aliases/CreateMutationResult.md) - [CreateQueryOptions](type-aliases/CreateQueryOptions.md) - [CreateQueryResult](type-aliases/CreateQueryResult.md) - [DefinedCreateBaseQueryResult](type-aliases/DefinedCreateBaseQueryResult.md) - [DefinedCreateQueryResult](type-aliases/DefinedCreateQueryResult.md) - [DefinedInitialDataOptions](type-aliases/DefinedInitialDataOptions.md) - [HydrationBoundary](type-aliases/HydrationBoundary.md) - [MutationStateOptions](type-aliases/MutationStateOptions.md) - [QueriesOptions](type-aliases/QueriesOptions.md) - [QueriesResults](type-aliases/QueriesResults.md) - [QueryClientProviderProps](type-aliases/QueryClientProviderProps.md) - [UndefinedInitialDataOptions](type-aliases/UndefinedInitialDataOptions.md) ## Variables - [HydrationBoundary](variables/HydrationBoundary.md) ## Functions - [createInfiniteQuery](functions/createInfiniteQuery.md) - [createMutation](functions/createMutation.md) - [createQueries](functions/createQueries.md) - [createQuery](functions/createQuery.md) - [getIsRestoringContext](functions/getIsRestoringContext.md) - [getQueryClientContext](functions/getQueryClientContext.md) - [infiniteQueryOptions](functions/infiniteQueryOptions.md) - [queryOptions](functions/queryOptions.md) - [setIsRestoringContext](functions/setIsRestoringContext.md) - [setQueryClientContext](functions/setQueryClientContext.md) - [useHydrate](functions/useHydrate.md) - [useIsFetching](functions/useIsFetching.md) - [useIsMutating](functions/useIsMutating.md) - [useIsRestoring](functions/useIsRestoring.md) - [useMutationState](functions/useMutationState.md) - [useQueryClient](functions/useQueryClient.md) ## References ### QueryClientProvider Renames and re-exports [HydrationBoundary](variables/HydrationBoundary.md) ================================================ FILE: docs/framework/svelte/reference/type-aliases/Accessor.md ================================================ --- id: Accessor title: Accessor --- # Type Alias: Accessor()\ ```ts type Accessor = () => T; ``` Defined in: [packages/svelte-query/src/types.ts:21](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L21) ## Type Parameters ### T `T` ## Returns `T` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateBaseMutationResult.md ================================================ --- id: CreateBaseMutationResult title: CreateBaseMutationResult --- # Type Alias: CreateBaseMutationResult\ ```ts type CreateBaseMutationResult = Override, { mutate: CreateMutateFunction; }> & object; ``` Defined in: [packages/svelte-query/src/types.ts:114](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L114) ## Type Declaration ### mutateAsync ```ts mutateAsync: CreateMutateAsyncFunction; ``` ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateBaseQueryOptions.md ================================================ --- id: CreateBaseQueryOptions title: CreateBaseQueryOptions --- # Type Alias: CreateBaseQueryOptions\ ```ts type CreateBaseQueryOptions = QueryObserverOptions; ``` Defined in: [packages/svelte-query/src/types.ts:24](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L24) Options for createBaseQuery ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryData `TQueryData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateBaseQueryResult.md ================================================ --- id: CreateBaseQueryResult title: CreateBaseQueryResult --- # Type Alias: CreateBaseQueryResult\ ```ts type CreateBaseQueryResult = QueryObserverResult; ``` Defined in: [packages/svelte-query/src/types.ts:33](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L33) Result from createBaseQuery ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateInfiniteQueryOptions.md ================================================ --- id: CreateInfiniteQueryOptions title: CreateInfiniteQueryOptions --- # Type Alias: CreateInfiniteQueryOptions\ ```ts type CreateInfiniteQueryOptions = InfiniteQueryObserverOptions; ``` Defined in: [packages/svelte-query/src/types.ts:53](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L53) Options for createInfiniteQuery ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ### TPageParam `TPageParam` = `unknown` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateInfiniteQueryResult.md ================================================ --- id: CreateInfiniteQueryResult title: CreateInfiniteQueryResult --- # Type Alias: CreateInfiniteQueryResult\ ```ts type CreateInfiniteQueryResult = InfiniteQueryObserverResult; ``` Defined in: [packages/svelte-query/src/types.ts:68](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L68) Result from createInfiniteQuery ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateMutateAsyncFunction.md ================================================ --- id: CreateMutateAsyncFunction title: CreateMutateAsyncFunction --- # Type Alias: CreateMutateAsyncFunction\ ```ts type CreateMutateAsyncFunction = MutateFunction; ``` Defined in: [packages/svelte-query/src/types.ts:107](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L107) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateMutateFunction.md ================================================ --- id: CreateMutateFunction title: CreateMutateFunction --- # Type Alias: CreateMutateFunction()\ ```ts type CreateMutateFunction = (...args) => void; ``` Defined in: [packages/svelte-query/src/types.ts:96](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L96) ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ## Parameters ### args ...`Parameters`\<`MutateFunction`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>\> ## Returns `void` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateMutationOptions.md ================================================ --- id: CreateMutationOptions title: CreateMutationOptions --- # Type Alias: CreateMutationOptions\ ```ts type CreateMutationOptions = OmitKeyof, "_defaulted">; ``` Defined in: [packages/svelte-query/src/types.ts:86](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L86) Options for createMutation ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `void` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateMutationResult.md ================================================ --- id: CreateMutationResult title: CreateMutationResult --- # Type Alias: CreateMutationResult\ ```ts type CreateMutationResult = CreateBaseMutationResult; ``` Defined in: [packages/svelte-query/src/types.ts:132](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L132) Result from createMutation ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ### TVariables `TVariables` = `unknown` ### TOnMutateResult `TOnMutateResult` = `unknown` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateQueryOptions.md ================================================ --- id: CreateQueryOptions title: CreateQueryOptions --- # Type Alias: CreateQueryOptions\ ```ts type CreateQueryOptions = CreateBaseQueryOptions; ``` Defined in: [packages/svelte-query/src/types.ts:39](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L39) Options for createQuery ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/svelte/reference/type-aliases/CreateQueryResult.md ================================================ --- id: CreateQueryResult title: CreateQueryResult --- # Type Alias: CreateQueryResult\ ```ts type CreateQueryResult = CreateBaseQueryResult; ``` Defined in: [packages/svelte-query/src/types.ts:47](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L47) Result from createQuery ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/svelte/reference/type-aliases/DefinedCreateBaseQueryResult.md ================================================ --- id: DefinedCreateBaseQueryResult title: DefinedCreateBaseQueryResult --- # Type Alias: DefinedCreateBaseQueryResult\ ```ts type DefinedCreateBaseQueryResult = DefinedQueryObserverResult; ``` Defined in: [packages/svelte-query/src/types.ts:74](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L74) Options for createBaseQuery with initialData ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/svelte/reference/type-aliases/DefinedCreateQueryResult.md ================================================ --- id: DefinedCreateQueryResult title: DefinedCreateQueryResult --- # Type Alias: DefinedCreateQueryResult\ ```ts type DefinedCreateQueryResult = DefinedCreateBaseQueryResult; ``` Defined in: [packages/svelte-query/src/types.ts:80](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L80) Options for createQuery with initialData ## Type Parameters ### TData `TData` = `unknown` ### TError `TError` = `DefaultError` ================================================ FILE: docs/framework/svelte/reference/type-aliases/DefinedInitialDataOptions.md ================================================ --- id: DefinedInitialDataOptions title: DefinedInitialDataOptions --- # Type Alias: DefinedInitialDataOptions\ ```ts type DefinedInitialDataOptions = CreateQueryOptions & object; ``` Defined in: [packages/svelte-query/src/queryOptions.ts:19](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/queryOptions.ts#L19) ## Type Declaration ### initialData ```ts initialData: | NonUndefinedGuard | () => NonUndefinedGuard; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/svelte/reference/type-aliases/HydrationBoundary.md ================================================ --- id: HydrationBoundary title: HydrationBoundary --- # Type Alias: HydrationBoundary ```ts type HydrationBoundary = SvelteComponent; ``` Defined in: node\_modules/.pnpm/svelte@5.39.3/node\_modules/svelte/types/index.d.ts:3092 ================================================ FILE: docs/framework/svelte/reference/type-aliases/MutationStateOptions.md ================================================ --- id: MutationStateOptions title: MutationStateOptions --- # Type Alias: MutationStateOptions\ ```ts type MutationStateOptions = object; ``` Defined in: [packages/svelte-query/src/types.ts:140](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L140) Options for useMutationState ## Type Parameters ### TResult `TResult` = `MutationState` ## Properties ### filters? ```ts optional filters: MutationFilters; ``` Defined in: [packages/svelte-query/src/types.ts:141](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L141) *** ### select()? ```ts optional select: (mutation) => TResult; ``` Defined in: [packages/svelte-query/src/types.ts:142](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L142) #### Parameters ##### mutation `Mutation`\<`unknown`, `DefaultError`, `unknown`, `unknown`\> #### Returns `TResult` ================================================ FILE: docs/framework/svelte/reference/type-aliases/QueriesOptions.md ================================================ --- id: QueriesOptions title: QueriesOptions --- # Type Alias: QueriesOptions\ ```ts type QueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? CreateQueryOptionsForCreateQueries[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryOptionsForCreateQueries] : T extends [infer Head, ...(infer Tails)] ? QueriesOptions<[...Tails], [...TResults, GetCreateQueryOptionsForCreateQueries], [...TDepth, 1]> : ReadonlyArray extends T ? T : T extends CreateQueryOptionsForCreateQueries[] ? CreateQueryOptionsForCreateQueries[] : CreateQueryOptionsForCreateQueries[]; ``` Defined in: [packages/svelte-query/src/createQueries.svelte.ts:129](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createQueries.svelte.ts#L129) QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/svelte/reference/type-aliases/QueriesResults.md ================================================ --- id: QueriesResults title: QueriesResults --- # Type Alias: QueriesResults\ ```ts type QueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? CreateQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResult] : T extends [infer Head, ...(infer Tails)] ? QueriesResults<[...Tails], [...TResults, GetCreateQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetCreateQueryResult }; ``` Defined in: [packages/svelte-query/src/createQueries.svelte.ts:171](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/createQueries.svelte.ts#L171) QueriesResults reducer recursively maps type param to results ## Type Parameters ### T `T` *extends* `any`[] ### TResults `TResults` *extends* `any`[] = \[\] ### TDepth `TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] ================================================ FILE: docs/framework/svelte/reference/type-aliases/QueryClientProviderProps.md ================================================ --- id: QueryClientProviderProps title: QueryClientProviderProps --- # Type Alias: QueryClientProviderProps ```ts type QueryClientProviderProps = object; ``` Defined in: [packages/svelte-query/src/types.ts:147](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L147) ## Properties ### children ```ts children: Snippet; ``` Defined in: [packages/svelte-query/src/types.ts:149](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L149) *** ### client ```ts client: QueryClient; ``` Defined in: [packages/svelte-query/src/types.ts:148](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/types.ts#L148) ================================================ FILE: docs/framework/svelte/reference/type-aliases/UndefinedInitialDataOptions.md ================================================ --- id: UndefinedInitialDataOptions title: UndefinedInitialDataOptions --- # Type Alias: UndefinedInitialDataOptions\ ```ts type UndefinedInitialDataOptions = CreateQueryOptions & object; ``` Defined in: [packages/svelte-query/src/queryOptions.ts:10](https://github.com/TanStack/query/blob/main/packages/svelte-query/src/queryOptions.ts#L10) ## Type Declaration ### initialData? ```ts optional initialData: InitialDataFunction>; ``` ## Type Parameters ### TQueryFnData `TQueryFnData` = `unknown` ### TError `TError` = `DefaultError` ### TData `TData` = `TQueryFnData` ### TQueryKey `TQueryKey` *extends* `QueryKey` = `QueryKey` ================================================ FILE: docs/framework/svelte/reference/variables/HydrationBoundary.md ================================================ --- id: HydrationBoundary title: HydrationBoundary --- # Variable: HydrationBoundary ```ts const HydrationBoundary: LegacyComponentType; ``` Defined in: node\_modules/.pnpm/svelte@5.39.3/node\_modules/svelte/types/index.d.ts:3092 ================================================ FILE: docs/framework/svelte/ssr.md ================================================ --- id: overview title: SSR and SvelteKit --- ## Setup SvelteKit defaults to rendering routes with SSR. Because of this, you need to disable the query on the server. Otherwise, your query will continue executing on the server asynchronously, even after the HTML has been sent to the client. The recommended way to achieve this is to use the `browser` module from SvelteKit in your `QueryClient` object. This will not disable `queryClient.prefetchQuery()`, which is used in one of the solutions below. **src/routes/+layout.svelte** ```svelte ``` ## Prefetching data Svelte Query supports two ways of prefetching data on the server and passing that to the client with SvelteKit. If you wish to view the ideal SSR setup, please have a look at the [SSR example](./examples/ssr). ### Using `initialData` Together with SvelteKit's [`load`](https://kit.svelte.dev/docs/load), you can pass the data loaded server-side into `createQuery`'s' `initialData` option: **src/routes/+page.ts** ```ts export async function load() { const posts = await getPosts() return { posts } } ``` **src/routes/+page.svelte** ```svelte ``` Pros: - This setup is minimal and this can be a quick solution for some cases - Works with both `+page.ts`/`+layout.ts` and `+page.server.ts`/`+layout.server.ts` load functions Cons: - If you are calling `createQuery` in a component deeper down in the tree you need to pass the `initialData` down to that point - If you are calling `createQuery` with the same query in multiple locations, you need to pass `initialData` to all of them - There is no way to know at what time the query was fetched on the server, so `dataUpdatedAt` and determining if the query needs refetching is based on when the page loaded instead ### Using `prefetchQuery` Svelte Query supports prefetching queries on the server. Using this setup below, you can fetch data and pass it into QueryClientProvider before it is sent to the user's browser. Therefore, this data is already available in the cache, and no initial fetch occurs client-side. **src/routes/+layout.ts** ```ts import { browser } from '$app/environment' import { QueryClient } from '@tanstack/svelte-query' export async function load() { const queryClient = new QueryClient({ defaultOptions: { queries: { enabled: browser, }, }, }) return { queryClient } } ``` **src/routes/+layout.svelte** ```svelte ``` **src/routes/+page.ts** ```ts export async function load({ parent, fetch }) { const { queryClient } = await parent() // You need to use the SvelteKit fetch function here await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: async () => (await fetch('/api/posts')).json(), }) } ``` **src/routes/+page.svelte** ```svelte ``` Pros: - Server-loaded data can be accessed anywhere without prop-drilling - No initial fetch occurs client-side once the page is rendered, as the query cache retains all information about the query was made including `dataUpdatedAt` Cons: - Requires more files for initial setup - Will not work with `+page.server.ts`/`+layout.server.ts` load functions (however, APIs which are used with TanStack Query need to be fully exposed to the browser anyway) ================================================ FILE: docs/framework/vue/devtools.md ================================================ --- id: devtools title: Devtools --- Wave your hands in the air and shout hooray because Vue Query comes with dedicated devtools! 🥳 When you begin your Vue Query journey, you'll want these devtools by your side. They help visualize all of the inner workings of Vue Query and will likely save you hours of debugging if you find yourself in a pinch! > For Chrome, Firefox, and Edge users: Third-party browser extensions are available for debugging TanStack Query directly in browser DevTools. These provide the same functionality as the framework-specific devtools packages: > > - Chrome logo [Devtools for Chrome](https://chromewebstore.google.com/detail/tanstack-query-devtools/annajfchloimdhceglpgglpeepfghfai) > - Firefox logo [Devtools for Firefox](https://addons.mozilla.org/en-US/firefox/addon/tanstack-query-devtools/) > - Edge logo [Devtools for Edge](https://microsoftedge.microsoft.com/addons/detail/tanstack-query-devtools/edmdpkgkacmjopodhfolmphdenmddobj) ## Component based Devtools (Vue 3) You can directly integrate the devtools component into your page using a dedicated package. Component-based devtools use a framework-agnostic implementation and are always up-to-date. The devtools component is a separate package that you need to install: ```bash npm i @tanstack/vue-query-devtools ``` or ```bash pnpm add @tanstack/vue-query-devtools ``` or ```bash yarn add @tanstack/vue-query-devtools ``` or ```bash bun add @tanstack/vue-query-devtools ``` By default, Vue Query Devtools are only included in bundles when `process.env.NODE_ENV === 'development'`, so you don't need to worry about excluding them during a production build. ## Floating Mode Devtools will be mounted as a fixed, floating element in your app and provide a toggle in the corner of the screen to show and hide the devtools. This toggle state will be stored and remembered in localStorage across reloads. Place the following code as high in your Vue app as you can. The closer it is to the root of the page, the better it will work! ```vue ``` ### Options - `initialIsOpen: boolean` - Set this `true` if you want the dev tools to default to being open. - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right"` - Defaults to `bottom-right`. - The position of the React Query logo to open and close the devtools panel. - `position?: "top" | "bottom" | "left" | "right"` - Defaults to `bottom`. - The position of the React Query devtools panel. - `client?: QueryClient` - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}` - Use this to predefine some errors that can be triggered on your queries. The initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ## Embedded Mode Embedded mode will show the development tools as a fixed element in your application, so you can use our panel in your own development tools. Place the following code as high in your React app as you can. The closer it is to the root of the page, the better it will work! ```vue ``` ### Options - `style?: React.CSSProperties` - Custom styles for the devtools panel - Default: `{ height: '500px' }` - Example: `{ height: '100%' }` - Example: `{ height: '100%', width: '100%' }` - `onClose?: () => unknown` - Callback function that is called when the devtools panel is closed - `client?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. - `errorTypes?: { name: string; initializer: (query: Query) => TError}[]` - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. - `styleNonce?: string` - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. - `shadowDOMTarget?: ShadowRoot` - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. ## Traditional Devtools Vue Query will seamlessly integrate with the [Official Vue devtools](https://github.com/vuejs/devtools-next), adding custom inspector and timeline events. Devtool code will be treeshaken from production bundles by default. To make it work, you only need to enable it in the plugin options: ```ts app.use(VueQueryPlugin, { enableDevtoolsV6Plugin: true, }) ``` Both v6 and v7 versions of devtools are supported. ================================================ FILE: docs/framework/vue/graphql.md ================================================ --- id: graphql title: GraphQL ref: docs/framework/react/graphql.md replace: { 'React': 'Vue', 'react-query': 'vue-query' } --- [//]: # 'Codegen' [//]: # 'Codegen' ================================================ FILE: docs/framework/vue/guides/background-fetching-indicators.md ================================================ --- id: background-fetching-indicators title: Background Fetching Indicators ref: docs/framework/react/guides/background-fetching-indicators.md --- [//]: # 'Example' ```vue ``` [//]: # 'Example' [//]: # 'Example2' ```vue ``` [//]: # 'Example2' ================================================ FILE: docs/framework/vue/guides/caching.md ================================================ --- id: caching title: Caching Examples ref: docs/framework/react/guides/caching.md --- ================================================ FILE: docs/framework/vue/guides/custom-client.md ================================================ --- id: custom-client title: Custom Client --- ### Custom client Vue Query allows providing custom `QueryClient` for Vue context. It might be handy when you need to create `QueryClient` beforehand to integrate it with other libraries that do not have access to the Vue context. For this reason, `VueQueryPlugin` accepts either `QueryClientConfig` or `QueryClient` as a plugin options. If You provide `QueryClientConfig`, `QueryClient` instance will be created internally and provided to Vue context. ```tsx const vueQueryPluginOptions: VueQueryPluginOptions = { queryClientConfig: { defaultOptions: { queries: { staleTime: 3600 } }, }, } app.use(VueQueryPlugin, vueQueryPluginOptions) ``` ```tsx const myClient = new QueryClient(queryClientConfig) const vueQueryPluginOptions: VueQueryPluginOptions = { queryClient: myClient, } app.use(VueQueryPlugin, vueQueryPluginOptions) ``` ### Custom context key You can also customize the key under which `QueryClient` will be accessible in Vue context. This can be useful is you want to avoid name clashing between multiple apps on the same page with Vue2. It works both with default, and custom `QueryClient` ```tsx const vueQueryPluginOptions: VueQueryPluginOptions = { queryClientKey: 'Foo', } app.use(VueQueryPlugin, vueQueryPluginOptions) ``` ```tsx const myClient = new QueryClient(queryClientConfig) const vueQueryPluginOptions: VueQueryPluginOptions = { queryClient: myClient, queryClientKey: 'Foo', } app.use(VueQueryPlugin, vueQueryPluginOptions) ``` To use the custom client key, You have to provide it as a query options ```js useQuery({ queryKey: ['query1'], queryFn: fetcher, queryClientKey: 'foo', }) ``` Internally custom key will be combined with default query key as a suffix. But user do not have to worry about it. ```tsx const vueQueryPluginOptions: VueQueryPluginOptions = { queryClientKey: 'Foo', } app.use(VueQueryPlugin, vueQueryPluginOptions) // -> VUE_QUERY_CLIENT:Foo ``` ================================================ FILE: docs/framework/vue/guides/default-query-function.md ================================================ --- id: default-query-function title: Default Query Function ref: docs/framework/react/guides/default-query-function.md --- [//]: # 'Example' ```tsx // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const vueQueryPluginOptions: VueQueryPluginOptions = { queryClientConfig: { defaultOptions: { queries: { queryFn: defaultQueryFn } }, }, } app.use(VueQueryPlugin, vueQueryPluginOptions) // All you have to do now is pass a key! const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], }) ``` [//]: # 'Example' ================================================ FILE: docs/framework/vue/guides/dependent-queries.md ================================================ --- id: dependent-queries title: Dependent Queries ref: docs/framework/react/guides/dependent-queries.md --- [//]: # 'Example' ```js // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: () => getUserByEmail(email.value), }) const userId = computed(() => user.value?.id) const enabled = computed(() => !!user.value?.id) // Then get the user's projects const { isIdle, data: projects } = useQuery({ queryKey: ['projects', userId], queryFn: () => getProjectsByUser(userId.value), enabled, // The query will not execute until `enabled == true` }) ``` [//]: # 'Example' [//]: # 'Example2' ```tsx // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) const queries = computed(() => { return userIds.value.length ? userIds.value.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [] }) // Then get the users messages const usersMessages = useQueries({ queries, // if userIds.value is undefined or has no items, an empty array will be returned }) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/vue/guides/disabling-queries.md ================================================ --- id: disabling-queries title: Disabling/Pausing Queries ref: docs/framework/react/guides/disabling-queries.md --- [//]: # 'Example' ```vue ``` [//]: # 'Example' [//]: # 'Example2' ```vue ``` [//]: # 'Example2' [//]: # 'Example3' ```vue ``` [//]: # 'Example3' ================================================ FILE: docs/framework/vue/guides/does-this-replace-client-state.md ================================================ --- id: does-this-replace-client-state title: Does Vue Query replace Vuex, Pinia or other global state managers? ref: docs/framework/react/guides/does-this-replace-client-state.md replace: { 'Redux, MobX': 'Vuex, Pinia' } --- ================================================ FILE: docs/framework/vue/guides/filters.md ================================================ --- id: filters title: Filters ref: docs/framework/react/guides/filters.md --- ================================================ FILE: docs/framework/vue/guides/important-defaults.md ================================================ --- id: important-defaults title: Important Defaults ref: docs/framework/react/guides/important-defaults.md --- [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/vue/guides/infinite-queries.md ================================================ --- id: infinite-queries title: Infinite Queries ref: docs/framework/react/guides/infinite-queries.md --- [//]: # 'Example' ```vue ``` [//]: # 'Example' ================================================ FILE: docs/framework/vue/guides/initial-query-data.md ================================================ --- id: initial-query-data title: Initial Query Data ref: docs/framework/react/guides/initial-query-data.md --- [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/vue/guides/invalidations-from-mutations.md ================================================ --- id: invalidations-from-mutations title: Invalidations from Mutations ref: docs/framework/react/guides/invalidations-from-mutations.md --- [//]: # 'Example2' ```tsx import { useMutation, useQueryClient } from '@tanstack/vue-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/vue/guides/migrating-to-v5.md ================================================ --- id: migrating-to-tanstack-query-5 title: Migrating to TanStack Query v5 ref: docs/framework/react/guides/migrating-to-v5.md --- [//]: # 'FrameworkSpecificBreakingChanges' ## Vue Query Breaking Changes ### `useQueries` composable returns `ref` instead of `reactive` To fix compatibility with Vue 2, `useQueries` composable now returns `queries` array wrapped in `ref`. Previously `reactive` was returned which led to multiple problems: - User could spread return value loosing reactivity. - `readonly` wrapper used for return value was breaking Vue 2 reactivity detection mechanism. This was a silent issue in Vue 2.6, but appeared as error in Vue 2.7. - Vue 2 does not support arrays as a root value of `reactive`. With this change all of those issues are fixed. Also this aligns `useQueries` with other composables which return all of the values as `refs`. ### Vue v3.3 is now required To be able to provide new features following Vue releases, we now require Vue 3 to be at least in v3.3 version. Requirements for Vue 2.x remain unchanged. [//]: # 'FrameworkSpecificBreakingChanges' [//]: # 'FrameworkSpecificNewFeatures' ### Ability to run `vue-query` composables in `injectionContext` Previously `vue-query` composables could be run only within `setup` function of the component. We had an escape hatch in place to allow those hooks to be run anywhere if user would provide `queryClient` as a composable option. Now you can use `vue-query` composables in any function that supports `injectionContext`. Ex. router navigation guards. When using this new feature, make sure that `vue-query` composable is running within `effectScope`. Otherwise it might lead to memory leaks. We have added `dev-only` warnings to inform users about potential misusage. [//]: # 'FrameworkSpecificNewFeatures' ================================================ FILE: docs/framework/vue/guides/mutations.md ================================================ --- id: mutations title: Mutations ref: docs/framework/react/guides/mutations.md --- [//]: # 'Example' ```vue ``` [//]: # 'Example' [//]: # 'Info1' [//]: # 'Info1' [//]: # 'Example2' [//]: # 'Example2' [//]: # 'Example3' ```vue ``` [//]: # 'Example3' [//]: # 'Example11' ```js const client = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults({ mutationKey: ['todos'], mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) const vueQueryOptions: VueQueryPluginOptions = { queryClient: client, clientPersister: (queryClient) => { return persistQueryClient({ queryClient, persister: createAsyncStoragePersister({ storage: localStorage }), }) }, clientPersisterOnSuccess: (queryClient) => { queryClient.resumePausedMutations() }, } createApp(App).use(VueQueryPlugin, vueQueryOptions).mount('#app') ``` [//]: # 'Example11' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/vue/guides/network-mode.md ================================================ --- id: network-mode title: Network Mode ref: docs/framework/react/guides/network-mode.md --- ================================================ FILE: docs/framework/vue/guides/optimistic-updates.md ================================================ --- id: optimistic-updates title: Optimistic Updates ref: docs/framework/react/guides/optimistic-updates.md replace: { 'React': 'Vue' } --- ================================================ FILE: docs/framework/vue/guides/paginated-queries.md ================================================ --- id: paginated-queries title: Paginated / Lagged Queries ref: docs/framework/react/guides/paginated-queries.md --- [//]: # 'Example2' ```vue ``` [//]: # 'Example2' ================================================ FILE: docs/framework/vue/guides/parallel-queries.md ================================================ --- id: parallel-queries title: Parallel Queries ref: docs/framework/react/guides/parallel-queries.md --- [//]: # 'Example' ```vue ``` [//]: # 'Example' [//]: # 'Info' [//]: # 'Info' [//]: # 'Example2' ```js const users = computed(...) const queries = computed(() => users.value.map(user => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }) ); const userQueries = useQueries({queries: queries}) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/vue/guides/placeholder-query-data.md ================================================ --- id: placeholder-query-data title: Placeholder Query Data ref: docs/framework/react/guides/placeholder-query-data.md --- [//]: # 'ExampleValue' ```tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) ``` [//]: # 'ExampleValue' [//]: # 'Memoization' [//]: # 'Memoization' [//]: # 'ExampleCache' ```tsx const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) ``` [//]: # 'ExampleCache' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/vue/guides/prefetching.md ================================================ --- id: prefetching title: Prefetching --- If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, you can use the `prefetchQuery` method to prefetch the results of a query to be placed into the cache: [//]: # 'ExamplePrefetching' ```tsx const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } ``` [//]: # 'ExamplePrefetching' - If **fresh** data for this query is already in the cache, the data will not be fetched - If a `staleTime` is passed eg. `prefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` and the data is older than the specified `staleTime`, the query will be fetched - If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime`. ## Prefetching Infinite Queries Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option, in which case you also have to provide a `getNextPageParam` function: [//]: # 'ExampleInfiniteQuery' ```tsx const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } ``` [//]: # 'ExampleInfiniteQuery' The above code will try to prefetch 3 pages in order, and `getNextPageParam` will be executed for each page to determine the next page to prefetch. If `getNextPageParam` returns `undefined`, the prefetching will stop. ================================================ FILE: docs/framework/vue/guides/queries.md ================================================ --- id: queries title: Queries ref: docs/framework/react/guides/queries.md --- [//]: # 'Example' ```ts import { useQuery } from '@tanstack/vue-query' const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) ``` [//]: # 'Example' [//]: # 'Example3' ```vue ``` [//]: # 'Example3' [//]: # 'Example4' ```vue ``` [//]: # 'Example4' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/framework/vue/guides/query-cancellation.md ================================================ --- id: query-cancellation title: Query Cancellation ref: docs/framework/react/guides/query-cancellation.md --- [//]: # 'Example7' ```ts const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() function onButtonClick() { queryClient.cancelQueries({ queryKey: ['todos'] }) } ``` [//]: # 'Example7' ================================================ FILE: docs/framework/vue/guides/query-functions.md ================================================ --- id: query-functions title: Query Functions ref: docs/framework/react/guides/query-functions.md --- [//]: # 'Example4' ```js const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ``` [//]: # 'Example4' ================================================ FILE: docs/framework/vue/guides/query-invalidation.md ================================================ --- id: query-invalidation title: Query Invalidation ref: docs/framework/react/guides/query-invalidation.md replace: { 'react-query': 'vue-query' } --- ================================================ FILE: docs/framework/vue/guides/query-keys.md ================================================ --- id: query-keys title: Query Keys ref: docs/framework/react/guides/query-keys.md --- [//]: # 'Example5' ```ts import type { Ref } from 'vue' function useTodos(todoId: Ref) { const queryKey = ['todos', todoId] return useQuery({ queryKey, queryFn: () => fetchTodoById(todoId.value), }) } ``` [//]: # 'Example5' ================================================ FILE: docs/framework/vue/guides/query-options.md ================================================ --- id: query-options title: Query Options ref: docs/framework/react/guides/query-options.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/guides/query-retries.md ================================================ --- id: query-retries title: Query Retries ref: docs/framework/react/guides/query-retries.md replace: { 'Provider': 'Plugin' } --- [//]: # 'Example' ```tsx import { useQuery } from '@tanstack/vue-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) ``` [//]: # 'Example' [//]: # 'Example2' ```ts import { VueQueryPlugin } from '@tanstack/vue-query' const vueQueryPluginOptions = { queryClientConfig: { defaultOptions: { queries: { retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }, }, }, } app.use(VueQueryPlugin, vueQueryPluginOptions) ``` [//]: # 'Example2' ================================================ FILE: docs/framework/vue/guides/scroll-restoration.md ================================================ --- id: scroll-restoration title: Scroll Restoration ref: docs/framework/react/guides/scroll-restoration.md --- ================================================ FILE: docs/framework/vue/guides/ssr.md ================================================ --- id: ssr title: SSR --- Vue Query supports prefetching multiple queries on the server and then _dehydrating_ those queries to the queryClient. This means the server can prerender markup that is immediately available on page load and as soon as JS is available, Vue Query can upgrade or _hydrate_ those queries with the full functionality of the library. This includes refetching those queries on the client if they have become stale since the time they were rendered on the server. ## Using Nuxt.js ### Nuxt 3 First create `vue-query.ts` file in your `plugins` directory with the following content: ```ts import type { DehydratedState, VueQueryPluginOptions, } from '@tanstack/vue-query' import { VueQueryPlugin, QueryClient, hydrate, dehydrate, } from '@tanstack/vue-query' // Nuxt 3 app aliases import { defineNuxtPlugin, useState } from '#imports' export default defineNuxtPlugin((nuxt) => { const vueQueryState = useState('vue-query') // Modify your Vue Query global settings here const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5000 } }, }) const options: VueQueryPluginOptions = { queryClient } nuxt.vueApp.use(VueQueryPlugin, options) if (import.meta.server) { nuxt.hooks.hook('app:rendered', () => { vueQueryState.value = dehydrate(queryClient) }) } if (import.meta.client) { hydrate(queryClient, vueQueryState.value) } }) ``` Now you are ready to prefetch some data in your pages with `onServerPrefetch`. - Prefetch all the queries that you need with `queryClient.prefetchQuery` or `suspense` ```ts export default defineComponent({ setup() { const { data, suspense } = useQuery({ queryKey: ['test'], queryFn: fetcher, }) onServerPrefetch(async () => { await suspense() }) return { data } }, }) ``` ### Nuxt 2 First create `vue-query.js` file in your `plugins` directory with the following content: ```js import Vue from 'vue' import { VueQueryPlugin, QueryClient, hydrate } from '@tanstack/vue-query' export default (context) => { // Modify your Vue Query global settings here const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5000 } }, }) if (process.server) { context.ssrContext.VueQuery = queryClient } if (process.client) { Vue.use(VueQueryPlugin, { queryClient }) if (context.nuxtState && context.nuxtState.vueQueryState) { hydrate(queryClient, context.nuxtState.vueQueryState) } } } ``` Add this plugin to your `nuxt.config.js` ```js module.exports = { ... plugins: ['~/plugins/vue-query.js'], } ``` Now you are ready to prefetch some data in your pages with `onServerPrefetch`. - Use `useContext` to get nuxt context - Use `useQueryClient` to get server-side instance of `queryClient` - Prefetch all the queries that you need with `queryClient.prefetchQuery` or `suspense` - Dehydrate `queryClient` to the `nuxtContext` ```vue // pages/todos.vue ``` As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` or `suspense` for a specific query. ## Using Vite SSR Sync VueQuery client state with [vite-ssr](https://github.com/frandiox/vite-ssr) in order to serialize it in the DOM: ```js // main.js (entry point) import App from './App.vue' import viteSSR from 'vite-ssr/vue' import { QueryClient, VueQueryPlugin, hydrate, dehydrate, } from '@tanstack/vue-query' export default viteSSR(App, { routes: [] }, ({ app, initialState }) => { // -- This is Vite SSR main hook, which is called once per request // Create a fresh VueQuery client const queryClient = new QueryClient() // Sync initialState with the client state if (import.meta.env.SSR) { // Indicate how to access and serialize VueQuery state during SSR initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) } } else { // Reuse the existing state in the browser hydrate(queryClient, initialState.vueQueryState) } // Mount and provide the client to the app components app.use(VueQueryPlugin, { queryClient }) }) ``` Then, call VueQuery from any component using Vue's `onServerPrefetch`: ```html ``` ## Tips, Tricks and Caveats ### Only successful queries are included in dehydration Any query with an error is automatically excluded from dehydration. This means that the default behavior is to pretend these queries were never loaded on the server, usually showing a loading state instead, and retrying the queries on the queryClient. This happens regardless of error. Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `fetchQuery` and catch any errors to handle those manually. ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was `dataUpdatedAt`. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because `staleTime` defaults to `0`, queries will be refetched in the background on page load by default. You might want to use a higher `staleTime` to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the `staleTime` of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the `QueryClient` for every request, Vue Query creates the isolated cache for this client, which is preserved in memory for the `gcTime` period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, `gcTime` defaults to `Infinity` which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity `gcTime` then you will be responsible for clearing the cache early. To clear the cache after it is not needed and to lower memory consumption, you can add a call to [`queryClient.clear()`](../../../reference/QueryClient/#queryclientclear) after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller `gcTime`. ================================================ FILE: docs/framework/vue/guides/suspense.md ================================================ --- id: suspense title: Suspense (experimental) --- > NOTE: Suspense mode for Vue Query is experimental, same as Vue's Suspense itself. These APIs WILL change and should not be used in production unless you lock both your Vue and Vue Query versions to patch-level versions that are compatible with each other. Vue Query can also be used with Vue's new [Suspense](https://vuejs.org/guide/built-ins/suspense.html) API's. To do that you need to wrap your suspendable component with `Suspense` component provided by Vue ```vue ``` And change your `setup` function in suspendable component to be `async`. Then you can use async `suspense` function that is provided by `vue-query`. ```vue ``` ## Fetch-on-render vs Render-as-you-fetch Out of the box, Vue Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing [Prefetching](./prefetching) on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ================================================ FILE: docs/framework/vue/guides/testing.md ================================================ --- id: testing title: Testing --- ================================================ FILE: docs/framework/vue/guides/updates-from-mutation-responses.md ================================================ --- id: updates-from-mutation-responses title: Updates from Mutation Responses ref: docs/framework/react/guides/updates-from-mutation-responses.md --- ================================================ FILE: docs/framework/vue/guides/window-focus-refetching.md ================================================ --- id: window-focus-refetching title: Window Focus Refetching ref: docs/framework/react/guides/window-focus-refetching.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- [//]: # 'Example' ```js const vueQueryPluginOptions: VueQueryPluginOptions = { queryClientConfig: { defaultOptions: { queries: { refetchOnWindowFocus: false, }, }, }, } app.use(VueQueryPlugin, vueQueryPluginOptions) ``` [//]: # 'Example' [//]: # 'ReactNative' [//]: # 'ReactNative' ================================================ FILE: docs/framework/vue/installation.md ================================================ --- id: installation title: Installation --- You can install Vue Query via [NPM](https://npmjs.com). ### NPM ```bash npm i @tanstack/vue-query ``` or ```bash pnpm add @tanstack/vue-query ``` or ```bash yarn add @tanstack/vue-query ``` or ```bash bun add @tanstack/vue-query ``` > Wanna give it a spin before you download? Try out the [basic](./examples/basic) example! Vue Query is compatible with Vue 2.x and 3.x > If you are using Vue 2.6, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api) ### Vue Query Initialization Before using Vue Query, you need to initialize it using `VueQueryPlugin` ```tsx import { VueQueryPlugin } from '@tanstack/vue-query' app.use(VueQueryPlugin) ``` ### Use of Composition API with ` ``` ================================================ FILE: docs/framework/vue/overview.md ================================================ --- id: overview title: Overview ref: docs/framework/react/overview.md replace: { 'React': 'Vue', 'react-query': 'vue-query' } --- [//]: # 'Example' [//]: # 'Example' [//]: # 'Materials' ## You talked me into it, so what now? - Learn Vue Query at your own pace with our amazingly thorough [Walkthrough Guide](./installation) and [API Reference](./reference/useQuery) [//]: # 'Materials' ================================================ FILE: docs/framework/vue/plugins/broadcastQueryClient.md ================================================ --- id: broadcastQueryClient title: broadcastQueryClient (Experimental) ref: docs/framework/react/plugins/broadcastQueryClient.md --- ================================================ FILE: docs/framework/vue/plugins/createPersister.md ================================================ --- id: createPersister title: experimental_createQueryPersister --- ## Installation This utility comes as a separate package and is available under the `'@tanstack/query-persist-client-core'` import. ```bash npm install @tanstack/query-persist-client-core ``` or ```bash pnpm add @tanstack/query-persist-client-core ``` or ```bash yarn add @tanstack/query-persist-client-core ``` or ```bash bun add @tanstack/query-persist-client-core ``` ## Usage - Import the `experimental_createQueryPersister` function - Create a new `experimental_createQueryPersister` - you can pass any `storage` to it that adheres to the `AsyncStorage` interface - Pass that `persister` as an option to your Query. This can be done either by passing it to the `defaultOptions` of the `QueryClient` or to any `useQuery` hook instance. - If you pass this `persister` as `defaultOptions`, all queries will be persisted to the provided `storage`. You can additionally narrow this down by passing `filters`. In contrast to the `persistClient` plugin, this will not persist the whole query client as a single item, but each query separately. As a key, the query hash is used. - If you provide this `persister` to a single `useQuery` hook, only this Query will be persisted. - Note: `queryClient.setQueryData()` operations are not persisted, this means that if you perform an optimistic update and refresh the page before the query has been invalidated, your changes to the query data will be lost. See https://github.com/TanStack/query/issues/6310 This way, you do not need to store whole `QueryClient`, but choose what is worth to be persisted in your application. Each query is lazily restored (when the Query is first used) and persisted (after each run of the `queryFn`), so it does not need to be throttled. `staleTime` is also respected after restoring the Query, so if data is considered `stale`, it will be refetched immediately after restoring. If data is `fresh`, the `queryFn` will not run. Garbage collecting a Query from memory **does not** affect the persisted data. That means Queries can be kept in memory for a shorter period of time to be more **memory efficient**. If they are used the next time, they will just be restored from the persistent storage again. ```tsx import { QueryClient } from '@tanstack/vue-query' import { experimental_createQueryPersister } from '@tanstack/query-persist-client-core' const persister = experimental_createQueryPersister({ storage: AsyncStorage, maxAge: 1000 * 60 * 60 * 12, // 12 hours }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 30, // 30 seconds persister: persister.persisterFn, }, }, }) ``` ### Adapted defaults The `createPersister` plugin technically wraps the `queryFn`, so it doesn't restore if the `queryFn` doesn't run. In that way, it acts as a caching layer between the Query and the network. Thus, the `networkMode` defaults to `'offlineFirst'` when a persister is used, so that restoring from the persistent storage can also happen even if there is no network connection. ## Additional utilities Invoking `experimental_createQueryPersister` returns additional utilities in addition to `persisterFn` for easier implementation of userland functionalities. ### `persistQueryByKey(queryKey: QueryKey, queryClient: QueryClient): Promise` This function will persist `Query` to storage and key defined when creating persister. This utility might be used along `setQueryData` to persist optimistic update to storage without waiting for invalidation. ```tsx const persister = experimental_createQueryPersister({ storage: AsyncStorage, maxAge: 1000 * 60 * 60 * 12, // 12 hours }) const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { ... // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // And persist it to storage persister.persistQueryByKey(['todos'], queryClient) ... }, }) ``` ### `retrieveQuery(queryHash: string): Promise` This function would attempt to retrieve persisted query by `queryHash`. If `query` is `expired`, `busted` or `malformed` it would be removed from the storage instead, and `undefined` would be returned. ### `persisterGc(): Promise` This function can be used to sporadically clean up stoage from `expired`, `busted` or `malformed` entries. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. ### `restoreQueries(queryClient: QueryClient, filters): Promise` This function can be used to restore queries that are currently stored by persister. For example when your app is starting up in offline mode, or you want all or only specific data from previous session to be immediately available without intermediate `loading` state. The filter object supports the following properties: - `queryKey?: QueryKey` - Set this property to define a query key to match on. - `exact?: boolean` - If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed. For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. ## API ### `experimental_createQueryPersister` ```tsx experimental_createQueryPersister(options: StoragePersisterOptions) ``` #### `Options` ```tsx export interface StoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. */ storage: AsyncStorage | Storage | undefined | null /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (persistedQuery: PersistedQuery) => string /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: string) => PersistedQuery /** * A unique string that can be used to forcefully invalidate existing caches, * if they do not share the same buster string */ buster?: string /** * The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be discarded * @default 24 hours */ maxAge?: number /** * Prefix to be used for storage key. * Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`. */ prefix?: string /** * If set to `true`, the query will refetch on successful query restoration if the data is stale. * If set to `false`, the query will not refetch on successful query restoration. * If set to `'always'`, the query will always refetch on successful query restoration. * Defaults to `true`. */ refetchOnRestore?: boolean | 'always' /** * Filters to narrow down which Queries should be persisted. */ filters?: QueryFilters } interface AsyncStorage { getItem: (key: string) => MaybePromise setItem: (key: string, value: TStorageValue) => MaybePromise removeItem: (key: string) => MaybePromise entries?: () => MaybePromise> } ``` The default options are: ```tsx { prefix = 'tanstack-query', maxAge = 1000 * 60 * 60 * 24, serialize = JSON.stringify, deserialize = JSON.parse, refetchOnRestore = true, } ``` ================================================ FILE: docs/framework/vue/quick-start.md ================================================ --- id: quick-start title: Quick Start ref: docs/framework/react/quick-start.md replace: { 'React': 'Vue', 'react-query': 'vue-query' } --- [//]: # 'Example' If you're looking for a fully functioning example, please have a look at our [basic codesandbox example](./examples/basic) ```vue ``` [//]: # 'Example' ================================================ FILE: docs/framework/vue/reactivity.md ================================================ --- id: reactivity title: Reactivity --- Vue uses the [the signals paradigm](https://vuejs.org/guide/extras/reactivity-in-depth.html#connection-to-signals) to handle and track reactivity. A key feature of this system is the reactive system only triggers updates on specifically watched reactive properties. A consequence of this is you also need to ensure that the queries are updated when values they consume are updated. # Keeping Queries Reactive When creating a composable for a query your first choice may be to write it like so: ```ts export function useUserProjects(userId: string) { return useQuery( queryKey: ['userProjects', userId], queryFn: () => api.fetchUserProjects(userId), ); } ``` We might consume this composable like so: ```ts // Reactive user ID ref. const userId = ref('1') // Fetches the user 1's projects. const { data: projects } = useUserProjects(userId.value) const onChangeUser = (newUserId: string) => { // Edits the userId, but the query will not re-fetch. userId.value = newUserId } ``` This code will not work as intended. This is because we are extracting the value from the userId ref directly. Vue-query is not tracking the `userId` `ref` so it has no way of knowing when the value changes. Luckily, the fix for this is trivial. The value must be made trackable in the query key. We can Just accept the `ref` directly in the composable and place it in the query key: ```ts export function useUserProjects(userId: Ref) { return useQuery( queryKey: ['userProjects', userId], queryFn: () => api.fetchUserProjects(userId.value), ); } ``` Now the query will re-fetch when the `userId` changes. ```ts const onChangeUser = (newUserId: string) => { // Query refetches data with new user ID! userId.value = newUserId } ``` In vue query any reactive properties within a query key are tracked for changes automatically. This allows vue-query to refetch data whenever the parameters for a given request change. ## Accounting for Non-Reactive Queries While far less likely, sometimes passing non-reactive variables is intentional. For example, some entities only need to be fetched once and don't need tracking or we invalidate a mutation a query options object after a mutation. If we use our custom composable defined above the usage in this case feels a bit off: ```ts const { data: projects } = useUserProjects(ref('1')) ``` We have to create an intermediate `ref` just to make the parameter type-compatible. We can do better here. Let's instead update our composable to accept both plain values and reactive values: ```ts export function useUserProjects(userId: MaybeRef) { return useQuery( queryKey: ['userProjects', userId], queryFn: () => api.fetchUserProjects(toValue(userId)), ); } ``` Now we can use the composable with both plain values and refs: ```ts // Fetches the user 1's projects, userId is not expected to change. const { data: projects } = useUserProjects('1') // Fetches the user 1's projects, queries will react to changes on userId. const userId = ref('1') // Make some changes to userId... // Query re-fetches based on any changes to userId. const { data: projects } = useUserProjects(userId) ``` ## Using Derived State inside Queries It's quite common to derive some new reactive state from another source of reactive state. Commonly, this problem manifests in situations where you deal with component props. Let's assume our `userId` is a prop passed to a component: ```vue ``` You may be tempted to use the prop directly in the query like so: ```ts // Won't react to changes in props.userId. const { data: projects } = useUserProjects(props.userId) ``` However, similar to the first example, this is not reactive. Property access on `reactive` variables causes reactivity to be lost. We can fix this by making this derived state reactive via a `computed`: ```ts const userId = computed(() => props.userId) // Reacts to changes in props.userId. const { data: projects } = useUserProjects(userId) ``` This works as expected, however, this solution isn't always the most optimal. Aside from the introduction of an intermediate variable, we also create a memoized value that is somewhat unnecessary. For trivial cases of simple property access `computed` is an optimization with no real benefit. In these cases a more appropriate solution is to use [reactive getters](https://blog.vuejs.org/posts/vue-3-3#better-getter-support-with-toref-and-tovalue). Reactive getters are simply functions that return a value based on some reactive state, similar to how `computed` works. Unlike `computed`, reactive getters do not memoize their values so it makes it a good candidate for simple property access. Let's once again refactor our composable, but this time we'll have it accept a `ref`, plain value, or a reactive getter: ```ts export function useUserProjects(userId: MaybeRefOrGetter) { ... } ``` Let's adjust our usage and now use a reactive getter: ```ts // Reacts to changes in props.userId. No `computed` needed! const { data: projects } = useUserProjects(() => props.userId) ``` This gives us a terse syntax and the reactivity we need without any unneeded memoization overhead. ## Other tracked Query Options Above, we only touched one query option that tracks reactive dependencies. However, in addition to `queryKey`, `enabled` also allows the use of reactive values. This comes in handy in situations where you want to control the fetching of a query based on some derived state: ```ts export function useUserProjects(userId: MaybeRef) { return useQuery( queryKey: ['userProjects', userId], queryFn: () => api.fetchUserProjects(toValue(userId)), enabled: () => userId.value === activeUserId.value, ); } ``` More details on this option can be found on the [useQuery reference](./reference/useQuery.md) page. ## Immutability Results from `useQuery` are always immutable. This is necessary for performance and caching purposes. If you need to mutate a value returned from `useQuery`, you must create a copy of the data. One implication of this design is that passing values from `useQuery` to a two-way binding such as `v-model` will not work. You must create a mutable copy of the data before attempting to update it in place. # Key Takeaways - `enabled` and `queryKey` are the two query options that can accept reactive values. - Pass query option that accept all three types of values in Vue: refs, plain values, and reactive getters. - If you expect a query to react to changes based on the values it consumes, ensure that the values are reactive. (i.e. pass in refs directly to the query, or use reactive getters) - If you don't need a query to be reactive pass in a plain value. - For trivial derived state such as property access consider using a reactive getter in place of a `computed`. - Results from `useQuery` are always immutable. ================================================ FILE: docs/framework/vue/reference/hydration.md ================================================ --- id: hydration title: hydration ref: docs/framework/react/reference/hydration.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- [//]: # 'HydrationBoundary' [//]: # 'HydrationBoundary' ================================================ FILE: docs/framework/vue/reference/infiniteQueryOptions.md ================================================ --- id: infiniteQueryOptions title: infiniteQueryOptions ref: docs/framework/react/reference/infiniteQueryOptions.md --- ================================================ FILE: docs/framework/vue/reference/queryOptions.md ================================================ --- id: queryOptions title: queryOptions ref: docs/framework/react/reference/queryOptions.md --- ================================================ FILE: docs/framework/vue/reference/useInfiniteQuery.md ================================================ --- id: useInfiniteQuery title: useInfiniteQuery ref: docs/framework/react/reference/useInfiniteQuery.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useIsFetching.md ================================================ --- id: useIsFetching title: useIsFetching ref: docs/framework/react/reference/useIsFetching.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useIsMutating.md ================================================ --- id: useIsMutating title: useIsMutating ref: docs/framework/react/reference/useIsMutating.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useMutation.md ================================================ --- id: useMutation title: useMutation ref: docs/framework/react/reference/useMutation.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useMutationState.md ================================================ --- id: useMutationState title: useMutationState ref: docs/framework/react/reference/useMutationState.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useQueries.md ================================================ --- id: useQueries title: useQueries ref: docs/framework/react/reference/useQueries.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useQuery.md ================================================ --- id: useQuery title: useQuery ref: docs/framework/react/reference/useQuery.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/reference/useQueryClient.md ================================================ --- id: useQueryClient title: useQueryClient ref: docs/framework/react/reference/useQueryClient.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- ================================================ FILE: docs/framework/vue/typescript.md ================================================ --- id: typescript title: TypeScript ref: docs/framework/react/typescript.md replace: { 'React': 'Vue', '@tanstack/react-query': '@tanstack/vue-query', 'react-query package version': 'vue-query package version', } --- [//]: # 'TypeInference1' ```tsx const { data } = useQuery({ // ^? const data: Ref | Ref queryKey: ['test'], queryFn: () => Promise.resolve(5), }) ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPQBuOAtAEcc+KgFgAUBNYRm8JABN6DInAC8KDNlx4AFAglw4nTocMA9APwG4Q7QGl0eAFxwA2lRjoWVALoAaa1t8ADFGFx0ASjUAPjgABXIQYAwAOigvCAAbbnQdAFYIgPFCCKA) [//]: # 'TypeInference1' [//]: # 'TypeInference2' ```tsx const { data } = useQuery({ // ^? const data: Ref | Ref queryKey: ['test'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPQBuOAtAEcc+KgFgAUBNYRm8JABN6DInAC8KDNlx4AFAglw4nTodNwAegH4DcIdoDS6PAC44AbSox0LKgF0ANDZ2+ABijK46AJRqAHxwAArkIMAYAHRQ3hAANtzoOgCskYHihhhZ6KwwEYoM0apxNfSpMBAAyjBQwIwA5lHFhJFAA) [//]: # 'TypeInference2' [//]: # 'TypeInference3' ```tsx const fetchGroups = (): Promise => axios.get('/groups').then((response) => response.data) const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const data: Ref | Ref ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPQBuOAtAEcc+KgFgAUKEiw49AB7AIqUuUpV5i1GPESYeMOjgBxcsjBwAvIjjAAJgC44jZCABGuIhImsIzeCXQYVgALEwgzZSsACgBKRwAFVWAMAB4wswBtAF0APksciThZBSUAOgBzQKiqTnLTMC0Y0phg9EYoqKh0VEhmdBj8uC6e3wxS23oGGK9xHz9rCYYiSxQMbFw8KKQhDYBpdDxHDKo68IaqLIAaOB38ADFGRwCg0PrlQmnxTk4i37gAPQA-EA) [//]: # 'TypeInference3' [//]: # 'TypeNarrowing' ```tsx const { data, isSuccess } = reactive( useQuery({ queryKey: ['test'], queryFn: () => Promise.resolve(5), }), ) if (isSuccess) { data // ^? const data: number } ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPQBuOAtAEcc+KgFgAUKEixEcKOnqsYwbuiKlylKr3RUA3BImsIzeEgAm9BgBo4wVAGVkrVulSp1AXjkKlK9AAUaFjCeAEA2lQwbjBUALq2AQCUcJ4AfHAACpr26AB08qgQADaqAQCsSVWGkiRwAfZOLm6oKQgScJ1wlgwSnJydAHoA-BKEEkA) [//]: # 'TypeNarrowing' [//]: # 'TypingError' ```tsx const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: Ref if (error.value instanceof Error) { error.value // ^? const error: Error } ``` [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPRTr2swBaAI458VALAAoUJFhx6AD2ARUpcpSqLlqCZKkw8YdHADi5ZGDgBeRHGAATAFxxGyEACNcRKVNYRm8CToMKwAFmYQFqo2ABQAlM4ACurAGAA8ERYA2gC6AHzWBVoqAHQA5sExVJxl5mA6cSUwoeiMMTyokMzGVgUdXRgl9vQMcT6SfgG2uORQRNYoGNi4eDFIIisA0uh4zllUtZH1VDkANHAb+ABijM5BIeF1qoRjkpyccJ9fAHoA-OPAEhwGLFVAlVIAQSUKgAolBZjEZtA4nFEFJPkioOi4O84H8pIQgA) [//]: # 'TypingError' [//]: # 'TypingError2' [//]: # 'TypingError2' [//]: # 'TypingError3' [//]: # 'TypingError3' [//]: # 'RegisterErrorType' ```tsx import '@tanstack/vue-query' declare module '@tanstack/vue-query' { interface Register { // Use unknown so call sites must narrow explicitly. defaultError: unknown } } const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: unknown | null ``` [//]: # 'RegisterErrorType' [//]: # 'TypingMeta' [//]: # 'TypingMeta' [//]: # 'TypingQueryOptions' [//]: # 'TypingQueryOptions' [//]: # 'Materials' [//]: # 'Materials' ================================================ FILE: docs/reference/InfiniteQueryObserver.md ================================================ --- id: InfiniteQueryObserver title: InfiniteQueryObserver --- ## `InfiniteQueryObserver` The `InfiniteQueryObserver` can be used to observe and switch between infinite queries. ```tsx const observer = new InfiniteQueryObserver(queryClient, { queryKey: ['posts'], queryFn: fetchPosts, getNextPageParam: (lastPage, allPages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor, }) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() }) ``` **Options** The options for the `InfiniteQueryObserver` are exactly the same as those of [`useInfiniteQuery`](../framework/react/reference/useInfiniteQuery). ================================================ FILE: docs/reference/MutationCache.md ================================================ --- id: MutationCache title: MutationCache --- The `MutationCache` is the storage for mutations. **Normally, you will not interact with the MutationCache directly and instead use the `QueryClient`.** ```tsx import { MutationCache } from '@tanstack/react-query' const mutationCache = new MutationCache({ onError: (error) => { console.log(error) }, onSuccess: (data) => { console.log(data) }, }) ``` Its available methods are: - [`getAll`](#mutationcachegetall) - [`subscribe`](#mutationcachesubscribe) - [`clear`](#mutationcacheclear) **Options** - `onError?: (error: unknown, variables: unknown, onMutateResult: unknown, mutation: Mutation, mutationFnContext: MutationFunctionContext) => Promise | unknown` - Optional - This function will be called if some mutation encounters an error. - If you return a Promise from it, it will be awaited - `onSuccess?: (data: unknown, variables: unknown, onMutateResult: unknown, mutation: Mutation, mutationFnContext: MutationFunctionContext) => Promise | unknown` - Optional - This function will be called if some mutation is successful. - If you return a Promise from it, it will be awaited - `onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, onMutateResult: unknown, mutation: Mutation, mutationFnContext: MutationFunctionContext) => Promise | unknown` - Optional - This function will be called if some mutation is settled (either successful or errored). - If you return a Promise from it, it will be awaited - `onMutate?: (variables: unknown, mutation: Mutation, mutationFnContext: MutationFunctionContext) => Promise | unknown` - Optional - This function will be called before some mutation executes. - If you return a Promise from it, it will be awaited ## Global callbacks The `onError`, `onSuccess`, `onSettled` and `onMutate` callbacks on the MutationCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because: - `defaultOptions` can be overridden by each Mutation - the global callbacks will **always** be called. - `onMutate` does not allow returning a result. ## `mutationCache.getAll` `getAll` returns all mutations within the cache. > Note: This is not typically needed for most applications, but can come in handy when needing more information about a mutation in rare scenarios ```tsx const mutations = mutationCache.getAll() ``` **Returns** - `Mutation[]` - Mutation instances from the cache ## `mutationCache.subscribe` The `subscribe` method can be used to subscribe to the mutation cache as a whole and be informed of safe/known updates to the cache like mutation states changing or mutations being updated, added or removed. ```tsx const callback = (event) => { console.log(event.type, event.mutation) } const unsubscribe = mutationCache.subscribe(callback) ``` **Options** - `callback: (mutation?: MutationCacheNotifyEvent) => void` - This function will be called with the mutation cache any time it is updated. **Returns** - `unsubscribe: Function => void` - This function will unsubscribe the callback from the mutation cache. ## `mutationCache.clear` The `clear` method can be used to clear the cache entirely and start fresh. ```tsx mutationCache.clear() ``` ================================================ FILE: docs/reference/QueriesObserver.md ================================================ --- id: QueriesObserver title: QueriesObserver --- ## `QueriesObserver` The `QueriesObserver` can be used to observe multiple queries. ```tsx const observer = new QueriesObserver(queryClient, [ { queryKey: ['post', 1], queryFn: fetchPost }, { queryKey: ['post', 2], queryFn: fetchPost }, ]) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() }) ``` **Options** The options for the `QueriesObserver` are exactly the same as those of [`useQueries`](../framework/react/reference/useQueries). ================================================ FILE: docs/reference/QueryCache.md ================================================ --- id: QueryCache title: QueryCache --- The `QueryCache` is the storage mechanism for TanStack Query. It stores all the data, meta information and state of queries it contains. **Normally, you will not interact with the QueryCache directly and instead use the `QueryClient` for a specific cache.** ```tsx import { QueryCache } from '@tanstack/react-query' const queryCache = new QueryCache({ onError: (error) => { console.log(error) }, onSuccess: (data) => { console.log(data) }, onSettled: (data, error) => { console.log(data, error) }, }) const query = queryCache.find(['posts']) ``` Its available methods are: - [`queryCache.find`](#querycachefind) - [`queryCache.findAll`](#querycachefindall) - [`queryCache.subscribe`](#querycachesubscribe) - [`queryCache.clear`](#querycacheclear) - [Further reading](#further-reading) **Options** - `onError?: (error: unknown, query: Query) => void` - Optional - This function will be called if some query encounters an error. - `onSuccess?: (data: unknown, query: Query) => void` - Optional - This function will be called if some query is successful. - `onSettled?: (data: unknown | undefined, error: unknown | null, query: Query) => void` - Optional - This function will be called if some query is settled (either successful or errored). ## `queryCache.find` `find` is a slightly more advanced synchronous method that can be used to get an existing query instance from the cache. This instance not only contains **all** the state for the query, but all of the instances, and underlying guts of the query as well. If the query does not exist, `undefined` will be returned. > Note: This is not typically needed for most applications, but can come in handy when needing more information about a query in rare scenarios (eg. Looking at the query.state.dataUpdatedAt timestamp to decide whether a query is fresh enough to be used as an initial value) ```tsx const query = queryCache.find(queryKey) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters#query-filters) **Returns** - `Query` - The query instance from the cache ## `queryCache.findAll` `findAll` is even more advanced synchronous method that can be used to get existing query instances from the cache that partially match query key. If queries do not exist, empty array will be returned. > Note: This is not typically needed for most applications, but can come in handy when needing more information about a query in rare scenarios ```tsx const queries = queryCache.findAll(queryKey) ``` **Options** - `queryKey?: QueryKey`: [Query Keys](../framework/react/guides/query-keys.md) - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) **Returns** - `Query[]` - Query instances from the cache ## `queryCache.subscribe` The `subscribe` method can be used to subscribe to the query cache as a whole and be informed of safe/known updates to the cache like query states changing or queries being updated, added or removed ```tsx const callback = (event) => { console.log(event.type, event.query) } const unsubscribe = queryCache.subscribe(callback) ``` **Options** - `callback: (event: QueryCacheNotifyEvent) => void` - This function will be called with the query cache any time it is updated via its tracked update mechanisms (eg, `query.setState`, `queryClient.removeQueries`, etc). Out of scope mutations to the cache are not encouraged and will not fire subscription callbacks **Returns** - `unsubscribe: Function => void` - This function will unsubscribe the callback from the query cache. ## `queryCache.clear` The `clear` method can be used to clear the cache entirely and start fresh. ```tsx queryCache.clear() ``` [//]: # 'Materials' ## Further reading To get a better understanding how the QueryCache works internally, have a look at [the Inside React Query article by TkDodo](https://tkdodo.eu/blog/inside-react-query). [//]: # 'Materials' ================================================ FILE: docs/reference/QueryClient.md ================================================ --- id: QueryClient title: QueryClient --- ## `QueryClient` The `QueryClient` can be used to interact with a cache: ```tsx import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity, }, }, }) await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts }) ``` Its available methods are: - [`queryClient.fetchQuery`](#queryclientfetchquery) - [`queryClient.fetchInfiniteQuery`](#queryclientfetchinfinitequery) - [`queryClient.prefetchQuery`](#queryclientprefetchquery) - [`queryClient.prefetchInfiniteQuery`](#queryclientprefetchinfinitequery) - [`queryClient.getQueryData`](#queryclientgetquerydata) - [`queryClient.ensureQueryData`](#queryclientensurequerydata) - [`queryClient.ensureInfiniteQueryData`](#queryclientensureinfinitequerydata) - [`queryClient.getQueriesData`](#queryclientgetqueriesdata) - [`queryClient.setQueryData`](#queryclientsetquerydata) - [`queryClient.getQueryState`](#queryclientgetquerystate) - [`queryClient.setQueriesData`](#queryclientsetqueriesdata) - [`queryClient.invalidateQueries`](#queryclientinvalidatequeries) - [`queryClient.refetchQueries`](#queryclientrefetchqueries) - [`queryClient.cancelQueries`](#queryclientcancelqueries) - [`queryClient.removeQueries`](#queryclientremovequeries) - [`queryClient.resetQueries`](#queryclientresetqueries) - [`queryClient.isFetching`](#queryclientisfetching) - [`queryClient.isMutating`](#queryclientismutating) - [`queryClient.getDefaultOptions`](#queryclientgetdefaultoptions) - [`queryClient.setDefaultOptions`](#queryclientsetdefaultoptions) - [`queryClient.getQueryDefaults`](#queryclientgetquerydefaults) - [`queryClient.setQueryDefaults`](#queryclientsetquerydefaults) - [`queryClient.getMutationDefaults`](#queryclientgetmutationdefaults) - [`queryClient.setMutationDefaults`](#queryclientsetmutationdefaults) - [`queryClient.getQueryCache`](#queryclientgetquerycache) - [`queryClient.getMutationCache`](#queryclientgetmutationcache) - [`queryClient.clear`](#queryclientclear) - [`queryClient.resumePausedMutations`](#queryclientresumepausedmutations) **Options** - `queryCache?: QueryCache` - Optional - The query cache this client is connected to. - `mutationCache?: MutationCache` - Optional - The mutation cache this client is connected to. - `defaultOptions?: DefaultOptions` - Optional - Define defaults for all queries and mutations using this queryClient. - You can also define defaults to be used for [hydration](../framework/react/reference/hydration.md) ## `queryClient.fetchQuery` `fetchQuery` is an asynchronous method that can be used to fetch and cache a query. It will either resolve with the data or throw with the error. Use the `prefetchQuery` method if you just want to fetch a query without needing the result. If the query exists and the data is not invalidated or older than the given `staleTime`, then the data from the cache will be returned. Otherwise it will try to fetch the latest data. ```tsx try { const data = await queryClient.fetchQuery({ queryKey, queryFn }) } catch (error) { console.log(error) } ``` Specify a `staleTime` to only fetch when the data is older than a certain amount of time: ```tsx try { const data = await queryClient.fetchQuery({ queryKey, queryFn, staleTime: 10000, }) } catch (error) { console.log(error) } ``` **Options** The options for `fetchQuery` are exactly the same as those of [`useQuery`](../framework/react/reference/useQuery.md), except the following: `enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, refetchOnMount, notifyOnChangeProps, throwOnError, select, suspense, placeholderData`; which are strictly for useQuery and useInfiniteQuery. You can check the [source code](https://github.com/TanStack/query/blob/7cd2d192e6da3df0b08e334ea1cf04cd70478827/packages/query-core/src/types.ts#L119) for more clarity. **Returns** - `Promise` ## `queryClient.fetchInfiniteQuery` `fetchInfiniteQuery` is similar to `fetchQuery` but can be used to fetch and cache an infinite query. ```tsx try { const data = await queryClient.fetchInfiniteQuery({ queryKey, queryFn }) console.log(data.pages) } catch (error) { console.log(error) } ``` **Options** The options for `fetchInfiniteQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery). **Returns** - `Promise>` ## `queryClient.prefetchQuery` `prefetchQuery` is an asynchronous method that can be used to prefetch a query before it is needed or rendered with `useQuery` and friends. The method works the same as `fetchQuery` except that it will not throw or return any data. ```tsx await queryClient.prefetchQuery({ queryKey, queryFn }) ``` You can even use it with a default queryFn in your config! ```tsx await queryClient.prefetchQuery({ queryKey }) ``` **Options** The options for `prefetchQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery). **Returns** - `Promise` - A promise is returned that will either immediately resolve if no fetch is needed or after the query has been executed. It will not return any data or throw any errors. ## `queryClient.prefetchInfiniteQuery` `prefetchInfiniteQuery` is similar to `prefetchQuery` but can be used to prefetch and cache an infinite query. ```tsx await queryClient.prefetchInfiniteQuery({ queryKey, queryFn }) ``` **Options** The options for `prefetchInfiniteQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery). **Returns** - `Promise` - A promise is returned that will either immediately resolve if no fetch is needed or after the query has been executed. It will not return any data or throw any errors. ## `queryClient.getQueryData` `getQueryData` is a synchronous function that can be used to get an existing query's cached data. If the query does not exist, `undefined` will be returned. ```tsx const data = queryClient.getQueryData(queryKey) ``` **Options** - `queryKey: QueryKey`: [Query Keys](../framework/react/guides/query-keys.md) **Returns** - `data: TQueryFnData | undefined` - The data for the cached query, or `undefined` if the query does not exist. ## `queryClient.ensureQueryData` `ensureQueryData` is an asynchronous function that can be used to get an existing query's cached data. If the query does not exist, `queryClient.fetchQuery` will be called and its results returned. ```tsx const data = await queryClient.ensureQueryData({ queryKey, queryFn }) ``` **Options** - the same options as [`fetchQuery`](#queryclientfetchquery) - `revalidateIfStale: boolean` - Optional - Defaults to `false` - If set to `true`, stale data will be refetched in the background, but cached data will be returned immediately. **Returns** - `Promise` ## `queryClient.ensureInfiniteQueryData` `ensureInfiniteQueryData` is an asynchronous function that can be used to get an existing infinite query's cached data. If the query does not exist, `queryClient.fetchInfiniteQuery` will be called and its results returned. ```tsx const data = await queryClient.ensureInfiniteQueryData({ queryKey, queryFn, initialPageParam, getNextPageParam, }) ``` **Options** - the same options as [`fetchInfiniteQuery`](#queryclientfetchinfinitequery) - `revalidateIfStale: boolean` - Optional - Defaults to `false` - If set to `true`, stale data will be refetched in the background, but cached data will be returned immediately. **Returns** - `Promise>` ## `queryClient.getQueriesData` `getQueriesData` is a synchronous function that can be used to get the cached data of multiple queries. Only queries that match the passed queryKey or queryFilter will be returned. If there are no matching queries, an empty array will be returned. ```tsx const data = queryClient.getQueriesData(filters) ``` **Options** - `filters: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) - if a filter is passed, the data with queryKeys matching the filter will be returned **Returns** - `[queryKey: QueryKey, data: TQueryFnData | undefined][]` - An array of tuples for the matched query keys, or `[]` if there are no matches. The tuples are the query key and its associated data. **Caveats** Because the returned data in each tuple can be of varying structures (i.e. using a filter to return "active" queries can return different data types), the `TData` generic defaults to `unknown`. If you provide a more specific type to `TData` it is assumed that you are certain each tuple's data entry is all the same type. This distinction is more a "convenience" for ts devs that know which structure will be returned. ## `queryClient.setQueryData` `setQueryData` is a synchronous function that can be used to immediately update a query's cached data. If the query does not exist, it will be created. **If the query is not utilized by a query hook in the default `gcTime` of 5 minutes, the query will be garbage collected**. To update multiple queries at once and match query keys partially, you need to use [`queryClient.setQueriesData`](#queryclientsetqueriesdata) instead. > The difference between using `setQueryData` and `fetchQuery` is that `setQueryData` is sync and assumes that you already synchronously have the data available. If you need to fetch the data asynchronously, it's suggested that you either refetch the query key or use `fetchQuery` to handle the asynchronous fetch. ```tsx queryClient.setQueryData(queryKey, updater) ``` **Options** - `queryKey: QueryKey`: [Query Keys](../framework/react/guides/query-keys.md) - `updater: TQueryFnData | undefined | ((oldData: TQueryFnData | undefined) => TQueryFnData | undefined)` - If non-function is passed, the data will be updated to this value - If a function is passed, it will receive the old data value and be expected to return a new one. **Using an updater value** ```tsx setQueryData(queryKey, newData) ``` If the value is `undefined`, the query data is not updated. **Using an updater function** For convenience in syntax, you can also pass an updater function which receives the current data value and returns the new one: ```tsx setQueryData(queryKey, (oldData) => newData) ``` If the updater function returns `undefined`, the query data will not be updated. If the updater function receives `undefined` as input, you can return `undefined` to bail out of the update and thus _not_ create a new cache entry. **Immutability** Updates via `setQueryData` must be performed in an _immutable_ way. **DO NOT** attempt to write directly to the cache by mutating `oldData` or data that you retrieved via `getQueryData` in place. ## `queryClient.getQueryState` `getQueryState` is a synchronous function that can be used to get an existing query's state. If the query does not exist, `undefined` will be returned. ```tsx const state = queryClient.getQueryState(queryKey) console.log(state.dataUpdatedAt) ``` **Options** - `queryKey: QueryKey`: [Query Keys](../framework/react/guides/query-keys.md) ## `queryClient.setQueriesData` `setQueriesData` is a synchronous function that can be used to immediately update cached data of multiple queries by using filter function or partially matching the query key. Only queries that match the passed queryKey or queryFilter will be updated - no new cache entries will be created. Under the hood, [`setQueryData`](#queryclientsetquerydata) is called for each existing query. ```tsx queryClient.setQueriesData(filters, updater) ``` **Options** - `filters: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) - if a filter is passed, queryKeys matching the filter will be updated - `updater: TQueryFnData | (oldData: TQueryFnData | undefined) => TQueryFnData` - the [setQueryData](#queryclientsetquerydata) updater function or new data, will be called for each matching queryKey ## `queryClient.invalidateQueries` The `invalidateQueries` method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background. - If you **do not want active queries to refetch**, and simply be marked as invalid, you can use the `refetchType: 'none'` option. - If you **want inactive queries to refetch** as well, use the `refetchType: 'all'` option - For refetching, [queryClient.refetchQueries](#queryclientrefetchqueries) is called. ```tsx await queryClient.invalidateQueries( { queryKey: ['posts'], exact, refetchType: 'active', }, { throwOnError, cancelRefetch }, ) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) - `queryKey?: QueryKey`: [Query Keys](../framework/react/guides/query-keys.md) - `refetchType?: 'active' | 'inactive' | 'all' | 'none'` - Defaults to `'active'` - When set to `active`, only queries that match the refetch predicate and are actively being rendered via `useQuery` and friends will be refetched in the background. - When set to `inactive`, only queries that match the refetch predicate and are NOT actively being rendered via `useQuery` and friends will be refetched in the background. - When set to `all`, all queries that match the refetch predicate will be refetched in the background. - When set to `none`, no queries will be refetched, and those that match the refetch predicate will be marked as invalid only. - `options?: InvalidateOptions`: - `throwOnError?: boolean` - When set to `true`, this method will throw if any of the query refetch tasks fail. - `cancelRefetch?: boolean` - Defaults to `true` - Per default, a currently running request will be cancelled before a new request is made - When set to `false`, no refetch will be made if there is already a request running. ## `queryClient.refetchQueries` The `refetchQueries` method can be used to refetch queries based on certain conditions. Examples: ```tsx // refetch all queries: await queryClient.refetchQueries() // refetch all stale queries: await queryClient.refetchQueries({ stale: true }) // refetch all active queries partially matching a query key: await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) // refetch all active queries exactly matching a query key: await queryClient.refetchQueries({ queryKey: ['posts', 1], type: 'active', exact: true, }) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) - `options?: RefetchOptions`: - `throwOnError?: boolean` - When set to `true`, this method will throw if any of the query refetch tasks fail. - `cancelRefetch?: boolean` - Defaults to `true` - Per default, a currently running request will be cancelled before a new request is made - When set to `false`, no refetch will be made if there is already a request running. **Returns** This function returns a promise that will resolve when all of the queries are done being refetched. By default, it **will not** throw an error if any of those queries refetches fail, but this can be configured by setting the `throwOnError` option to `true` **Notes** - Queries that are "disabled" because they only have disabled Observers will never be refetched. - Queries that are "static" because they only have Observers with a Static StaleTime will never be refetched. ## `queryClient.cancelQueries` The `cancelQueries` method can be used to cancel outgoing queries based on their query keys or any other functionally accessible property/state of the query. This is most useful when performing optimistic updates since you will likely need to cancel any outgoing query refetches so they don't clobber your optimistic update when they resolve. ```tsx await queryClient.cancelQueries( { queryKey: ['posts'], exact: true }, { silent: true }, ) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) - `cancelOptions?: CancelOptions`: [Cancel Options](../framework/react/guides/query-cancellation.md#cancel-options) **Returns** This method does not return anything ## `queryClient.removeQueries` The `removeQueries` method can be used to remove queries from the cache based on their query keys or any other functionally accessible property/state of the query. ```tsx queryClient.removeQueries({ queryKey, exact: true }) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) **Returns** This method does not return anything ## `queryClient.resetQueries` The `resetQueries` method can be used to reset queries in the cache to their initial state based on their query keys or any other functionally accessible property/state of the query. This will notify subscribers — unlike `clear`, which removes all subscribers — and reset the query to its pre-loaded state — unlike `invalidateQueries`. If a query has `initialData`, the query's data will be reset to that. If a query is active, it will be refetched. ```tsx queryClient.resetQueries({ queryKey, exact: true }) ``` **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) - `options?: ResetOptions`: - `throwOnError?: boolean` - When set to `true`, this method will throw if any of the query refetch tasks fail. - `cancelRefetch?: boolean` - Defaults to `true` - Per default, a currently running request will be cancelled before a new request is made - When set to `false`, no refetch will be made if there is already a request running. **Returns** This method returns a promise that resolves when all active queries have been refetched. ## `queryClient.isFetching` This `isFetching` method returns an `integer` representing how many queries, if any, in the cache are currently fetching (including background-fetching, loading new pages, or loading more infinite query results) ```tsx if (queryClient.isFetching()) { console.log('At least one query is fetching!') } ``` TanStack Query also exports a handy [`useIsFetching`](../framework/react/reference/useIsFetching.md) hook that will let you subscribe to this state in your components without creating a manual subscription to the query cache. **Options** - `filters?: QueryFilters`: [Query Filters](../framework/react/guides/filters.md#query-filters) **Returns** This method returns the number of fetching queries. ## `queryClient.isMutating` This `isMutating` method returns an `integer` representing how many mutations, if any, in the cache are currently fetching. ```tsx if (queryClient.isMutating()) { console.log('At least one mutation is fetching!') } ``` TanStack Query also exports a handy [`useIsMutating`](../framework/react/reference/useIsMutating.md) hook that will let you subscribe to this state in your components without creating a manual subscription to the mutation cache. **Options** - `filters: MutationFilters`: [Mutation Filters](../framework/react/guides/filters.md#mutation-filters) **Returns** This method returns the number of fetching mutations. ## `queryClient.getDefaultOptions` The `getDefaultOptions` method returns the default options which have been set when creating the client or with `setDefaultOptions`. ```tsx const defaultOptions = queryClient.getDefaultOptions() ``` ## `queryClient.setDefaultOptions` The `setDefaultOptions` method can be used to dynamically set the default options for this queryClient. Previously defined default options will be overwritten. ```tsx queryClient.setDefaultOptions({ queries: { staleTime: Infinity, }, }) ``` ## `queryClient.getQueryDefaults` The `getQueryDefaults` method returns the default options which have been set for specific queries: ```tsx const defaultOptions = queryClient.getQueryDefaults(['posts']) ``` > Note that if several query defaults match the given query key, they will be merged together based on the order of registration. > See [`setQueryDefaults`](#queryclientsetquerydefaults). ## `queryClient.setQueryDefaults` `setQueryDefaults` can be used to set default options for specific queries: ```tsx queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts }) function Component() { const { data } = useQuery({ queryKey: ['posts'] }) } ``` **Options** - `queryKey: QueryKey`: [Query Keys](../framework/react/guides/query-keys.md) - `options: QueryOptions` > As stated in [`getQueryDefaults`](#queryclientgetquerydefaults), the order of registration of query defaults does matter. > Since the matching defaults are merged by `getQueryDefaults`, the registration should be made in the following order: from the **most generic key** to the **least generic one** . > This way, more specific defaults will override more generic defaults. ## `queryClient.getMutationDefaults` The `getMutationDefaults` method returns the default options which have been set for specific mutations: ```tsx const defaultOptions = queryClient.getMutationDefaults(['addPost']) ``` ## `queryClient.setMutationDefaults` `setMutationDefaults` can be used to set default options for specific mutations: ```tsx queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost }) function Component() { const { data } = useMutation({ mutationKey: ['addPost'] }) } ``` **Options** - `mutationKey: unknown[]` - `options: MutationOptions` > Similar to [`setQueryDefaults`](#queryclientsetquerydefaults), the order of registration does matter here. ## `queryClient.getQueryCache` The `getQueryCache` method returns the query cache this client is connected to. ```tsx const queryCache = queryClient.getQueryCache() ``` ## `queryClient.getMutationCache` The `getMutationCache` method returns the mutation cache this client is connected to. ```tsx const mutationCache = queryClient.getMutationCache() ``` ## `queryClient.clear` The `clear` method clears all connected caches. ```tsx queryClient.clear() ``` ## `queryClient.resumePausedMutations` Can be used to resume mutations that have been paused because there was no network connection. ```tsx queryClient.resumePausedMutations() ``` ================================================ FILE: docs/reference/QueryObserver.md ================================================ --- id: QueryObserver title: QueryObserver --- The `QueryObserver` can be used to observe and switch between queries. ```tsx const observer = new QueryObserver(queryClient, { queryKey: ['posts'] }) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() }) ``` **Options** The options for the `QueryObserver` are exactly the same as those of [`useQuery`](../framework/react/reference/useQuery). ================================================ FILE: docs/reference/environmentManager.md ================================================ --- id: EnvironmentManager title: environmentManager --- The `environmentManager` manages how TanStack Query detects whether the current runtime should be treated as server-side. By default, it uses the same server detection as the exported `isServer` utility from query-core. Use this manager to override server detection globally for runtimes that are not traditional browser/server environments (for example, extension workers). Its available methods are: - [`isServer`](#environmentmanagerisserver) - [`setIsServer`](#environmentmanagersetisserver) ## `environmentManager.isServer` Returns whether the current runtime is treated as a server environment. ```tsx import { environmentManager } from '@tanstack/react-query' const server = environmentManager.isServer() ``` ## `environmentManager.setIsServer` Overrides the server check globally. ```tsx import { environmentManager } from '@tanstack/react-query' // Override environmentManager.setIsServer(() => { return typeof window === 'undefined' && !('chrome' in globalThis) }) ``` **Options** - `isServerValue: () => boolean` To restore the default behavior, set the function back to query-core's `isServer` utility: ```tsx import { environmentManager, isServer } from '@tanstack/react-query' environmentManager.setIsServer(() => isServer) ``` ================================================ FILE: docs/reference/focusManager.md ================================================ --- id: FocusManager title: FocusManager --- The `FocusManager` manages the focus state within TanStack Query. It can be used to change the default event listeners or to manually change the focus state. Its available methods are: - [`setEventListener`](#focusmanagerseteventlistener) - [`subscribe`](#focusmanagersubscribe) - [`setFocused`](#focusmanagersetfocused) - [`isFocused`](#focusmanagerisfocused) ## `focusManager.setEventListener` `setEventListener` can be used to set a custom event listener: ```tsx import { focusManager } from '@tanstack/react-query' focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('visibilitychange', handleFocus, false) } return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', handleFocus) } }) ``` ## `focusManager.subscribe` `subscribe` can be used to subscribe to changes in the visibility state. It returns an unsubscribe function: ```tsx import { focusManager } from '@tanstack/react-query' const unsubscribe = focusManager.subscribe((isVisible) => { console.log('isVisible', isVisible) }) ``` ## `focusManager.setFocused` `setFocused` can be used to manually set the focus state. Set `undefined` to fall back to the default focus check. ```tsx import { focusManager } from '@tanstack/react-query' // Set focused focusManager.setFocused(true) // Set unfocused focusManager.setFocused(false) // Fallback to the default focus check focusManager.setFocused(undefined) ``` **Options** - `focused: boolean | undefined` ## `focusManager.isFocused` `isFocused` can be used to get the current focus state. ```tsx const isFocused = focusManager.isFocused() ``` ================================================ FILE: docs/reference/notifyManager.md ================================================ --- id: NotifyManager title: NotifyManager --- The `notifyManager` handles scheduling and batching callbacks in TanStack Query. It exposes the following methods: - [batch](#notifymanagerbatch) - [batchCalls](#notifymanagerbatchcalls) - [schedule](#notifymanagerschedule) - [setNotifyFunction](#notifymanagersetnotifyfunction) - [setBatchNotifyFunction](#notifymanagersetbatchnotifyfunction) - [setScheduler](#notifymanagersetscheduler) ## `notifyManager.batch` `batch` can be used to batch all updates scheduled inside the passed callback. This is mainly used internally to optimize queryClient updating. ```ts function batch(callback: () => T): T ``` ## `notifyManager.batchCalls` `batchCalls` is a higher-order function that takes a callback and wraps it. All calls to the wrapped function schedule the callback to be run on the next batch. ```ts type BatchCallsCallback> = (...args: T) => void function batchCalls>( callback: BatchCallsCallback, ): BatchCallsCallback ``` ## `notifyManager.schedule` `schedule` schedules a function to be run on the next batch. By default, the batch is run with a setTimeout, but this can be configured. ```ts function schedule(callback: () => void): void ``` ## `notifyManager.setNotifyFunction` `setNotifyFunction` overrides the notify function. This function is passed the callback when it should be executed. The default notifyFunction just calls it. This can be used to for example wrap notifications with `React.act` while running tests: ```ts import { notifyManager } from '@tanstack/react-query' import { act } from 'react-dom/test-utils' notifyManager.setNotifyFunction(act) ``` ## `notifyManager.setBatchNotifyFunction` `setBatchNotifyFunction` sets the function to use for batched updates If your framework supports a custom batching function, you can let TanStack Query know about it by calling notifyManager.setBatchNotifyFunction. For example, this is how the batch function is set in solid-query: ```ts import { notifyManager } from '@tanstack/query-core' import { batch } from 'solid-js' notifyManager.setBatchNotifyFunction(batch) ``` ## `notifyManager.setScheduler` `setScheduler` configures a custom callback that should schedules when the next batch runs. The default behaviour is `setTimeout(callback, 0)`. ```ts import { notifyManager } from '@tanstack/react-query' // Schedule batches in the next microtask notifyManager.setScheduler(queueMicrotask) // Schedule batches before the next frame is rendered notifyManager.setScheduler(requestAnimationFrame) // Schedule batches some time in the future notifyManager.setScheduler((cb) => setTimeout(cb, 10)) ``` ================================================ FILE: docs/reference/onlineManager.md ================================================ --- id: OnlineManager title: OnlineManager --- The `OnlineManager` manages the online state within TanStack Query. It can be used to change the default event listeners or to manually change the online state. > Per default, the `onlineManager` assumes an active network connection, and listens to the `online` and `offline` events on the `window` object to detect changes. > In previous versions, `navigator.onLine` was used to determine the network status. However, it doesn't work well in Chromium based browsers. There are [a lot of issues](https://bugs.chromium.org/p/chromium/issues/list?q=navigator.online) around false negatives, which lead to Queries being wrongfully marked as `offline`. > To circumvent this, we now always start with `online: true` and only listen to `online` and `offline` events to update the status. > This should reduce the likelihood of false negatives, however, it might mean false positives for offline apps that load via serviceWorkers, which can work even without an internet connection. Its available methods are: - [`setEventListener`](#onlinemanagerseteventlistener) - [`subscribe`](#onlinemanagersubscribe) - [`setOnline`](#onlinemanagersetonline) - [`isOnline`](#onlinemanagerisonline) ## `onlineManager.setEventListener` `setEventListener` can be used to set a custom event listener: ```tsx import NetInfo from '@react-native-community/netinfo' import { onlineManager } from '@tanstack/react-query' onlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { setOnline(!!state.isConnected) }) }) ``` ## `onlineManager.subscribe` `subscribe` can be used to subscribe to changes in the online state. It returns an unsubscribe function: ```tsx import { onlineManager } from '@tanstack/react-query' const unsubscribe = onlineManager.subscribe((isOnline) => { console.log('isOnline', isOnline) }) ``` ## `onlineManager.setOnline` `setOnline` can be used to manually set the online state. ```tsx import { onlineManager } from '@tanstack/react-query' // Set to online onlineManager.setOnline(true) // Set to offline onlineManager.setOnline(false) ``` **Options** - `online: boolean` ## `onlineManager.isOnline` `isOnline` can be used to get the current online state. ```tsx const isOnline = onlineManager.isOnline() ``` ================================================ FILE: docs/reference/streamedQuery.md ================================================ --- id: streamedQuery title: streamedQuery --- `streamedQuery` is a helper function to create a query function that streams data from an [AsyncIterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator). Data will be an Array of all the chunks received. The query will be in a `pending` state until the first chunk of data is received, but will go to `success` after that. The query will stay in fetchStatus `fetching` until the stream ends. To see `streamedQuery` in action, take a look at our chat example in the [examples/react/chat directory on GitHub](https://github.com/TanStack/query/tree/main/examples/react/chat). ```tsx import { experimental_streamedQuery as streamedQuery } from '@tanstack/react-query' const query = queryOptions({ queryKey: ['data'], queryFn: streamedQuery({ streamFn: fetchDataInChunks, }), }) ``` > Note: `streamedQuery` is currently marked as `experimental` because we want to gather feedback from the community. If you've tried out the API and have feedback for us, please provide it in this [GitHub discussion](https://github.com/TanStack/query/discussions/9065). **Options** - `streamFn: (context: QueryFunctionContext) => Promise>` - **Required** - The function that returns a Promise of an AsyncIterable with data to stream in. - Receives a [QueryFunctionContext](../framework/react/guides/query-functions.md#queryfunctioncontext) - `refetchMode?: 'append' | 'reset' | 'replace'` - Optional - Defines how refetches are handled. - Defaults to `'reset'` - When set to `'reset'`, the query will erase all data and go back into `pending` state. - When set to `'append'`, data will be appended to existing data. - When set to `'replace'`, all data will be written to the cache once the stream ends. - `reducer?: (accumulator: TData, chunk: TQueryFnData) => TData` - Optional - Reduces streamed chunks (`TQueryFnData`) into the final data shape (`TData`). - Default: appends each chunk to the end of the accumulator when `TData` is an array. - If `TData` is not an array, you must provide a custom `reducer`. - `initialValue?: TData = TQueryFnData` - Optional - Defines the initial data to be used while the first chunk is being fetched, and it is also returned when the stream yields no values. - It is mandatory when custom `reducer` is provided. - Defaults to an empty array. ================================================ FILE: docs/reference/timeoutManager.md ================================================ --- id: TimeoutManager title: TimeoutManager --- The `TimeoutManager` handles `setTimeout` and `setInterval` timers in TanStack Query. TanStack Query uses timers to implement features like query `staleTime` and `gcTime`, as well as retries, throttling, and debouncing. By default, TimeoutManager uses the global `setTimeout` and `setInterval`, but it can be configured to use custom implementations instead. Its available methods are: - [`timeoutManager.setTimeoutProvider`](#timeoutmanagersettimeoutprovider) - [`TimeoutProvider`](#timeoutprovider) - [`timeoutManager.setTimeout`](#timeoutmanagersettimeout) - [`timeoutManager.clearTimeout`](#timeoutmanagercleartimeout) - [`timeoutManager.setInterval`](#timeoutmanagersetinterval) - [`timeoutManager.clearInterval`](#timeoutmanagerclearinterval) ## `timeoutManager.setTimeoutProvider` `setTimeoutProvider` can be used to set a custom implementation of the `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` functions, called a `TimeoutProvider`. This may be useful if you notice event loop performance issues with thousands of queries. A custom TimeoutProvider could also support timer delays longer than the global `setTimeout` maximum delay value of about [24 days](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value). It is important to call `setTimeoutProvider` before creating a QueryClient or queries, so that the same provider is used consistently for all timers in the application, since different TimeoutProviders cannot cancel each others' timers. ```tsx import { timeoutManager, QueryClient } from '@tanstack/react-query' import { CustomTimeoutProvider } from './CustomTimeoutProvider' timeoutManager.setTimeoutProvider(new CustomTimeoutProvider()) export const queryClient = new QueryClient() ``` ### `TimeoutProvider` Timers are very performance sensitive. Short term timers (such as those with delays less than 5 seconds) tend to be latency sensitive, where long-term timers may benefit more from [timer coalescing](https://en.wikipedia.org/wiki/Timer_coalescing) - batching timers with similar deadlines together - using a data structure like a [hierarchical time wheel](https://www.npmjs.com/package/timer-wheel). The `TimeoutProvider` type requires that implementations handle timer ID objects that can be converted to `number` via [Symbol.toPrimitive][toPrimitive] because runtimes like NodeJS return [objects][nodejs-timeout] from their global `setTimeout` and `setInterval` functions. TimeoutProvider implementations are free to coerce timer IDs to number internally, or to return their own custom object type that implements `{ [Symbol.toPrimitive]: () => number }`. [nodejs-timeout]: https://nodejs.org/api/timers.html#class-timeout [toPrimitive]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive ```tsx type ManagedTimerId = number | { [Symbol.toPrimitive]: () => number } type TimeoutProvider = { readonly setTimeout: (callback: TimeoutCallback, delay: number) => TTimerId readonly clearTimeout: (timeoutId: TTimerId | undefined) => void readonly setInterval: (callback: TimeoutCallback, delay: number) => TTimerId readonly clearInterval: (intervalId: TTimerId | undefined) => void } ``` ## `timeoutManager.setTimeout` `setTimeout(callback, delayMs)` schedules a callback to run after approximately `delay` milliseconds, like the global [setTimeout function](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout).The callback can be canceled with `timeoutManager.clearTimeout`. It returns a timer ID, which may be a number or an object that can be coerced to a number via [Symbol.toPrimitive][toPrimitive]. ```tsx import { timeoutManager } from '@tanstack/react-query' const timeoutId = timeoutManager.setTimeout( () => console.log('ran at:', new Date()), 1000, ) const timeoutIdNumber: number = Number(timeoutId) ``` ## `timeoutManager.clearTimeout` `clearTimeout(timerId)` cancels a timeout callback scheduled with `setTimeout`, like the global [clearTimeout function](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout). It should be called with a timer ID returned by `timeoutManager.setTimeout`. ```tsx import { timeoutManager } from '@tanstack/react-query' const timeoutId = timeoutManager.setTimeout( () => console.log('ran at:', new Date()), 1000, ) timeoutManager.clearTimeout(timeoutId) ``` ## `timeoutManager.setInterval` `setInterval(callback, intervalMs)` schedules a callback to be called approximately every `intervalMs`, like the global [setInterval function](https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval). Like `setTimeout`, it returns a timer ID, which may be a number or an object that can be coerced to a number via [Symbol.toPrimitive](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive). ```tsx import { timeoutManager } from '@tanstack/react-query' const intervalId = timeoutManager.setInterval( () => console.log('ran at:', new Date()), 1000, ) ``` ## `timeoutManager.clearInterval` `clearInterval(intervalId)` can be used to cancel an interval, like the global [clearInterval function](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearInterval). It should be called with an interval ID returned by `timeoutManager.setInterval`. ```tsx import { timeoutManager } from '@tanstack/react-query' const intervalId = timeoutManager.setInterval( () => console.log('ran at:', new Date()), 1000, ) timeoutManager.clearInterval(intervalId) ``` ================================================ FILE: eslint.config.js ================================================ // @ts-check // @ts-ignore Needed due to moduleResolution Node vs Bundler import { tanstackConfig } from '@tanstack/eslint-config' import pluginCspell from '@cspell/eslint-plugin' import vitest from '@vitest/eslint-plugin' export default [ ...tanstackConfig, { name: 'tanstack/temp', plugins: { cspell: pluginCspell, }, rules: { 'cspell/spellchecker': [ 'warn', { cspell: { words: [ 'Promisable', // Our public interface 'TSES', // @typescript-eslint package's interface 'codemod', // We support our codemod 'combinate', // Library name 'datatag', // Query options tagging 'extralight', // Our public interface 'jscodeshift', 'refetches', // Query refetch operations 'retryer', // Our public interface 'solidjs', // Our target framework 'tabular-nums', // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric 'tanstack', // Our package scope 'todos', // Too general word to be caught as error 'tsqd', // Our public interface (TanStack Query Devtools shorthand) 'tsup', // We use tsup as builder 'typecheck', // Field of vite.config.ts 'vue-demi', // dependency of @tanstack/vue-query 'ɵkind', // Angular specific 'ɵproviders', // Angular specific ], }, }, ], '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', 'no-case-declarations': 'off', 'prefer-const': 'off', }, }, { files: ['**/*.spec.ts*', '**/*.test.ts*', '**/*.test-d.ts*'], plugins: { vitest }, rules: { ...vitest.configs.recommended.rules, 'vitest/no-standalone-expect': [ 'error', { additionalTestBlockFunctions: ['testIf'], }, ], }, settings: { vitest: { typecheck: true } }, }, ] ================================================ FILE: examples/angular/auto-refetching/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/auto-refetching/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/auto-refetching/README.md ================================================ # TanStack Query Angular auto-refetching example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/auto-refetching/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "auto-refetching": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/auto-refetching", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "auto-refetching:build:production" }, "development": { "buildTarget": "auto-refetching:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "auto-refetching:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/auto-refetching/package.json ================================================ { "name": "@tanstack/query-example-angular-auto-refetching", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/auto-refetching/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { AutoRefetchingExampleComponent } from './components/auto-refetching.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-root', template: ``, imports: [AutoRefetchingExampleComponent], }) export class AppComponent {} ================================================ FILE: examples/angular/auto-refetching/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch, withInterceptors, } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { mockInterceptor } from './interceptor/mock-api.interceptor' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch(), withInterceptors([mockInterceptor])), provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }), withDevtools(), ), ], } ================================================ FILE: examples/angular/auto-refetching/src/app/components/auto-refetching.component.html ================================================

    Auto Refetch with stale-time set to {{ intervalMs() }}ms

    This example is best experienced on your own machine, where you can open multiple tabs to the same localhost server and see your changes propagate between the two.

    Todo List

      @for (item of tasks.data(); track item) {
    • {{ item }}
    • }
    ================================================ FILE: examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject, signal, } from '@angular/core' import { injectMutation, injectQuery, } from '@tanstack/angular-query-experimental' import { NgStyle } from '@angular/common' import { TasksService } from '../services/tasks.service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'auto-refetching-example', templateUrl: './auto-refetching.component.html', imports: [NgStyle], }) export class AutoRefetchingExampleComponent { readonly #tasksService = inject(TasksService) readonly intervalMs = signal(1000) readonly tasks = injectQuery(() => this.#tasksService.allTasks(this.intervalMs()), ) readonly addMutation = injectMutation(() => this.#tasksService.addTask()) readonly clearMutation = injectMutation(() => this.#tasksService.clearAllTasks(), ) clearTasks() { this.clearMutation.mutate() } inputChange($event: Event) { const target = $event.target as HTMLInputElement this.intervalMs.set(Number(target.value)) } addItem($event: Event) { const target = $event.target as HTMLInputElement const value = target.value this.addMutation.mutate(value) target.value = '' } } ================================================ FILE: examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts ================================================ /** * MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints. * It handles the following operations: * - GET: Fetches all tasks from localStorage. * - POST: Adds a new task to localStorage. * - DELETE: Clears all tasks from localStorage. * Simulated responses include a delay to mimic network latency. */ import { HttpResponse } from '@angular/common/http' import { delay, of } from 'rxjs' import type { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest, } from '@angular/common/http' import type { Observable } from 'rxjs' export const mockInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn, ): Observable> => { const respondWith = (status: number, body: any) => of(new HttpResponse({ status, body })).pipe(delay(100)) if (req.url === '/api/tasks') { switch (req.method) { case 'GET': return respondWith( 200, JSON.parse(localStorage.getItem('tasks') || '[]'), ) case 'POST': const tasks = JSON.parse(localStorage.getItem('tasks') || '[]') tasks.push(req.body) localStorage.setItem('tasks', JSON.stringify(tasks)) return respondWith(201, { status: 'success', task: req.body, }) case 'DELETE': localStorage.removeItem('tasks') return respondWith(200, { status: 'success' }) } } return next(req) } ================================================ FILE: examples/angular/auto-refetching/src/app/services/tasks.service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' import { QueryClient, mutationOptions, queryOptions, } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' @Injectable({ providedIn: 'root', }) export class TasksService { readonly #queryClient = inject(QueryClient) // Manages query state and caching readonly #http = inject(HttpClient) // Handles HTTP requests /** * Fetches all tasks from the API. * Returns an observable containing an array of task strings. */ allTasks = (intervalMs: number) => queryOptions({ queryKey: ['tasks'], queryFn: () => { return lastValueFrom(this.#http.get>('/api/tasks')) }, refetchInterval: intervalMs, }) /** * Creates a mutation for adding a task. * On success, invalidates and refetches the "tasks" query cache to update the task list. */ addTask() { return mutationOptions({ mutationFn: (task: string) => lastValueFrom(this.#http.post('/api/tasks', task)), mutationKey: ['tasks'], onSuccess: () => { this.#queryClient.invalidateQueries({ queryKey: ['tasks'] }) }, }) } /** * Creates a mutation for clearing all tasks. * On success, invalidates and refetches the "tasks" query cache to ensure consistency. */ clearAllTasks() { return mutationOptions({ mutationFn: () => lastValueFrom(this.#http.delete('/api/tasks')), mutationKey: ['clearTasks'], onSuccess: () => { this.#queryClient.invalidateQueries({ queryKey: ['tasks'] }) }, }) } } ================================================ FILE: examples/angular/auto-refetching/src/index.html ================================================ TanStack Query Angular auto-refetching example ================================================ FILE: examples/angular/auto-refetching/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig) .then(() => { // an simple endpoint for getting current list localStorage.setItem( 'tasks', JSON.stringify(['Item 1', 'Item 2', 'Item 3']), ) }) .catch((err) => console.error(err)) ================================================ FILE: examples/angular/auto-refetching/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/auto-refetching/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/basic/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/basic/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/basic/README.md ================================================ # TanStack Query Angular basic example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/basic/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "basic": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/basic", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "basic:build:production" }, "development": { "buildTarget": "basic:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "basic:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/basic/package.json ================================================ { "name": "@tanstack/query-example-angular-basic", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/basic/src/app/app.component.html ================================================

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes! (You may need to throttle your network speed to simulate longer loading sequences)

    @if (postId() > -1) { } @else { } ================================================ FILE: examples/angular/basic/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { PostComponent } from './components/post.component' import { PostsComponent } from './components/posts.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'basic-example', templateUrl: './app.component.html', imports: [PostComponent, PostsComponent], }) export class BasicExampleComponent { readonly postId = signal(-1) } ================================================ FILE: examples/angular/basic/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch()), provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }), withDevtools(), ), ], } ================================================ FILE: examples/angular/basic/src/app/components/post.component.html ================================================
    @if (postQuery.isPending()) { Loading... } @else if (postQuery.isError()) { Error: {{ postQuery.error().message }} } @if (postQuery.data(); as post) {

    {{ post.title }}

    {{ post.body }}

    @if (postQuery.isFetching()) { Background Updating... } }
    ================================================ FILE: examples/angular/basic/src/app/components/post.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject, input, output, } from '@angular/core' import { injectQuery } from '@tanstack/angular-query-experimental' import { fromEvent, lastValueFrom, takeUntil } from 'rxjs' import { PostsService } from '../services/posts-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'post', templateUrl: './post.component.html', }) export class PostComponent { readonly #postsService = inject(PostsService) readonly setPostId = output() readonly postId = input(0) readonly postQuery = injectQuery(() => ({ enabled: this.postId() > 0, queryKey: ['post', this.postId()], queryFn: (context) => { // Cancels the request when component is destroyed before the request finishes const abort$ = fromEvent(context.signal, 'abort') return lastValueFrom( this.#postsService.postById$(this.postId()).pipe(takeUntil(abort$)), ) }, })) } ================================================ FILE: examples/angular/basic/src/app/components/posts.component.html ================================================

    Posts

    @if (postsQuery.isPending()) { Loading... } @else if (postsQuery.isError()) { Error: {{ postsQuery.error().message }} } @else if (postsQuery.isSuccess()) {
    @for (post of postsQuery.data(); track post.id) {

    {{ post.title }}

    }
    }
    @if (postsQuery.isFetching()) { Background Updating... }
    ================================================ FILE: examples/angular/basic/src/app/components/posts.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject, output, } from '@angular/core' import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { PostsService } from '../services/posts-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'posts', templateUrl: './posts.component.html', }) export class PostsComponent { readonly queryClient = inject(QueryClient) readonly #postsService = inject(PostsService) readonly setPostId = output() readonly postsQuery = injectQuery(() => ({ queryKey: ['posts'], queryFn: () => lastValueFrom(this.#postsService.allPosts$()), })) } ================================================ FILE: examples/angular/basic/src/app/services/posts-service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' @Injectable({ providedIn: 'root', }) export class PostsService { readonly #http = inject(HttpClient) postById$ = (postId: number) => this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`) allPosts$ = () => this.#http.get>('https://jsonplaceholder.typicode.com/posts') } export interface Post { id: number title: string body: string } ================================================ FILE: examples/angular/basic/src/index.html ================================================ TanStack Query Angular basic example ================================================ FILE: examples/angular/basic/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { BasicExampleComponent } from './app/app.component' bootstrapApplication(BasicExampleComponent, appConfig).catch((err) => console.error(err), ) ================================================ FILE: examples/angular/basic/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/basic/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/basic-persister/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/basic-persister/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/basic-persister/README.md ================================================ # TanStack Query Angular basic persister example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/basic-persister/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "basic-persister": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/basic-persister", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "basic-persister:build:production" }, "development": { "buildTarget": "basic-persister:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "basic-persister:build" } } } } } } ================================================ FILE: examples/angular/basic-persister/package.json ================================================ { "name": "@tanstack/query-example-angular-basic-persister", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "@tanstack/angular-query-persist-client": "^5.62.32", "@tanstack/query-async-storage-persister": "^5.90.27", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/basic-persister/src/app/app.component.html ================================================

    Try to mock offline behavior with the button in the devtools. You can navigate around as long as there is already data in the cache. You'll get a refetch as soon as you go "online" again.

    @if (postId() > -1) { } @else { } ================================================ FILE: examples/angular/basic-persister/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { PostComponent } from './components/post.component' import { PostsComponent } from './components/posts.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'basic-example', templateUrl: './app.component.html', imports: [PostComponent, PostsComponent], }) export class BasicExampleComponent { postId = signal(-1) } ================================================ FILE: examples/angular/basic-persister/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withPersistQueryClient } from '@tanstack/angular-query-persist-client' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import type { ApplicationConfig } from '@angular/core' const localStoragePersister = createAsyncStoragePersister({ storage: window.localStorage, }) export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch()), provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, // 1 minute gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }), withDevtools(), withPersistQueryClient({ persistOptions: { persister: localStoragePersister, }, }), ), ], } ================================================ FILE: examples/angular/basic-persister/src/app/components/post.component.html ================================================
    @if (postQuery.isPending()) { Loading... } @else if (postQuery.isError()) { Error: {{ postQuery.error().message }} } @if (postQuery.data(); as post) {

    {{ post.title }}

    {{ post.body }}

    @if (postQuery.isFetching()) { Background Updating... } }
    ================================================ FILE: examples/angular/basic-persister/src/app/components/post.component.ts ================================================ import { ChangeDetectionStrategy, Component, EventEmitter, Output, inject, input, } from '@angular/core' import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' import { fromEvent, lastValueFrom, takeUntil } from 'rxjs' import { PostsService } from '../services/posts-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'post', templateUrl: './post.component.html', }) export class PostComponent { #postsService = inject(PostsService) @Output() setPostId = new EventEmitter() postId = input(0) postQuery = injectQuery(() => ({ enabled: this.postId() > 0, queryKey: ['post', this.postId()], queryFn: async (context) => { // Cancels the request when component is destroyed before the request finishes const abort$ = fromEvent(context.signal, 'abort') return lastValueFrom( this.#postsService.postById$(this.postId()).pipe(takeUntil(abort$)), ) }, })) queryClient = inject(QueryClient) } ================================================ FILE: examples/angular/basic-persister/src/app/components/posts.component.html ================================================

    Posts

    @if (postsQuery.isPending()) { Loading... } @else if (postsQuery.isError()) { Error: {{ postsQuery.error().message }} } @else if (postsQuery.isSuccess()) {
    @for (post of postsQuery.data(); track post.id) {

    {{ post.title }}

    }
    }
    @if (postsQuery.isFetching()) { Background Updating... }
    ================================================ FILE: examples/angular/basic-persister/src/app/components/posts.component.ts ================================================ import { ChangeDetectionStrategy, Component, EventEmitter, Output, inject, } from '@angular/core' import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { PostsService } from '../services/posts-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'posts', templateUrl: './posts.component.html', }) export class PostsComponent { queryClient = inject(QueryClient) #postsService = inject(PostsService) @Output() setPostId = new EventEmitter() postsQuery = injectQuery(() => ({ queryKey: ['posts'], queryFn: () => lastValueFrom(this.#postsService.allPosts$()), })) } ================================================ FILE: examples/angular/basic-persister/src/app/services/posts-service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' @Injectable({ providedIn: 'root', }) export class PostsService { #http = inject(HttpClient) postById$ = (postId: number) => this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`) allPosts$ = () => this.#http.get>('https://jsonplaceholder.typicode.com/posts') } export interface Post { id: number title: string body: string } ================================================ FILE: examples/angular/basic-persister/src/index.html ================================================ TanStack Query Angular basic persister example ================================================ FILE: examples/angular/basic-persister/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { BasicExampleComponent } from './app/app.component' bootstrapApplication(BasicExampleComponent, appConfig).catch((err) => console.error(err), ) ================================================ FILE: examples/angular/basic-persister/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/basic-persister/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/devtools-panel/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/devtools-panel/README.md ================================================ # TanStack Query Angular devtools panel example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/devtools-panel/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "devtools-panel": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/devtools-panel", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "devtools-panel:build:production" }, "development": { "buildTarget": "devtools-panel:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "devtools-panel:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/devtools-panel/package.json ================================================ { "name": "@tanstack/query-example-angular-devtools-panel", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/devtools-panel/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { RouterLink, RouterOutlet } from '@angular/router' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-root', template: ` `, imports: [RouterOutlet, RouterLink], }) export class AppComponent {} ================================================ FILE: examples/angular/devtools-panel/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch } from '@angular/common/http' import { provideRouter } from '@angular/router' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { routes } from './app.routes' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch()), provideRouter(routes), provideTanStackQuery(new QueryClient()), ], } ================================================ FILE: examples/angular/devtools-panel/src/app/app.routes.ts ================================================ import type { Route } from '@angular/router' export const routes: Array = [ { path: '', redirectTo: 'basic', pathMatch: 'full', }, { path: 'basic', loadComponent: () => import('./components/basic-devtools-panel-example.component'), }, { path: 'lazy', loadComponent: () => import('./components/lazy-load-devtools-panel-example.component'), }, ] ================================================ FILE: examples/angular/devtools-panel/src/app/components/basic-devtools-panel-example.component.ts ================================================ import { ChangeDetectionStrategy, Component, signal, viewChild, } from '@angular/core' import { injectDevtoolsPanel } from '@tanstack/angular-query-experimental/devtools-panel' import { ExampleQueryComponent } from './example-query.component' import type { ElementRef } from '@angular/core' @Component({ selector: 'basic-devtools-panel-example', changeDetection: ChangeDetectionStrategy.OnPush, template: `

    Basic devtools panel example

    In this example, the devtools panel is loaded programmatically when the button is clicked

    @if (isOpen()) {
    } `, imports: [ExampleQueryComponent], }) export default class BasicDevtoolsPanelExampleComponent { readonly isOpen = signal(false) readonly divEl = viewChild('div') toggleIsOpen() { this.isOpen.update((prev) => !prev) } readonly devtools = injectDevtoolsPanel(() => ({ hostElement: this.divEl(), })) } ================================================ FILE: examples/angular/devtools-panel/src/app/components/example-query.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { injectQuery } from '@tanstack/angular-query-experimental' import { HttpClient } from '@angular/common/http' import { lastValueFrom } from 'rxjs' interface Response { name: string description: string subscribers_count: number stargazers_count: number forks_count: number } @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'example-query', template: `
    @if (query.isPending()) {
    Loading...
    } @if (query.isError()) {
    An error has occurred: {{ query.error().message }}
    } @if (query.isSuccess()) { @let data = query.data();

    {{ data.name }}

    {{ data.description }}

    👀 {{ data.subscribers_count }} ✨ {{ data.stargazers_count }} 🍴 {{ data.forks_count }} }
    `, }) export class ExampleQueryComponent { readonly #http = inject(HttpClient) readonly query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => lastValueFrom( this.#http.get('https://api.github.com/repos/tanstack/query'), ), })) } ================================================ FILE: examples/angular/devtools-panel/src/app/components/lazy-load-devtools-panel-example.component.ts ================================================ import { ChangeDetectionStrategy, Component, Injector, computed, effect, inject, signal, viewChild, } from '@angular/core' import { ExampleQueryComponent } from './example-query.component' import type { ElementRef } from '@angular/core' import type { DevtoolsPanelRef } from '@tanstack/angular-query-experimental/devtools-panel' @Component({ selector: 'lazy-load-devtools-panel-example', changeDetection: ChangeDetectionStrategy.OnPush, template: `

    Lazy load devtools panel example

    In this example, the devtools panel is loaded programmatically when the button is clicked. In addition, the code is lazy loaded.

    @if (isOpen()) {
    } `, imports: [ExampleQueryComponent], }) export default class LazyLoadDevtoolsPanelExampleComponent { readonly isOpen = signal(false) readonly devtools = signal | undefined>(undefined) readonly injector = inject(Injector) readonly divEl = viewChild('div') readonly devToolsOptions = computed(() => ({ hostElement: this.divEl(), })) toggleIsOpen() { this.isOpen.update((prev) => !prev) } readonly loadDevtoolsEffect = effect(() => { if (this.devtools()) return if (this.isOpen()) { this.devtools.set( import('@tanstack/angular-query-experimental/devtools-panel').then( ({ injectDevtoolsPanel }) => injectDevtoolsPanel(this.devToolsOptions, { injector: this.injector, }), ), ) } }) } ================================================ FILE: examples/angular/devtools-panel/src/index.html ================================================ TanStack Query devtools panel example ================================================ FILE: examples/angular/devtools-panel/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/devtools-panel/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/devtools-panel/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/infinite-query-with-max-pages/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/infinite-query-with-max-pages/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/infinite-query-with-max-pages/README.md ================================================ # TanStack Query Angular infinite query example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/infinite-query-with-max-pages/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "infinite-query-with-max-pages": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/infinite-query-with-max-pages", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/mockServiceWorker.js"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "infinite-query-with-max-pages:build:production" }, "development": { "buildTarget": "infinite-query-with-max-pages:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "infinite-query-with-max-pages:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/infinite-query-with-max-pages/package.json ================================================ { "name": "@tanstack/query-example-angular-infinite-query-with-max-pages", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/api/projects-mock.interceptor.ts ================================================ import { HttpResponse } from '@angular/common/http' import { delayWhen, of, timer } from 'rxjs' import type { Observable } from 'rxjs' import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http' export const projectsMockInterceptor: HttpInterceptorFn = ( req, next, ): Observable> => { const { url } = req if (url.includes('/api/projects')) { const cursor = parseInt( new URLSearchParams(req.url.split('?')[1]).get('cursor') || '0', 10, ) const pageSize = 4 const data = Array(pageSize) .fill(0) .map((_, i) => { return { name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`, id: i + cursor, } }) const nextId = cursor < 20 ? data[data.length - 1].id + 1 : null const previousId = cursor > -20 ? data[0].id - pageSize : null // Simulate network latency with a random delay between 100ms and 500ms const delayDuration = Math.random() * (500 - 100) + 100 return of( new HttpResponse({ status: 200, body: { data, nextId, previousId, }, }), ).pipe(delayWhen(() => timer(delayDuration))) } return next(req) } ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ExampleComponent } from './components/example.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-root', template: ``, imports: [ExampleComponent], }) export class AppComponent {} ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch, withInterceptors, } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { projectsMockInterceptor } from './api/projects-mock.interceptor' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withInterceptors([projectsMockInterceptor]), withFetch()), provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }), withDevtools(), ), ], } ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/components/example.component.html ================================================

    Infinite Query with max pages

    4 projects per page

    3 pages max

    @if (query.isPending()) {

    Loading...

    } @else if (query.isError()) { Error: {{ query.error().message }} } @else {
    @for (page of query.data().pages; track $index) { @for (project of page.data; track project.id) {

    {{ project.name }} {{ project.id }}

    } }
    {{ query.isFetching() && !query.isFetchingNextPage() ? 'Background Updating...' : null }}
    }
    ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts ================================================ import { ChangeDetectionStrategy, Component, computed, inject, } from '@angular/core' import { injectInfiniteQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { ProjectStyleDirective } from '../directives/project-style.directive' import { ProjectsService } from '../services/projects.service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'example', templateUrl: './example.component.html', imports: [ProjectStyleDirective], }) export class ExampleComponent { readonly projectsService = inject(ProjectsService) readonly query = injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: ({ pageParam }) => { return lastValueFrom(this.projectsService.getProjects(pageParam)) }, initialPageParam: 0, getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, maxPages: 3, })) readonly nextButtonDisabled = computed( () => !this.#hasNextPage() || this.#isFetchingNextPage(), ) readonly nextButtonText = computed(() => this.#isFetchingNextPage() ? 'Loading more...' : this.#hasNextPage() ? 'Load newer' : 'Nothing more to load', ) readonly previousButtonDisabled = computed( () => !this.#hasPreviousPage() || this.#isFetchingNextPage(), ) readonly previousButtonText = computed(() => this.#isFetchingPreviousPage() ? 'Loading more...' : this.#hasPreviousPage() ? 'Load Older' : 'Nothing more to load', ) readonly #hasPreviousPage = this.query.hasPreviousPage readonly #hasNextPage = this.query.hasNextPage readonly #isFetchingPreviousPage = this.query.isFetchingPreviousPage readonly #isFetchingNextPage = this.query.isFetchingNextPage } ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/directives/project-style.directive.ts ================================================ import { Directive, computed, input } from '@angular/core' @Directive({ selector: '[projectStyle]', host: { '[style]': 'style()', }, }) export class ProjectStyleDirective { readonly projectStyle = input.required() readonly style = computed( () => ` border: 1px solid gray; border-radius: 5px; padding: 8px; font-size: 14px; background: hsla(${this.projectStyle() * 30}, 60%, 80%, 1); `, ) } ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/app/services/projects.service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' interface Project { id: number name: string } interface ProjectResponse { data: Array nextId: number | undefined previousId: number | undefined } @Injectable({ providedIn: 'root', }) export class ProjectsService { readonly #http = inject(HttpClient) getProjects = (page: number) => this.#http.get(`/api/projects?cursor=${page}`) } ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/index.html ================================================ TanStack Query Angular infinite query example ================================================ FILE: examples/angular/infinite-query-with-max-pages/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/infinite-query-with-max-pages/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/infinite-query-with-max-pages/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/optimistic-updates/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/optimistic-updates/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/optimistic-updates/README.md ================================================ # TanStack Query Angular optimistic-updates example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/optimistic-updates/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "optimistic-updates": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/optimistic-updates", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "optimistic-updates:build:production" }, "development": { "buildTarget": "optimistic-updates:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "optimistic-updates:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/optimistic-updates/package.json ================================================ { "name": "@tanstack/query-example-angular-optimistic-updates", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/optimistic-updates/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { OptimisticUpdatesComponent } from './components/optimistic-updates.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-root', template: ``, imports: [OptimisticUpdatesComponent], }) export class AppComponent {} ================================================ FILE: examples/angular/optimistic-updates/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch, withInterceptors, } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { mockInterceptor } from './interceptor/mock-api.interceptor' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch(), withInterceptors([mockInterceptor])), provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }), withDevtools(), ), ], } ================================================ FILE: examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { injectMutation, injectQuery, } from '@tanstack/angular-query-experimental' import { FormsModule } from '@angular/forms' import { DatePipe } from '@angular/common' import { TasksService } from '../services/tasks.service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'optimistic-updates', imports: [FormsModule, DatePipe], template: `

    In this example, new items can be created using a mutation. The new item will be optimistically added to the list in hopes that the server accepts the item. If it does, the list is refetched with the true items from the list. Every now and then, the mutation may fail though. When that happens, the previous list of items is restored and the list is again refetched from the server.


    @if (tasks.isLoading()) {

    Loading...

    }
      @for (task of tasks.data(); track task) {
    • {{ task }}
    • }
    Updated At: {{ tasks.dataUpdatedAt() | date: 'MMMM d, h:mm:ss a ' }}
    @if (!tasks.isLoading() && tasks.isFetching()) {

    Fetching in background

    }
    `, }) export class OptimisticUpdatesComponent { #tasksService = inject(TasksService) tasks = injectQuery(() => this.#tasksService.allTasks()) clearMutation = injectMutation(() => this.#tasksService.addTask()) addMutation = injectMutation(() => this.#tasksService.addTask()) newItem = '' failMutation = false addItem() { if (!this.newItem) return this.addMutation.mutate({ task: this.newItem, failMutation: this.failMutation, }) this.newItem = '' } } ================================================ FILE: examples/angular/optimistic-updates/src/app/interceptor/mock-api.interceptor.ts ================================================ /** * MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints. * It handles the following operations: * - GET: Fetches all tasks from sessionStorage. * - POST: Adds a new task to sessionStorage. * Simulated responses include a delay to mimic network latency. */ import { HttpResponse } from '@angular/common/http' import { delay, of } from 'rxjs' import type { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest, } from '@angular/common/http' import type { Observable } from 'rxjs' export const mockInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn, ): Observable> => { const respondWith = (status: number, body: any) => of(new HttpResponse({ status, body })).pipe(delay(1000)) if (req.url === '/api/tasks') { switch (req.method) { case 'GET': return respondWith( 200, JSON.parse( sessionStorage.getItem('optimistic-updates-tasks') || '[]', ), ) case 'POST': const tasks = JSON.parse( sessionStorage.getItem('optimistic-updates-tasks') || '[]', ) tasks.push(req.body) sessionStorage.setItem( 'optimistic-updates-tasks', JSON.stringify(tasks), ) return respondWith(201, { status: 'success', task: req.body, }) } } if (req.url === '/api/tasks-wrong-url') { return respondWith(500, { status: 'error', }) } return next(req) } ================================================ FILE: examples/angular/optimistic-updates/src/app/services/tasks.service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' import { QueryClient, mutationOptions, queryOptions, } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' @Injectable({ providedIn: 'root', }) export class TasksService { #queryClient = inject(QueryClient) // Manages query state and caching #http = inject(HttpClient) // Handles HTTP requests /** * Fetches all tasks from the API. * Returns an observable containing an array of task strings. */ allTasks = () => queryOptions({ queryKey: ['tasks'], queryFn: () => { return lastValueFrom(this.#http.get>('/api/tasks')) }, }) /** * Creates a mutation for adding a task. * On success, invalidates and refetches the "tasks" query cache to update the task list. */ addTask() { return mutationOptions({ mutationFn: ({ task, failMutation = false, }: { task: string failMutation: boolean }) => lastValueFrom( this.#http.post( `/api/tasks${failMutation ? '-wrong-url' : ''}`, task, ), ), mutationKey: ['tasks'], onSuccess: () => { this.#queryClient.invalidateQueries({ queryKey: ['tasks'] }) }, onMutate: async ({ task }) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await this.#queryClient.cancelQueries({ queryKey: ['tasks'] }) // Snapshot the previous value const previousTodos = this.#queryClient.getQueryData>([ 'tasks', ]) // Optimistically update to the new value if (previousTodos) { this.#queryClient.setQueryData>( ['tasks'], [...previousTodos, task], ) } return previousTodos }, onError: (err, variables, context) => { if (context) { this.#queryClient.setQueryData>(['tasks'], context) } }, // Always refetch after error or success: onSettled: () => { this.#queryClient.invalidateQueries({ queryKey: ['tasks'] }) }, }) } } ================================================ FILE: examples/angular/optimistic-updates/src/index.html ================================================ TanStack Query Angular Optimistic Updates Example ================================================ FILE: examples/angular/optimistic-updates/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/optimistic-updates/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/optimistic-updates/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/pagination/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/pagination/README.md ================================================ # TanStack Query Angular pagination example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/pagination/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "pagination": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/pagination", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "pagination:build:production" }, "development": { "buildTarget": "pagination:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "pagination:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/pagination/package.json ================================================ { "name": "@tanstack/query-example-angular-pagination", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/pagination/src/app/api/projects-mock.interceptor.ts ================================================ import { HttpResponse } from '@angular/common/http' import { delay, of } from 'rxjs' import type { Observable } from 'rxjs' import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http' export const projectsMockInterceptor: HttpInterceptorFn = ( req, next, ): Observable> => { const { url } = req if (url.includes('/api/projects')) { const page = parseInt( new URLSearchParams(req.url.split('?')[1]).get('page') || '0', 10, ) const pageSize = 10 const projects = Array(pageSize) .fill(0) .map((_, i) => { const id = page * pageSize + (i + 1) return { name: 'Project ' + id, id, } }) return of( new HttpResponse({ status: 200, body: { projects, hasMore: page < 9, }, }), ).pipe(delay(1000)) } return next(req) } ================================================ FILE: examples/angular/pagination/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ExampleComponent } from './components/example.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-root', template: ``, imports: [ExampleComponent], }) export class AppComponent {} ================================================ FILE: examples/angular/pagination/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch, withInterceptors, } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { projectsMockInterceptor } from './api/projects-mock.interceptor' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withInterceptors([projectsMockInterceptor]), withFetch()), provideTanStackQuery(new QueryClient(), withDevtools()), ], } ================================================ FILE: examples/angular/pagination/src/app/components/example.component.html ================================================

    In this example, each page of data remains visible as the next page is fetched. The buttons and capability to proceed to the next page are also supressed until the next page cursor is known. Each page is cached as a normal query too, so when going to previous pages, you'll see them instantaneously while they are also refetched invisibly in the background.

    @if (query.isPending()) {
    Loading...
    } @else if (query.isError()) {
    Error: {{ query.error().message }}
    } @else if (query.isSuccess()) {
    @for (project of query.data().projects; track project.id) {

    {{ project.name }}

    }
    }
    Current Page: {{ page() + 1 }}
    @if (query.isFetching()) { Loading... }
    ================================================ FILE: examples/angular/pagination/src/app/components/example.component.ts ================================================ import { ChangeDetectionStrategy, Component, effect, inject, signal, untracked, } from '@angular/core' import { QueryClient, injectQuery, keepPreviousData, } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { ProjectsService } from '../services/projects.service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'example', templateUrl: './example.component.html', }) export class ExampleComponent { readonly queryClient = inject(QueryClient) readonly projectsService = inject(ProjectsService) readonly page = signal(0) readonly query = injectQuery(() => ({ queryKey: ['projects', this.page()], queryFn: () => { return lastValueFrom(this.projectsService.getProjects(this.page())) }, placeholderData: keepPreviousData, staleTime: 5000, })) readonly prefetchEffect = effect(() => { const data = this.query.data() const isPlaceholderData = this.query.isPlaceholderData() const newPage = this.page() + 1 untracked(() => { if (!isPlaceholderData && data?.hasMore) { void this.queryClient.prefetchQuery({ queryKey: ['projects', newPage], queryFn: () => lastValueFrom(this.projectsService.getProjects(newPage)), }) } }) }) previousPage() { this.page.update((currentPage) => { return Math.max(currentPage - 1, 0) }) } nextPage() { this.page.update((currentPage) => { return this.query.data()?.hasMore ? currentPage + 1 : currentPage }) } } ================================================ FILE: examples/angular/pagination/src/app/services/projects.service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' interface Project { id: number name: string } interface ProjectResponse { projects: Array hasMore: boolean } @Injectable({ providedIn: 'root', }) export class ProjectsService { readonly #http = inject(HttpClient) getProjects(page: number) { return this.#http.get(`/api/projects?page=${page}`) } } ================================================ FILE: examples/angular/pagination/src/index.html ================================================ TanStack Query Angular pagination example ================================================ FILE: examples/angular/pagination/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/pagination/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/pagination/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/query-options-from-a-service/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/query-options-from-a-service/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/query-options-from-a-service/README.md ================================================ # TanStack Query Angular query options from a service example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/query-options-from-a-service/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "query-options-from-a-service": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/query-options-from-a-service", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "query-options-from-a-service:build:production" }, "development": { "buildTarget": "query-options-from-a-service:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "query-options-from-a-service:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/query-options-from-a-service/package.json ================================================ { "name": "@tanstack/query-example-angular-query-options-from-a-service", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/query-options-from-a-service/src/app/app.component.html ================================================

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes! (You may need to throttle your network speed to simulate longer loading sequences)

    ================================================ FILE: examples/angular/query-options-from-a-service/src/app/app.component.ts ================================================ import { Component } from '@angular/core' import { RouterOutlet } from '@angular/router' @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.component.html', }) export class AppComponent {} ================================================ FILE: examples/angular/query-options-from-a-service/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch } from '@angular/common/http' import { provideRouter, withComponentInputBinding } from '@angular/router' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { routes } from './app.routes' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch()), provideRouter(routes, withComponentInputBinding()), provideTanStackQuery(new QueryClient(), withDevtools()), ], } ================================================ FILE: examples/angular/query-options-from-a-service/src/app/app.routes.ts ================================================ import type { Route } from '@angular/router' // loadComponent lazily loads the component // when the component is the default export, there is no need to handle the promise export const routes: Array = [ { path: '', loadComponent: () => import('./components/posts.component'), }, { path: 'post/:postId', loadComponent: () => import('./components/post.component'), }, ] ================================================ FILE: examples/angular/query-options-from-a-service/src/app/components/post.component.html ================================================
    @if (postQuery.isPending()) { Loading... } @else if (postQuery.isError()) { Error: {{ postQuery.error().message }} } @if (postQuery.isSuccess()) { @let post = postQuery.data();

    {{ post.title }}

    {{ post.body }}

    @if (postQuery.isFetching()) { Background Updating... } }
    ================================================ FILE: examples/angular/query-options-from-a-service/src/app/components/post.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject, input, numberAttribute, } from '@angular/core' import { RouterLink } from '@angular/router' import { injectQuery } from '@tanstack/angular-query-experimental' import { QueriesService } from '../services/queries-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'post', templateUrl: './post.component.html', imports: [RouterLink], }) export default class PostComponent { private readonly queries = inject(QueriesService) readonly postId = input.required({ transform: numberAttribute, }) readonly postQuery = injectQuery(() => this.queries.post(this.postId())) } ================================================ FILE: examples/angular/query-options-from-a-service/src/app/components/posts.component.html ================================================

    Posts

    @if (postsQuery.isPending()) { Loading... } @else if (postsQuery.isError()) { Error: {{ postsQuery.error().message }} } @else if (postsQuery.isSuccess()) {
    @for (post of postsQuery.data(); track post.id) {

    {{ post.title }}

    }
    }
    @if (postsQuery.isFetching()) { Background Updating... }
    ================================================ FILE: examples/angular/query-options-from-a-service/src/app/components/posts.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { RouterLink } from '@angular/router' import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' import { QueriesService } from '../services/queries-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'posts', templateUrl: './posts.component.html', imports: [RouterLink], }) export default class PostsComponent { private readonly queries = inject(QueriesService) readonly postsQuery = injectQuery(() => this.queries.posts()) readonly queryClient = inject(QueryClient) } ================================================ FILE: examples/angular/query-options-from-a-service/src/app/services/queries-service.ts ================================================ import { Injectable, inject } from '@angular/core' import { lastValueFrom } from 'rxjs' import { queryOptions } from '@tanstack/angular-query-experimental' import { HttpClient } from '@angular/common/http' export interface Post { id: number title: string body: string } @Injectable({ providedIn: 'root', }) export class QueriesService { private readonly http = inject(HttpClient) post(postId: number) { return queryOptions({ queryKey: ['post', postId], queryFn: () => { return lastValueFrom( this.http.get( `https://jsonplaceholder.typicode.com/posts/${postId}`, ), ) }, }) } posts() { return queryOptions({ queryKey: ['posts'], queryFn: () => lastValueFrom( this.http.get>( 'https://jsonplaceholder.typicode.com/posts', ), ), }) } } ================================================ FILE: examples/angular/query-options-from-a-service/src/index.html ================================================ TanStack Query Angular query options from a service example ================================================ FILE: examples/angular/query-options-from-a-service/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/query-options-from-a-service/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/query-options-from-a-service/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/router/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/router/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/router/README.md ================================================ # TanStack Query Angular router example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/router/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "router": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/router", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "router:build:production" }, "development": { "buildTarget": "router:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "router:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/router/package.json ================================================ { "name": "@tanstack/query-example-angular-router", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/router/src/app/app.component.html ================================================

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes! (You may need to throttle your network speed to simulate longer loading sequences)

    ================================================ FILE: examples/angular/router/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { RouterOutlet } from '@angular/router' @Component({ selector: 'app-root', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterOutlet], templateUrl: './app.component.html', }) export class AppComponent {} ================================================ FILE: examples/angular/router/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch } from '@angular/common/http' import { provideRouter, withComponentInputBinding } from '@angular/router' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { routes } from './app.routes' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch()), provideTanStackQuery(new QueryClient(), withDevtools()), provideRouter(routes, withComponentInputBinding()), ], } ================================================ FILE: examples/angular/router/src/app/app.routes.ts ================================================ import type { Route } from '@angular/router' // loadComponent lazily loads the component // when the component is the default export, there is no need to handle the promise export const routes: Array = [ { path: '', loadComponent: () => import('./components/posts.component'), }, { path: 'post/:postId', loadComponent: () => import('./components/post.component'), }, ] ================================================ FILE: examples/angular/router/src/app/components/post.component.html ================================================
    @if (postQuery.isPending()) { Loading... } @else if (postQuery.isError()) { Error: {{ postQuery.error().message }} } @if (postQuery.isSuccess()) { @let post = postQuery.data();

    {{ post.title }}

    {{ post.body }}

    @if (postQuery.isFetching()) { Background Updating... } }
    ================================================ FILE: examples/angular/router/src/app/components/post.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject, input, numberAttribute, } from '@angular/core' import { RouterLink } from '@angular/router' import { injectQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { PostsService } from '../services/posts-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'post', templateUrl: './post.component.html', imports: [RouterLink], }) export default class PostComponent { readonly #postsService = inject(PostsService) // The Angular router will automatically bind postId // as `withComponentInputBinding` is added to `provideRouter`. // See https://angular.dev/api/router/withComponentInputBinding readonly postId = input.required({ transform: numberAttribute, }) readonly postQuery = injectQuery(() => ({ queryKey: ['post', this.postId()], queryFn: () => { return lastValueFrom(this.#postsService.postById$(this.postId())) }, })) } ================================================ FILE: examples/angular/router/src/app/components/posts.component.html ================================================

    Posts

    @if (postsQuery.isPending()) { Loading... } @else if (postsQuery.isError()) { Error: {{ postsQuery.error().message }} } @else if (postsQuery.isSuccess()) {
    @for (post of postsQuery.data(); track post.id) {

    {{ post.title }}

    }
    }
    @if (postsQuery.isFetching()) { Background Updating... }
    ================================================ FILE: examples/angular/router/src/app/components/posts.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { RouterLink } from '@angular/router' import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' import { lastValueFrom } from 'rxjs' import { PostsService } from '../services/posts-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'posts', templateUrl: './posts.component.html', imports: [RouterLink], }) export default class PostsComponent { readonly #postsService = inject(PostsService) readonly postsQuery = injectQuery(() => ({ queryKey: ['posts'], queryFn: () => lastValueFrom(this.#postsService.allPosts$()), })) readonly queryClient = inject(QueryClient) } ================================================ FILE: examples/angular/router/src/app/services/posts-service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' @Injectable({ providedIn: 'root', }) export class PostsService { readonly #http = inject(HttpClient) postById$ = (postId: number) => this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`) allPosts$ = () => this.#http.get>('https://jsonplaceholder.typicode.com/posts') } export interface Post { id: number title: string body: string } ================================================ FILE: examples/angular/router/src/index.html ================================================ TanStack Query Angular router example ================================================ FILE: examples/angular/router/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/router/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/router/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/rxjs/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/rxjs/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/rxjs/README.md ================================================ # TanStack Query Angular RxJS Example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/rxjs/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "rxjs": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/rxjs", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/mockServiceWorker.js"], "styles": [], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "rxjs:build:production" }, "development": { "buildTarget": "rxjs:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "rxjs:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/rxjs/package.json ================================================ { "name": "@tanstack/query-example-angular-rxjs", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/rxjs/src/app/api/autocomplete-mock.interceptor.ts ================================================ import { HttpResponse } from '@angular/common/http' import { delayWhen, of, timer } from 'rxjs' import type { Observable } from 'rxjs' import type { HttpEvent, HttpInterceptorFn } from '@angular/common/http' export const autocompleteMockInterceptor: HttpInterceptorFn = ( req, next, ): Observable> => { const { url } = req if (url.includes('/api/autocomplete')) { const term = new URLSearchParams(req.url.split('?')[1]).get('term') || '' const data = [ 'C#', 'C++', 'Go', 'Java', 'JavaScript', 'Kotlin', 'Lisp', 'Objective-C', 'PHP', 'Perl', 'Python', 'R', 'Ruby', 'Rust', 'SQL', 'Scala', 'Shell', 'Swift', 'TypeScript', ] // Simulate network latency with a random delay between 100ms and 500ms const delayDuration = Math.random() * (500 - 100) + 100 return of( new HttpResponse({ status: 200, body: { suggestions: data.filter((item) => item.toLowerCase().startsWith(term.toLowerCase()), ), }, }), ).pipe(delayWhen(() => timer(delayDuration))) } return next(req) } ================================================ FILE: examples/angular/rxjs/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ExampleComponent } from './components/example.component' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-root', template: ``, imports: [ExampleComponent], }) export class AppComponent {} ================================================ FILE: examples/angular/rxjs/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch, withInterceptors, } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import { autocompleteMockInterceptor } from './api/autocomplete-mock.interceptor' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withFetch(), withInterceptors([autocompleteMockInterceptor]), ), provideTanStackQuery( new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }), withDevtools(), ), ], } ================================================ FILE: examples/angular/rxjs/src/app/components/example.component.html ================================================

    Search for a programming language

    @if (query.isSuccess() && query.data().suggestions.length) {
      @for (suggestion of query.data().suggestions; track suggestion) {
    • {{ suggestion }}
    • }
    }
    ================================================ FILE: examples/angular/rxjs/src/app/components/example.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { NonNullableFormBuilder, ReactiveFormsModule } from '@angular/forms' import { injectQuery, keepPreviousData, } from '@tanstack/angular-query-experimental' import { debounceTime, distinctUntilChanged, lastValueFrom } from 'rxjs' import { AutocompleteService } from '../services/autocomplete-service' @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'example', templateUrl: './example.component.html', imports: [ReactiveFormsModule], }) export class ExampleComponent { readonly #autocompleteService = inject(AutocompleteService) readonly #fb = inject(NonNullableFormBuilder) readonly form = this.#fb.group({ term: '', }) readonly term = toSignal( this.form.controls.term.valueChanges.pipe( debounceTime(300), distinctUntilChanged(), ), { initialValue: '' }, ) readonly query = injectQuery(() => ({ queryKey: ['suggestions', this.term()], queryFn: () => { return lastValueFrom( this.#autocompleteService.getSuggestions(this.term()), ) }, placeholderData: keepPreviousData, staleTime: 1000 * 60 * 5, // 5 minutes })) } ================================================ FILE: examples/angular/rxjs/src/app/services/autocomplete-service.ts ================================================ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' import { of } from 'rxjs' interface Response { suggestions: Array } @Injectable({ providedIn: 'root', }) export class AutocompleteService { readonly #http = inject(HttpClient) getSuggestions = (term: string = '') => term.trim() === '' ? of({ suggestions: [] }) : this.#http.get( `/api/autocomplete?term=${encodeURIComponent(term)}`, ) } ================================================ FILE: examples/angular/rxjs/src/index.html ================================================ TanStack Query Angular RxJS example ================================================ FILE: examples/angular/rxjs/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/rxjs/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/rxjs/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/angular/simple/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:22" } ================================================ FILE: examples/angular/simple/.eslintrc.cjs ================================================ // @ts-check /** @type {import('eslint').Linter.Config} */ const config = {} module.exports = config ================================================ FILE: examples/angular/simple/README.md ================================================ # TanStack Query Angular simple example To run this example: - `npm install` or `yarn` or `pnpm i` or `bun i` - `npm run start` or `yarn start` or `pnpm start` or `bun start` ================================================ FILE: examples/angular/simple/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "simple": { "projectType": "application", "schematics": { "@schematics/angular:component": { "inlineTemplate": true, "inlineStyle": true, "skipTests": true }, "@schematics/angular:class": { "skipTests": true }, "@schematics/angular:directive": { "skipTests": true }, "@schematics/angular:guard": { "skipTests": true }, "@schematics/angular:interceptor": { "skipTests": true }, "@schematics/angular:pipe": { "skipTests": true }, "@schematics/angular:resolver": { "skipTests": true }, "@schematics/angular:service": { "skipTests": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/simple", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "simple:build:production" }, "development": { "buildTarget": "simple:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "simple:build" } } } } }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: examples/angular/simple/package.json ================================================ { "name": "@tanstack/query-example-angular-simple", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "private": true, "dependencies": { "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@tanstack/angular-query-experimental": "^5.90.28", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.0" }, "devDependencies": { "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "typescript": "5.8.3" } } ================================================ FILE: examples/angular/simple/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { SimpleExampleComponent } from './components/simple-example.component' @Component({ selector: 'app-root', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SimpleExampleComponent], template: ``, }) export class AppComponent {} ================================================ FILE: examples/angular/simple/src/app/app.config.ts ================================================ import { provideHttpClient, withFetch } from '@angular/common/http' import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withDevtools } from '@tanstack/angular-query-experimental/devtools' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withFetch()), provideTanStackQuery(new QueryClient(), withDevtools()), ], } ================================================ FILE: examples/angular/simple/src/app/components/simple-example.component.html ================================================ @if (query.isPending()) {
    Loading...
    } @if (query.isError()) {
    An error has occurred: {{ query.error().message }}
    } @if (query.data(); as data) {

    {{ data.name }}

    {{ data.description }}

    👀 {{ data.subscribers_count }} ✨ {{ data.stargazers_count }} 🍴 {{ data.forks_count }} } ================================================ FILE: examples/angular/simple/src/app/components/simple-example.component.ts ================================================ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { injectQuery } from '@tanstack/angular-query-experimental' import { HttpClient } from '@angular/common/http' import { lastValueFrom } from 'rxjs' interface Response { name: string description: string subscribers_count: number stargazers_count: number forks_count: number } @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'simple-example', templateUrl: './simple-example.component.html', }) export class SimpleExampleComponent { readonly #http = inject(HttpClient) readonly query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => lastValueFrom( this.#http.get('https://api.github.com/repos/tanstack/query'), ), })) } ================================================ FILE: examples/angular/simple/src/index.html ================================================ TanStack Query Angular simple example ================================================ FILE: examples/angular/simple/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/simple/src/styles.css ================================================ /* You can add global styles to this file, and also import other style files */ ================================================ FILE: examples/angular/simple/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/simple/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "Bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictStandalone": true, "strictTemplates": true } } ================================================ FILE: examples/preact/simple/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/preact/simple/README.md ================================================ # `create-preact`

    Get started using Preact and Vite!

    ## Getting Started - `pnpm dev` - Starts a dev server at http://localhost:5173/ - `pnpm build` - Builds for production, emitting to `dist/` - `pnpm preview` - Starts a server at http://localhost:4173/ to test production build locally ================================================ FILE: examples/preact/simple/index.html ================================================ Vite + Preact
    ================================================ FILE: examples/preact/simple/package.json ================================================ { "name": "@tanstack/query-example-preact-simple", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/preact-query": "workspace:^", "preact": "^10.28.0" }, "devDependencies": { "@preact/preset-vite": "^2.10.2", "eslint": "^9.36.0", "eslint-config-preact": "^2.0.0", "typescript": "^5.9.3", "vite": "^6.4.1" }, "eslintConfig": { "extends": "preact" } } ================================================ FILE: examples/preact/simple/src/index.tsx ================================================ import { render } from 'preact' import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/preact-query' const queryClient = new QueryClient() export function App() { return ( ) } const Example = () => { const { isPending, error, data, isFetching } = useQuery({ queryKey: ['repoData'], queryFn: async () => { const response = await fetch( 'https://api.github.com/repos/TanStack/query', ) return await response.json() }, }) if (isPending) return 'Loading...' if (error !== null) return 'An error has occurred: ' + error.message return (

    {data.full_name}

    {data.description}

    👀 {data.subscribers_count}{' '} ✨ {data.stargazers_count}{' '} 🍴 {data.forks_count}
    {isFetching ? 'Updating...' : ''}
    ) } const app = document.getElementById('app') if (!app) throw new Error('Missing #app element') render(, app) ================================================ FILE: examples/preact/simple/src/style.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color: #222; background-color: #ffffff; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } body { margin: 0; display: flex; align-items: center; min-height: 100vh; } #app { max-width: 1280px; margin: 0 auto; text-align: center; } img { margin-bottom: 1.5rem; } img:hover { filter: drop-shadow(0 0 2em #673ab8aa); } section { margin-top: 5rem; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 1.5rem; } .resource { padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-align: left; text-decoration: none; color: #222; background-color: #f1f1f1; border: 1px solid transparent; } .resource:hover { border: 1px solid #000; box-shadow: 0 25px 50px -12px #673ab888; } @media (max-width: 639px) { #app { margin: 2rem; } section { margin-top: 5rem; grid-template-columns: 1fr; row-gap: 1rem; } } @media (prefers-color-scheme: dark) { :root { color: #ccc; background-color: #1a1a1a; } .resource { color: #ccc; background-color: #161616; } .resource:hover { border: 1px solid #bbb; } } ================================================ FILE: examples/preact/simple/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "noEmit": true, "allowJs": true, "checkJs": true, /* Preact Config */ "jsx": "react-jsx", "jsxImportSource": "preact", "skipLibCheck": true, "paths": { "react": ["./node_modules/preact/compat/"], "react-dom": ["./node_modules/preact/compat/"] } }, "include": ["node_modules/vite/client.d.ts", "**/*"] } ================================================ FILE: examples/preact/simple/vite.config.ts ================================================ import { defineConfig } from 'vite' import preact from '@preact/preset-vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [preact()], }) ================================================ FILE: examples/react/algolia/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/algolia/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/algolia/eslint.config.js ================================================ import { tanstackConfig } from '@tanstack/eslint-config' import pluginQuery from '@tanstack/eslint-plugin-query' import pluginReact from '@eslint-react/eslint-plugin' export default [ ...tanstackConfig, ...pluginQuery.configs['flat/recommended'], pluginReact.configs.recommended, ] ================================================ FILE: examples/react/algolia/index.html ================================================ TanStack Query React Algolia example App
    ================================================ FILE: examples/react/algolia/package.json ================================================ { "name": "@tanstack/query-example-react-algolia", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@algolia/client-search": "5.2.1", "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^5.91.5", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.4", "typescript": "5.8.3", "vite": "^6.4.1" } } ================================================ FILE: examples/react/algolia/src/App.tsx ================================================ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import './styles.css' import Search from './Search' const queryClient = new QueryClient() export default function App() { return (

    TanStack Query with Algolia

    ) } ================================================ FILE: examples/react/algolia/src/Search.tsx ================================================ import * as React from 'react' import SearchResults from './SearchResults' export default function Search() { const [query, setQuery] = React.useState('') const handleOnChange = (event: React.ChangeEvent) => { event.preventDefault() // It is recommended to debounce this event in prod setQuery(event.target.value) } return (
    ) } ================================================ FILE: examples/react/algolia/src/SearchResults.tsx ================================================ import useAlgolia from './useAlgolia' type Product = { name: string shortDescription: string salePrice: number } type SearchResultsProps = { query: string } export default function SearchResults({ query = '' }: SearchResultsProps) { const { hits, isLoading, isFetching, status, hasNextPage, isFetchingNextPage, fetchNextPage, } = useAlgolia({ indexName: 'bestbuy', query, hitsPerPage: 5, staleTime: 1000 * 30, // 30s gcTime: 1000 * 60 * 15, // 15m }) if (!query) return null if (isLoading) return
    Loading...
    return (
    Status: {status} {isFetching && fetching...}
    {hits && hits.length > 0 ? ( hits.map((product) => (
  • {product.name} {product.shortDescription && ( <>
    {product.shortDescription} )}
    ${product.salePrice}
  • )) ) : (

    No products found!

    )}
    {hasNextPage && (
    fetchNextPage()}> more
    )} {isFetchingNextPage && (
    Fetching next page...
    )}
    ) } ================================================ FILE: examples/react/algolia/src/algolia.ts ================================================ import { searchClient } from '@algolia/client-search' import type { Hit } from '@algolia/client-search' // From Algolia example // https://github.com/algolia/react-instantsearch const ALGOLIA_APP_ID = 'latency' const ALGOLIA_SEARCH_API_KEY = '6be0576ff61c053d5f9a3225e2a90f76' type SearchOptions = { indexName: string query: string pageParam: number hitsPerPage: number } export async function search({ indexName, query, pageParam, hitsPerPage = 10, }: SearchOptions): Promise<{ hits: Array> nextPage: number | undefined }> { const client = searchClient(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY) console.log('algolia:search', { indexName, query, pageParam, hitsPerPage }) const { hits, page, nbPages } = await client.searchSingleIndex({ indexName, searchParams: { query, page: pageParam, hitsPerPage }, }) const nextPage = page + 1 < nbPages ? page + 1 : undefined return { hits, nextPage } } ================================================ FILE: examples/react/algolia/src/index.tsx ================================================ import ReactDOM from 'react-dom/client' import App from './App' const rootElement = document.getElementById('root') as HTMLElement ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/algolia/src/styles.css ================================================ .App { font-family: sans-serif; font-size: 14px; padding: 20px; } .loading { padding-top: 20px; padding-left: 20px; color: darkmagenta; } .search-status { padding-top: 20px; padding-left: 20px; color: gray; } .search-result { padding-left: 20px; padding-top: 20px; } .search-more { color: blue; padding-left: 20px; padding-top: 20px; cursor: pointer; font-weight: bold; text-transform: uppercase; } .product { padding-bottom: 5px; } .product-name { font-weight: bold; font-size: 12px; } .product-description { font-size: 12px; } .product-price { font-size: 12px; font-weight: bold; } ================================================ FILE: examples/react/algolia/src/useAlgolia.ts ================================================ import { skipToken, useInfiniteQuery } from '@tanstack/react-query' import { search } from './algolia' export type UseAlgoliaOptions = { indexName: string query: string hitsPerPage?: number staleTime?: number gcTime?: number } export default function useAlgolia({ indexName, query, hitsPerPage = 10, staleTime, gcTime, }: UseAlgoliaOptions) { const queryInfo = useInfiniteQuery({ queryKey: ['algolia', indexName, query, hitsPerPage], queryFn: query ? ({ pageParam }) => search({ indexName, query, pageParam, hitsPerPage }) : skipToken, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextPage, staleTime, gcTime, }) const hits = queryInfo.data?.pages.map((page) => page.hits).flat() return { ...queryInfo, hits } } ================================================ FILE: examples/react/algolia/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ================================================ FILE: examples/react/algolia/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/auto-refetching/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/auto-refetching/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/auto-refetching/next.config.js ================================================ // @ts-check /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, typescript: { ignoreBuildErrors: true, }, } export default nextConfig ================================================ FILE: examples/react/auto-refetching/package.json ================================================ { "name": "@tanstack/query-example-react-auto-refetching", "private": true, "type": "module", "scripts": { "dev": "next", "build": "next build", "start": "next start" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "typescript": "5.8.3" } } ================================================ FILE: examples/react/auto-refetching/src/pages/api/data.ts ================================================ import type { NextApiRequest, NextApiResponse } from 'next' // an simple endpoint for getting current list let list = ['Item 1', 'Item 2', 'Item 3'] export default async ( req: NextApiRequest, res: NextApiResponse, ) => { if (req.query.add) { if (!list.includes(req.query.add)) { list.push(req.query.add) } } else if (req.query.clear) { list = [] } await new Promise((r) => setTimeout(r, 100)) res.json(list) } ================================================ FILE: examples/react/auto-refetching/src/pages/index.tsx ================================================ import React from 'react' import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient() export default function App() { return ( ) } function Example() { const queryClient = useQueryClient() const [intervalMs, setIntervalMs] = React.useState(1000) const [value, setValue] = React.useState('') const { status, data, error, isFetching } = useQuery({ queryKey: ['todos'], queryFn: async (): Promise> => { const response = await fetch('/api/data') return await response.json() }, // Refetch the data every second refetchInterval: intervalMs, }) const addMutation = useMutation({ mutationFn: (add: string) => fetch(`/api/data?add=${add}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const clearMutation = useMutation({ mutationFn: () => fetch(`/api/data?clear=1`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) if (status === 'pending') return

    Loading...

    if (status === 'error') return Error: {error.message} return (

    Auto Refetch with stale-time set to {intervalMs}ms

    This example is best experienced on your own machine, where you can open multiple tabs to the same localhost server and see your changes propagate between the two.

    Todo List

    { event.preventDefault() addMutation.mutate(value, { onSuccess: () => { setValue('') }, }) }} > setValue(ev.target.value)} />
      {data.map((item) => (
    • {item}
    • ))}
    ) } ================================================ FILE: examples/react/auto-refetching/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "target": "ES2017" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: examples/react/basic/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/basic/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/basic/eslint.config.js ================================================ import { tanstackConfig } from '@tanstack/eslint-config' import pluginQuery from '@tanstack/eslint-plugin-query' import pluginReact from '@eslint-react/eslint-plugin' export default [ ...tanstackConfig, ...pluginQuery.configs['flat/recommended'], pluginReact.configs.recommended, ] ================================================ FILE: examples/react/basic/index.html ================================================ TanStack Query React Basic Example App
    ================================================ FILE: examples/react/basic/package.json ================================================ { "name": "@tanstack/query-example-react-basic", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "test:eslint": "eslint ./src" }, "dependencies": { "@tanstack/query-async-storage-persister": "^5.90.27", "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-query-persist-client": "^5.90.27", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^5.91.5", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.4", "typescript": "5.8.3", "vite": "^6.4.1" }, "nx": { "targets": { "test:eslint": { "dependsOn": [ "^build" ] } } } } ================================================ FILE: examples/react/basic/src/index.tsx ================================================ import * as React from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) const persister = createAsyncStoragePersister({ storage: window.localStorage, }) type Post = { id: number title: string body: string } function usePosts() { return useQuery({ queryKey: ['posts'], queryFn: async (): Promise> => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') return await response.json() }, }) } function Posts({ setPostId, }: { setPostId: React.Dispatch> }) { const queryClient = useQueryClient() const { status, data, error, isFetching } = usePosts() return (

    Posts

    {status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } const getPostById = async (id: number): Promise => { const response = await fetch( `https://jsonplaceholder.typicode.com/posts/${id}`, ) return await response.json() } function usePost(postId: number) { return useQuery({ queryKey: ['post', postId], queryFn: () => getPostById(postId), enabled: !!postId, }) } function Post({ postId, setPostId, }: { postId: number setPostId: React.Dispatch> }) { const { status, data, error, isFetching } = usePost(postId) return (
    {!postId || status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>

    {data.title}

    {data.body}

    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } function App() { const [postId, setPostId] = React.useState(-1) return (

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes!{' '} (You may need to throttle your network speed to simulate longer loading sequences)

    {postId > -1 ? ( ) : ( )}
    ) } const rootElement = document.getElementById('root') if (!rootElement) throw new Error('Missing #root element') ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/basic/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src", "eslint.config.js"] } ================================================ FILE: examples/react/basic/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/basic-graphql-request/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/basic-graphql-request/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/basic-graphql-request/eslint.config.js ================================================ import { tanstackConfig } from '@tanstack/eslint-config' import pluginQuery from '@tanstack/eslint-plugin-query' import pluginReact from '@eslint-react/eslint-plugin' export default [ ...tanstackConfig, ...pluginQuery.configs['flat/recommended'], pluginReact.configs.recommended, ] ================================================ FILE: examples/react/basic-graphql-request/index.html ================================================ TanStack Query React Basic GraphQl Request Example App
    ================================================ FILE: examples/react/basic-graphql-request/package.json ================================================ { "name": "@tanstack/query-example-react-basic-graphql-request", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "graphql": "^16.9.0", "graphql-request": "^7.1.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.4.1" } } ================================================ FILE: examples/react/basic-graphql-request/src/index.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, QueryClientProvider, useQuery, useQueryClient, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { gql, request } from 'graphql-request' const endpoint = 'https://graphqlzero.almansi.me/api' const queryClient = new QueryClient() type Post = { id: number title: string body: string } function App() { const [postId, setPostId] = React.useState(-1) return (

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes!{' '} (You may need to throttle your network speed to simulate longer loading sequences)

    {postId > -1 ? ( ) : ( )}
    ) } function usePosts() { return useQuery({ queryKey: ['posts'], queryFn: async () => { const { posts: { data }, } = await request<{ posts: { data: Array } }>( endpoint, gql` query { posts { data { id title } } } `, ) return data }, }) } function Posts({ setPostId, }: { setPostId: React.Dispatch> }) { const queryClient = useQueryClient() const { status, data, error, isFetching } = usePosts() return (

    Posts

    {status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } function usePost(postId: number) { return useQuery({ queryKey: ['post', postId], queryFn: async () => { const { post } = await request<{ post: Post }>( endpoint, gql` query { post(id: ${postId}) { id title body } } `, ) return post }, enabled: !!postId, }) } function Post({ postId, setPostId, }: { postId: number setPostId: React.Dispatch> }) { const { status, data, error, isFetching } = usePost(postId) return (
    {!postId || status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>

    {data.title}

    {data.body}

    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } const rootElement = document.getElementById('root') as HTMLElement ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/basic-graphql-request/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src", "eslint.config.js"] } ================================================ FILE: examples/react/chat/.eslintrc ================================================ { "extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"] } ================================================ FILE: examples/react/chat/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/chat/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/chat/index.html ================================================ TanStack Query React Simple Example App
    ================================================ FILE: examples/react/chat/package.json ================================================ { "name": "@tanstack/query-example-chat", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@tailwindcss/vite": "^4.0.14", "@vitejs/plugin-react": "^4.3.4", "tailwindcss": "^4.0.14", "typescript": "5.8.3", "vite": "^6.4.1" } } ================================================ FILE: examples/react/chat/src/chat.ts ================================================ import { queryOptions, experimental_streamedQuery as streamedQuery, } from '@tanstack/react-query' const answers = [ "I'm just an example chat, I can't really answer any questions :(".split(' '), 'TanStack is great. Would you like to know more?'.split(' '), ] function chatAnswer(_question: string) { return { async *[Symbol.asyncIterator]() { const answer = answers[Math.floor(Math.random() * answers.length)] let index = 0 while (index < answer.length) { await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 300), ) yield answer[index++] } }, } } export const chatQueryOptions = (question: string) => queryOptions({ queryKey: ['chat', question], queryFn: streamedQuery({ streamFn: () => chatAnswer(question), }), staleTime: Infinity, }) ================================================ FILE: examples/react/chat/src/index.tsx ================================================ import ReactDOM from 'react-dom/client' import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import './style.css' import { useState } from 'react' import { chatQueryOptions } from './chat' import { Message } from './message' const queryClient = new QueryClient() export default function App() { return ( ) } function ChatMessage({ question }: { question: string }) { const { error, data = [], isFetching } = useQuery(chatQueryOptions(question)) if (error) return 'An error has occurred: ' + error.message return (
    ) } function Example() { const [questions, setQuestions] = useState>([]) const [currentQuestion, setCurrentQuestion] = useState('') const submitMessage = () => { setQuestions([...questions, currentQuestion]) setCurrentQuestion('') } return (

    TanStack Chat Example

    {questions.map((question) => ( ))}
    setCurrentQuestion(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { submitMessage() } }} placeholder="Type your message..." />
    ) } const rootElement = document.getElementById('root') as HTMLElement ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/chat/src/message.tsx ================================================ export function Message({ inProgress, message, }: { inProgress?: boolean message: { content: string; isQuestion: boolean } }) { return (
    {message.content} {inProgress ? '...' : null}
    ) } ================================================ FILE: examples/react/chat/src/style.css ================================================ @import 'tailwindcss'; ================================================ FILE: examples/react/chat/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src", "eslint.config.js"] } ================================================ FILE: examples/react/chat/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [tailwindcss(), react()], }) ================================================ FILE: examples/react/default-query-function/.eslintrc ================================================ { "extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"] } ================================================ FILE: examples/react/default-query-function/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/default-query-function/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/default-query-function/index.html ================================================ TanStack Query React Default Query Function Example App
    ================================================ FILE: examples/react/default-query-function/package.json ================================================ { "name": "@tanstack/query-example-react-default-query-function", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "typescript": "5.8.3", "vite": "^6.4.1" } } ================================================ FILE: examples/react/default-query-function/src/index.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, QueryClientProvider, useQuery, useQueryClient, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import type { QueryKey } from '@tanstack/react-query' type Post = { id: number title: string body: string } // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }: { queryKey: QueryKey }) => { const response = await fetch( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return await response.json() } // provide the default query function to your app via the query client const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { const [postId, setPostId] = React.useState(-1) return (

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes!{' '} (You may need to throttle your network speed to simulate longer loading sequences)

    {postId > -1 ? ( ) : ( )}
    ) } function Posts({ setPostId, }: { setPostId: React.Dispatch> }) { const queryClient = useQueryClient() // All you have to do now is pass a key! const { status, data, error, isFetching } = useQuery>({ queryKey: ['/posts'], }) return (

    Posts

    {status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } function Post({ postId, setPostId, }: { postId: number setPostId: React.Dispatch> }) { // You can even leave out the queryFn and just go straight into options const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) return (
    {!postId || status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>

    {data.title}

    {data.body}

    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } const rootElement = document.getElementById('root') as HTMLElement ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/default-query-function/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src", "eslint.config.js"] } ================================================ FILE: examples/react/default-query-function/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/devtools-panel/.eslintrc ================================================ { "extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"] } ================================================ FILE: examples/react/devtools-panel/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/devtools-panel/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/devtools-panel/index.html ================================================ TanStack Query React Devtools Panel Example App
    ================================================ FILE: examples/react/devtools-panel/package.json ================================================ { "name": "@tanstack/query-example-react-devtools-panel", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "typescript": "5.8.3", "vite": "^6.4.1" } } ================================================ FILE: examples/react/devtools-panel/src/index.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query' import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' const queryClient = new QueryClient() export default function App() { const [isOpen, setIsOpen] = React.useState(false) return ( {isOpen && setIsOpen(false)} />} ) } function Example() { const { isPending, error, data, isFetching } = useQuery({ queryKey: ['repoData'], queryFn: async () => { const response = await fetch( 'https://api.github.com/repos/TanStack/query', ) return await response.json() }, }) if (isPending) return 'Loading...' if (error) return 'An error has occurred: ' + error.message return (

    {data.full_name}

    {data.description}

    👀 {data.subscribers_count}{' '} ✨ {data.stargazers_count}{' '} 🍴 {data.forks_count}
    {isFetching ? 'Updating...' : ''}
    ) } const rootElement = document.getElementById('root') as HTMLElement ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/devtools-panel/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src", "eslint.config.js"] } ================================================ FILE: examples/react/devtools-panel/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/eslint-legacy/.eslintrc ================================================ { "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "extends": ["plugin:@tanstack/query/recommended"] } ================================================ FILE: examples/react/eslint-legacy/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/eslint-legacy/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/eslint-legacy/index.html ================================================ TanStack Query React Basic Example App
    ================================================ FILE: examples/react/eslint-legacy/package.json ================================================ { "name": "@tanstack/query-example-eslint-legacy", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "test:eslint": "ESLINT_USE_FLAT_CONFIG=false eslint ./src/**/*.tsx" }, "dependencies": { "@tanstack/query-async-storage-persister": "^5.90.27", "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-query-persist-client": "^5.90.27", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^5.91.5", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.4", "typescript": "5.8.3", "vite": "^6.4.1" }, "nx": { "targets": { "test:eslint": { "dependsOn": [ "^build" ] } } } } ================================================ FILE: examples/react/eslint-legacy/src/index.tsx ================================================ import * as React from 'react' import ReactDOM from 'react-dom/client' import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) const persister = createAsyncStoragePersister({ storage: window.localStorage, }) type Post = { id: number title: string body: string } function usePosts() { return useQuery({ queryKey: ['posts'], queryFn: async (): Promise> => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') return await response.json() }, }) } function Posts({ setPostId, }: { setPostId: React.Dispatch> }) { const queryClient = useQueryClient() const { status, data, error, isFetching } = usePosts() return (

    Posts

    {status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } const getPostById = async (id: number): Promise => { const response = await fetch( `https://jsonplaceholder.typicode.com/posts/${id}`, ) return await response.json() } function usePost(postId: number) { return useQuery({ queryKey: ['post', postId], queryFn: () => getPostById(postId), enabled: !!postId, }) } function Post({ postId, setPostId, }: { postId: number setPostId: React.Dispatch> }) { const { status, data, error, isFetching } = usePost(postId) return (
    {!postId || status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>

    {data.title}

    {data.body}

    {isFetching ? 'Background Updating...' : ' '}
    )}
    ) } function App() { const [postId, setPostId] = React.useState(-1) return (

    As you visit the posts below, you will notice them in a loading state the first time you load them. However, after you return to this list and click on any posts you have already visited again, you will see them load instantly and background refresh right before your eyes!{' '} (You may need to throttle your network speed to simulate longer loading sequences)

    {postId > -1 ? ( ) : ( )}
    ) } const rootElement = document.getElementById('root') as HTMLElement ReactDOM.createRoot(rootElement).render() ================================================ FILE: examples/react/eslint-legacy/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ================================================ FILE: examples/react/eslint-legacy/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/infinite-query-with-max-pages/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/infinite-query-with-max-pages/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/infinite-query-with-max-pages/next.config.js ================================================ // @ts-check /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, typescript: { ignoreBuildErrors: true, }, } export default nextConfig ================================================ FILE: examples/react/infinite-query-with-max-pages/package.json ================================================ { "name": "@tanstack/query-example-react-infinite-query-with-max-pages", "private": true, "type": "module", "scripts": { "dev": "next", "build": "next build", "start": "next start" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "typescript": "5.8.3" } } ================================================ FILE: examples/react/infinite-query-with-max-pages/src/pages/api/projects.ts ================================================ import type { NextApiRequest, NextApiResponse } from 'next' export default (req: NextApiRequest, res: NextApiResponse) => { const cursor = parseInt(req.query.cursor) || 0 const pageSize = 4 const data = Array(pageSize) .fill(0) .map((_, i) => { return { name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`, id: i + cursor, } }) const nextId = cursor < 20 ? data[data.length - 1].id + 1 : null const previousId = cursor > -20 ? data[0].id - pageSize : null setTimeout(() => res.json({ data, nextId, previousId }), 300) } ================================================ FILE: examples/react/infinite-query-with-max-pages/src/pages/index.tsx ================================================ import React from 'react' import { QueryClient, QueryClientProvider, useInfiniteQuery, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient() export default function App() { return ( ) } function Example() { const { status, data, error, isFetching, isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: async ({ pageParam }) => { const response = await fetch(`/api/projects?cursor=${pageParam}`) return await response.json() }, initialPageParam: 0, getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, maxPages: 3, }) return (

    Infinite Query with max pages

    4 projects per page

    3 pages max

    {status === 'pending' ? (

    Loading...

    ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    {data.pages.map((page) => ( {page.data.map((project) => (

    {project.name}

    ))}
    ))}
    {isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
    )}
    ) } ================================================ FILE: examples/react/infinite-query-with-max-pages/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "target": "ES2017" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: examples/react/load-more-infinite-scroll/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/load-more-infinite-scroll/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/load-more-infinite-scroll/next.config.js ================================================ // @ts-check /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, typescript: { ignoreBuildErrors: true, }, } export default nextConfig ================================================ FILE: examples/react/load-more-infinite-scroll/package.json ================================================ { "name": "@tanstack/query-example-react-load-more-infinite-scroll", "private": true, "type": "module", "scripts": { "dev": "next", "build": "next build", "start": "next start" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1", "react-intersection-observer": "^9.16.0" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "typescript": "5.8.3" } } ================================================ FILE: examples/react/load-more-infinite-scroll/src/pages/about.tsx ================================================ export default () => { return ( { window.history.back() e.preventDefault() }} > Back ) } ================================================ FILE: examples/react/load-more-infinite-scroll/src/pages/api/projects.ts ================================================ import type { NextApiRequest, NextApiResponse } from 'next' export default (req: NextApiRequest, res: NextApiResponse) => { const cursor = parseInt(req.query.cursor) || 0 const pageSize = 5 const data = Array(pageSize) .fill(0) .map((_, i) => { return { name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`, id: i + cursor, } }) const nextId = cursor < 10 ? data[data.length - 1].id + 1 : null const previousId = cursor > -10 ? data[0].id - pageSize : null setTimeout(() => res.json({ data, nextId, previousId }), 1000) } ================================================ FILE: examples/react/load-more-infinite-scroll/src/pages/index.tsx ================================================ import React from 'react' import Link from 'next/link' import { useInView } from 'react-intersection-observer' import { useInfiniteQuery, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient() export default function App() { return ( ) } function Example() { const { ref, inView } = useInView() const { status, data, error, isFetching, isFetchingNextPage, isFetchingPreviousPage, fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: async ({ pageParam, }): Promise<{ data: Array<{ name: string; id: number }> previousId: number nextId: number }> => { const response = await fetch(`/api/projects?cursor=${pageParam}`) return await response.json() }, initialPageParam: 0, getPreviousPageParam: (firstPage) => firstPage.previousId, getNextPageParam: (lastPage) => lastPage.nextId, }) React.useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]) return (

    Infinite Loading

    {status === 'pending' ? (

    Loading...

    ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    {data.pages.map((page) => ( {page.data.map((project) => (

    {project.name}

    ))}
    ))}
    {isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
    )}
    Go to another page
    ) } ================================================ FILE: examples/react/load-more-infinite-scroll/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "target": "ES2017" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: examples/react/nextjs/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/nextjs/README.md ================================================ # Example In this simple example, we integrate React-Query seamlessly with Next.js data fetching methods to fetch queries in the server and hydrate them in the browser. In addition to fetching and mutating data, React-Query analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run. To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/nextjs/next.config.js ================================================ // @ts-check /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, typescript: { ignoreBuildErrors: true, }, } export default nextConfig ================================================ FILE: examples/react/nextjs/package.json ================================================ { "name": "@tanstack/query-example-react-nextjs", "private": true, "type": "module", "scripts": { "dev": "next", "build": "next build", "start": "next start" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "typescript": "5.8.3" } } ================================================ FILE: examples/react/nextjs/src/components/Header.tsx ================================================ import React from 'react' import { useRouter } from 'next/router' import Link from 'next/link' export const Header = () => { const { pathname } = useRouter() return (
    Home Client-Only
    ) } ================================================ FILE: examples/react/nextjs/src/components/InfoBox.tsx ================================================ import React from 'react' const InfoBox = ({ children }: { children: React.ReactNode }) => (
    {children}
    ) export { InfoBox } ================================================ FILE: examples/react/nextjs/src/components/Layout.tsx ================================================ import React from 'react' export const Layout = ({ children }: { children: React.ReactNode }) => { return (
    {children}
    ) } ================================================ FILE: examples/react/nextjs/src/components/PostList.tsx ================================================ import React, { useState } from 'react' import { usePosts } from '../hooks/usePosts' export const PostList = () => { const [postCount, setPostCount] = useState(10) const { data, isPending, isFetching } = usePosts(postCount) if (isPending) return
    Loading
    return (
      {data?.map((post, index) => (
    • {index + 1}. {post.title}
    • ))}
    {postCount <= 90 && ( )}
    ) } ================================================ FILE: examples/react/nextjs/src/components/index.ts ================================================ export * from './Header' export * from './InfoBox' export * from './Layout' export * from './PostList' ================================================ FILE: examples/react/nextjs/src/hooks/usePosts.ts ================================================ import { useQuery } from '@tanstack/react-query' type Post = { id: number title: string body: string } const fetchPosts = async (limit = 10): Promise> => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') const data = await response.json() return data.filter((x: Post) => x.id <= limit) } const usePosts = (limit: number) => { return useQuery({ queryKey: ['posts', limit], queryFn: () => fetchPosts(limit), }) } export { usePosts, fetchPosts } ================================================ FILE: examples/react/nextjs/src/pages/_app.tsx ================================================ import React from 'react' import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import type { AppProps } from 'next/app' export default function MyApp({ Component, pageProps }: AppProps) { const [queryClient] = React.useState(() => new QueryClient()) return ( ) } ================================================ FILE: examples/react/nextjs/src/pages/client-only.tsx ================================================ import React from 'react' import { Header, InfoBox, Layout, PostList } from '../components' const ClientOnly = () => { return (
    ℹ️ This data is loaded on client and not prefetched ) } export default ClientOnly ================================================ FILE: examples/react/nextjs/src/pages/index.tsx ================================================ import React from 'react' import { QueryClient, dehydrate } from '@tanstack/react-query' import { Header, InfoBox, Layout, PostList } from '../components' import { fetchPosts } from '../hooks/usePosts' const Home = () => { return (
    ℹ️ This page shows how to use SSG with React-Query. ) } export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts', 10], queryFn: () => fetchPosts(10), }) return { props: { dehydratedState: dehydrate(queryClient), }, } } export default Home ================================================ FILE: examples/react/nextjs/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "target": "ES2017" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: examples/react/nextjs-app-prefetching/.eslintrc.cjs ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: ['plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'], settings: { react: { version: 'detect', }, }, } ================================================ FILE: examples/react/nextjs-app-prefetching/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo ================================================ FILE: examples/react/nextjs-app-prefetching/README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: examples/react/nextjs-app-prefetching/app/get-query-client.ts ================================================ import { QueryClient, defaultShouldDehydrateQuery, isServer, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } ================================================ FILE: examples/react/nextjs-app-prefetching/app/layout.tsx ================================================ import Providers from './providers' import type React from 'react' import type { Metadata } from 'next' export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ================================================ FILE: examples/react/nextjs-app-prefetching/app/page.tsx ================================================ import React from 'react' import { HydrationBoundary, dehydrate } from '@tanstack/react-query' import { pokemonOptions } from '@/app/pokemon' import { getQueryClient } from '@/app/get-query-client' import { PokemonInfo } from './pokemon-info' export default function Home() { const queryClient = getQueryClient() void queryClient.prefetchQuery(pokemonOptions) return (

    Pokemon Info

    ) } ================================================ FILE: examples/react/nextjs-app-prefetching/app/pokemon-info.tsx ================================================ 'use client' import React from 'react' import { useSuspenseQuery } from '@tanstack/react-query' import { pokemonOptions } from '@/app/pokemon' export function PokemonInfo() { const { data } = useSuspenseQuery(pokemonOptions) return (
    {data.name}

    I'm {data.name}

    ) } ================================================ FILE: examples/react/nextjs-app-prefetching/app/pokemon.ts ================================================ import { queryOptions } from '@tanstack/react-query' export const pokemonOptions = queryOptions({ queryKey: ['pokemon'], queryFn: async () => { const response = await fetch('https://pokeapi.co/api/v2/pokemon/25') return response.json() }, }) ================================================ FILE: examples/react/nextjs-app-prefetching/app/providers.tsx ================================================ 'use client' import { QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { getQueryClient } from '@/app/get-query-client' import type * as React from 'react' export default function Providers({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient() return ( {children} ) } ================================================ FILE: examples/react/nextjs-app-prefetching/next.config.js ================================================ // @ts-check /** @type {import('next').NextConfig} */ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, typescript: { ignoreBuildErrors: true, }, } export default nextConfig ================================================ FILE: examples/react/nextjs-app-prefetching/package.json ================================================ { "name": "@tanstack/query-example-react-nextjs-app-prefetching", "private": true, "type": "module", "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "typescript": "5.8.3" } } ================================================ FILE: examples/react/nextjs-app-prefetching/tsconfig.json ================================================ { "compilerOptions": { "target": "ES5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".eslintrc.cjs", ".next/dev/types/**/*.ts" ], "exclude": ["node_modules"] } ================================================ FILE: examples/react/nextjs-suspense-streaming/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo ================================================ FILE: examples/react/nextjs-suspense-streaming/next.config.js ================================================ // @ts-check /** @type {import('next').NextConfig} */ const nextConfig = { typescript: { ignoreBuildErrors: true, }, webpack: (config) => { if (config.name === 'server') config.optimization.concatenateModules = false return config }, } export default nextConfig ================================================ FILE: examples/react/nextjs-suspense-streaming/package.json ================================================ { "name": "@tanstack/query-example-nextjs-suspense-streaming", "private": true, "type": "module", "scripts": { "dev": "next dev --webpack", "build": "next build --webpack", "start": "next start" }, "dependencies": { "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-query-next-experimental": "^5.91.0", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "typescript": "5.8.3" } } ================================================ FILE: examples/react/nextjs-suspense-streaming/src/app/api/wait/route.ts ================================================ import { NextResponse } from 'next/server' export async function GET(request: Request) { const { searchParams } = new URL(request.url) const wait = Number(searchParams.get('wait')) await new Promise((resolve) => setTimeout(resolve, wait)) return NextResponse.json(`waited ${wait}ms`) } ================================================ FILE: examples/react/nextjs-suspense-streaming/src/app/layout.tsx ================================================ import { Providers } from './providers' export const metadata = { title: 'Next.js', description: 'Generated by Next.js', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ================================================ FILE: examples/react/nextjs-suspense-streaming/src/app/page.tsx ================================================ 'use client' import { isServer, useSuspenseQuery } from '@tanstack/react-query' import { Suspense } from 'react' export const runtime = 'edge' // 'nodejs' (default) | 'edge' function getBaseURL() { if (!isServer) { return '' } if (process.env.VERCEL_URL) { return `https://${process.env.VERCEL_URL}` } return 'http://localhost:3000' } const baseUrl = getBaseURL() function useWaitQuery(props: { wait: number }) { const query = useSuspenseQuery({ queryKey: ['wait', props.wait], queryFn: async () => { const path = `/api/wait?wait=${props.wait}` const url = baseUrl + path const res: string = await ( await fetch(url, { cache: 'no-store', }) ).json() return res }, }) return [query.data as string, query] as const } function MyComponent(props: { wait: number }) { const [data] = useWaitQuery(props) return
    result: {data}
    } export default function MyPage() { return ( <> waiting 100....}> waiting 200....}> waiting 300....}> waiting 400....}> waiting 500....}> waiting 600....}> waiting 700....}>
    combined Suspense-container
    waiting 800....
    waiting 900....
    waiting 1000....
    } >
    ) } ================================================ FILE: examples/react/nextjs-suspense-streaming/src/app/providers.tsx ================================================ 'use client' import { QueryClient, QueryClientProvider, isServer, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { return makeQueryClient() } else { if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { const queryClient = getQueryClient() return ( {props.children} ) } ================================================ FILE: examples/react/nextjs-suspense-streaming/tsconfig.json ================================================ { "compilerOptions": { "target": "ES5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "plugins": [ { "name": "next" } ] }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], "exclude": ["node_modules"] } ================================================ FILE: examples/react/offline/.eslintrc ================================================ { "extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"] } ================================================ FILE: examples/react/offline/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build pnpm-lock.yaml yarn.lock package-lock.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/offline/README.md ================================================ # Example To run this example: - `npm install` - `npm run dev` ================================================ FILE: examples/react/offline/index.html ================================================ TanStack Query React Offline Example App
    ================================================ FILE: examples/react/offline/package.json ================================================ { "name": "@tanstack/query-example-react-offline", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/query-async-storage-persister": "^5.90.27", "@tanstack/react-location": "^3.7.4", "@tanstack/react-query": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-query-persist-client": "^5.90.27", "msw": "^2.6.6", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.2" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "typescript": "5.8.3", "vite": "^6.4.1" }, "msw": { "workerDirectory": [ "public" ] } } ================================================ FILE: examples/react/offline/public/mockServiceWorker.js ================================================ /* eslint-disable */ /** * Mock Service Worker (2.1.7). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. */ const INTEGRITY_CHECKSUM = '223d191a56023cd36aa88c802961b911' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() self.addEventListener('install', function () { self.skipWaiting() }) self.addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()) }) self.addEventListener('message', async function (event) { const clientId = event.source.id if (!clientId || !self.clients) { return } const client = await self.clients.get(clientId) if (!client) { return } const allClients = await self.clients.matchAll({ type: 'window', }) switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', }) break } case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', payload: INTEGRITY_CHECKSUM, }) break } case 'MOCK_ACTIVATE': { activeClientIds.add(clientId) sendToClient(client, { type: 'MOCKING_ENABLED', payload: true, }) break } case 'MOCK_DEACTIVATE': { activeClientIds.delete(clientId) break } case 'CLIENT_CLOSED': { activeClientIds.delete(clientId) const remainingClients = allClients.filter((client) => { return client.id !== clientId }) // Unregister itself when there are no more clients if (remainingClients.length === 0) { self.registration.unregister() } break } } }) self.addEventListener('fetch', function (event) { const { request } = event // Bypass navigation requests. if (request.mode === 'navigate') { return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been deleted (still remains active until the next reload). if (activeClientIds.size === 0) { return } // Generate unique request ID. const requestId = crypto.randomUUID() event.respondWith(handleRequest(event, requestId)) }) async function handleRequest(event, requestId) { const client = await resolveMainClient(event) const response = await getResponse(event, client, requestId) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { ;(async function () { const responseClone = response.clone() sendToClient( client, { type: 'RESPONSE', payload: { requestId, isMockedResponse: IS_MOCKED_RESPONSE in response, type: responseClone.type, status: responseClone.status, statusText: responseClone.statusText, body: responseClone.body, headers: Object.fromEntries(responseClone.headers.entries()), }, }, [responseClone.body], ) })() } return response } // Resolve the main client for the given event. // Client that issues a request doesn't necessarily equal the client // that registered the worker. It's with the latter the worker should // communicate with during the response resolving phase. async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) if (client?.frameType === 'top-level') { return client } const allClients = await self.clients.matchAll({ type: 'window', }) return allClients .filter((client) => { // Get only those clients that are currently visible. return client.visibilityState === 'visible' }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. return activeClientIds.has(client.id) }) } async function getResponse(event, client, requestId) { const { request } = event // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). const requestClone = request.clone() function passthrough() { const headers = Object.fromEntries(requestClone.headers.entries()) // Remove internal MSW request header so the passthrough request // complies with any potential CORS preflight checks on the server. // Some servers forbid unknown request headers. delete headers['x-msw-intention'] return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. if (!client) { return passthrough() } // Bypass initial page load requests (i.e. static assets). // The absence of the immediate/parent client in the map of the active clients // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { return passthrough() } // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". const mswIntention = request.headers.get('x-msw-intention') if (['bypass', 'passthrough'].includes(mswIntention)) { return passthrough() } // Notify the client that a request has been intercepted. const requestBuffer = await request.arrayBuffer() const clientMessage = await sendToClient( client, { type: 'REQUEST', payload: { id: requestId, url: request.url, mode: request.mode, method: request.method, headers: Object.fromEntries(request.headers.entries()), cache: request.cache, credentials: request.credentials, destination: request.destination, integrity: request.integrity, redirect: request.redirect, referrer: request.referrer, referrerPolicy: request.referrerPolicy, body: requestBuffer, keepalive: request.keepalive, }, }, [requestBuffer], ) switch (clientMessage.type) { case 'MOCK_RESPONSE': { return respondWithMock(clientMessage.data) } case 'MOCK_NOT_FOUND': { return passthrough() } } return passthrough() } function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data && event.data.error) { return reject(event.data.error) } resolve(event.data) } client.postMessage( message, [channel.port2].concat(transferrables.filter(Boolean)), ) }) } async function respondWithMock(response) { // Setting response status code to 0 is a no-op. // However, when responding with a "Response.error()", the produced Response // instance will have status code set to 0. Since it's not possible to create // a Response instance with status code 0, handle that use-case separately. if (response.status === 0) { return Response.error() } const mockedResponse = new Response(response.body, response) Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { value: true, enumerable: true, }) return mockedResponse } ================================================ FILE: examples/react/offline/src/App.tsx ================================================ import * as React from 'react' import { MutationCache, QueryClient, onlineManager, useIsRestoring, useQuery, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { Toaster, toast } from 'react-hot-toast' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import { Link, Outlet, ReactLocation, Router, useMatch, } from '@tanstack/react-location' import * as api from './api' import { movieKeys, useMovie } from './movies' const persister = createAsyncStoragePersister({ storage: window.localStorage, }) const location = new ReactLocation() const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours staleTime: 2000, retry: 0, }, }, // configure global cache callbacks to show toast notifications mutationCache: new MutationCache({ onSuccess: (data) => { toast.success(data.message) }, onError: (error) => { toast.error(error.message) }, }), }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(movieKeys.all(), { mutationFn: async ({ id, comment }) => { // to avoid clashes with our optimistic update when an offline mutation continues await queryClient.cancelQueries({ queryKey: movieKeys.detail(id) }) return api.updateMovie(id, comment) }, }) export default function App() { return ( { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations().then(() => { queryClient.invalidateQueries() }) }} > ) } function Movies() { const isRestoring = useIsRestoring() return ( , }, { path: ':movieId', element: , errorElement: , loader: ({ params: { movieId } }) => queryClient.getQueryData(movieKeys.detail(movieId)) ?? // do not load if we are offline or hydrating because it returns a promise that is pending until we go online again // we just let the Detail component handle it (onlineManager.isOnline() && !isRestoring ? queryClient.fetchQuery({ queryKey: movieKeys.detail(movieId), queryFn: () => api.fetchMovie(movieId), }) : undefined), }, ]} > ) } function List() { const moviesQuery = useQuery({ queryKey: movieKeys.list(), queryFn: api.fetchMovies, }) if (moviesQuery.isLoading) { return 'Loading...' } if (moviesQuery.data) { return (

    Movies

    Try to mock offline behaviour with the button in the devtools. You can navigate around as long as there is already data in the cache. You'll get a refetch as soon as you go online again.

      {moviesQuery.data.movies.map((movie) => (
    • {movie.title}
    • ))}
    Updated at: {new Date(moviesQuery.data.ts).toLocaleTimeString()}
    {moviesQuery.isFetching && 'fetching...'}
    ) } // query will be in 'idle' fetchStatus while restoring from localStorage return null } function MovieError() { const { error } = useMatch() return (
    Back

    Couldn't load movie!

    {error.message}
    ) } function Detail() { const { params: { movieId }, } = useMatch() const { comment, setComment, updateMovie, movieQuery } = useMovie(movieId) if (movieQuery.isLoading) { return 'Loading...' } function submitForm(event: any) { event.preventDefault() updateMovie.mutate({ id: movieId, comment, }) } if (movieQuery.data) { return (
    Back

    Movie: {movieQuery.data.movie.title}

    Try to mock offline behaviour with the button in the devtools, then update the comment. The optimistic update will succeed, but the actual mutation will be paused and resumed once you go online again.

    You can also reload the page, which will make the persisted mutation resume, as you will be online again when you "come back".

    {dataEditError() ? 'Invalid Value' : ''}
    Query Explorer
    ) } const MutationDetails = () => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const { colors } = tokens const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === selectedMutationId(), ) if (!mutation) return false return mutation.state.isPaused }) const status = createSubscribeToMutationCacheBatcher((mutationCache) => { const mutations = mutationCache().getAll() const mutation = mutations.find( (m) => m.mutationId === selectedMutationId(), ) if (!mutation) return 'idle' return mutation.state.status }) const color = createMemo(() => getMutationStatusColor({ isPaused: isPaused(), status: status(), }), ) const activeMutation = createSubscribeToMutationCacheBatcher( (mutationCache) => mutationCache() .getAll() .find((mutation) => mutation.mutationId === selectedMutationId()), false, ) const getQueryStatusColors = () => { if (color() === 'gray') { return css` background-color: ${t(colors[color()][200], colors[color()][700])}; color: ${t(colors[color()][700], colors[color()][300])}; border-color: ${t(colors[color()][400], colors[color()][600])}; ` } return css` background-color: ${t(colors[color()][100], colors[color()][900])}; color: ${t(colors[color()][700], colors[color()][300])}; border-color: ${t(colors[color()][400], colors[color()][600])}; ` } return (
    Mutation Details
                  
                    
                      {displayValue(activeMutation()!.options.mutationKey, true)}
                    
                  
                
    pending {status()}
    Submitted At: {new Date( activeMutation()!.state.submittedAt, ).toLocaleTimeString()}
    Variables Details
    Context Details
    Data Explorer
    Mutations Explorer
    ) } const queryCacheMap = new Map< (q: Accessor) => any, { setter: Setter shouldUpdate: (event: QueryCacheNotifyEvent) => boolean } >() const setupQueryCacheSubscription = () => { const queryCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getQueryCache() }) const unsubscribe = queryCache().subscribe((q) => { batch(() => { for (const [callback, value] of queryCacheMap.entries()) { if (!value.shouldUpdate(q)) continue value.setter(callback(queryCache)) } }) }) onCleanup(() => { queryCacheMap.clear() unsubscribe() }) return unsubscribe } const createSubscribeToQueryCacheBatcher = ( callback: (queryCache: Accessor) => Exclude, equalityCheck: boolean = true, shouldUpdate: (event: QueryCacheNotifyEvent) => boolean = () => true, ) => { const queryCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getQueryCache() }) const [value, setValue] = createSignal( callback(queryCache), !equalityCheck ? { equals: false } : undefined, ) createEffect(() => { setValue(callback(queryCache)) }) queryCacheMap.set(callback, { setter: setValue, shouldUpdate: shouldUpdate, }) onCleanup(() => { queryCacheMap.delete(callback) }) return value } const mutationCacheMap = new Map< (q: Accessor) => any, Setter >() const setupMutationCacheSubscription = () => { const mutationCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getMutationCache() }) const unsubscribe = mutationCache().subscribe(() => { for (const [callback, setter] of mutationCacheMap.entries()) { queueMicrotask(() => { setter(callback(mutationCache)) }) } }) onCleanup(() => { mutationCacheMap.clear() unsubscribe() }) return unsubscribe } const createSubscribeToMutationCacheBatcher = ( callback: (queryCache: Accessor) => Exclude, equalityCheck: boolean = true, ) => { const mutationCache = createMemo(() => { const client = useQueryDevtoolsContext().client return client.getMutationCache() }) const [value, setValue] = createSignal( callback(mutationCache), !equalityCheck ? { equals: false } : undefined, ) createEffect(() => { setValue(callback(mutationCache)) }) mutationCacheMap.set(callback, setValue) onCleanup(() => { mutationCacheMap.delete(callback) }) return value } type DevToolsActionType = | 'REFETCH' | 'INVALIDATE' | 'RESET' | 'REMOVE' | 'TRIGGER_ERROR' | 'RESTORE_ERROR' | 'TRIGGER_LOADING' | 'RESTORE_LOADING' | 'CLEAR_MUTATION_CACHE' | 'CLEAR_QUERY_CACHE' const DEV_TOOLS_EVENT = '@tanstack/query-devtools-event' const sendDevToolsEvent = ({ type, queryHash, metadata, }: { type: DevToolsActionType queryHash?: string metadata?: Record }) => { const event = new CustomEvent(DEV_TOOLS_EVENT, { detail: { type, queryHash, metadata }, bubbles: true, cancelable: true, }) window.dispatchEvent(event) } const stylesFactory = ( theme: 'light' | 'dark', css: (typeof goober)['css'], ) => { const { colors, font, size, alpha, shadow, border } = tokens const t = (light: string, dark: string) => (theme === 'light' ? light : dark) return { devtoolsBtn: css` z-index: 100000; position: fixed; padding: 4px; text-align: left; display: flex; align-items: center; justify-content: center; border-radius: 9999px; box-shadow: ${shadow.md()}; overflow: hidden; & div { position: absolute; top: -8px; left: -8px; right: -8px; bottom: -8px; border-radius: 9999px; & svg { position: absolute; width: 100%; height: 100%; } filter: blur(6px) saturate(1.2) contrast(1.1); } &:focus-within { outline-offset: 2px; outline: 3px solid ${colors.green[600]}; } & button { position: relative; z-index: 1; padding: 0; border-radius: 9999px; background-color: transparent; border: none; height: 40px; display: flex; width: 40px; overflow: hidden; cursor: pointer; outline: none; & svg { position: absolute; width: 100%; height: 100%; } } `, panel: css` position: fixed; z-index: 9999; display: flex; gap: ${tokens.size[0.5]}; & * { box-sizing: border-box; text-transform: none; } & *::-webkit-scrollbar { width: 7px; } & *::-webkit-scrollbar-track { background: transparent; } & *::-webkit-scrollbar-thumb { background: ${t(colors.gray[300], colors.darkGray[200])}; } & *::-webkit-scrollbar-thumb:hover { background: ${t(colors.gray[400], colors.darkGray[300])}; } `, parentPanel: css` z-index: 9999; display: flex; height: 100%; gap: ${tokens.size[0.5]}; & * { box-sizing: border-box; text-transform: none; } & *::-webkit-scrollbar { width: 7px; } & *::-webkit-scrollbar-track { background: transparent; } & *::-webkit-scrollbar-thumb { background: ${t(colors.gray[300], colors.darkGray[200])}; } & *::-webkit-scrollbar-thumb:hover { background: ${t(colors.gray[400], colors.darkGray[300])}; } `, 'devtoolsBtn-position-bottom-right': css` bottom: 12px; right: 12px; `, 'devtoolsBtn-position-bottom-left': css` bottom: 12px; left: 12px; `, 'devtoolsBtn-position-top-left': css` top: 12px; left: 12px; `, 'devtoolsBtn-position-top-right': css` top: 12px; right: 12px; `, 'devtoolsBtn-position-relative': css` position: relative; `, 'panel-position-top': css` top: 0; right: 0; left: 0; max-height: 90%; min-height: ${size[14]}; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; `, 'panel-position-bottom': css` bottom: 0; right: 0; left: 0; max-height: 90%; min-height: ${size[14]}; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; `, 'panel-position-right': css` bottom: 0; right: 0; top: 0; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; max-width: 90%; `, 'panel-position-left': css` bottom: 0; left: 0; top: 0; border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; max-width: 90%; `, closeBtn: css` position: absolute; cursor: pointer; z-index: 5; display: flex; align-items: center; justify-content: center; outline: none; background-color: ${t(colors.gray[50], colors.darkGray[700])}; &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:focus-visible { outline: 2px solid ${colors.blue[600]}; } & svg { color: ${t(colors.gray[600], colors.gray[400])}; width: ${size[2]}; height: ${size[2]}; } `, 'closeBtn-position-top': css` bottom: 0; right: ${size[2]}; transform: translate(0, 100%); border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: none; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-radius: 0px 0px ${border.radius.sm} ${border.radius.sm}; padding: ${size[0.5]} ${size[1.5]} ${size[1]} ${size[1.5]}; &::after { content: ' '; position: absolute; bottom: 100%; left: -${size[2.5]}; height: ${size[1.5]}; width: calc(100% + ${size[5]}); } & svg { transform: rotate(180deg); } `, 'closeBtn-position-bottom': css` top: 0; right: ${size[2]}; transform: translate(0, -100%); border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-bottom: none; border-radius: ${border.radius.sm} ${border.radius.sm} 0px 0px; padding: ${size[1]} ${size[1.5]} ${size[0.5]} ${size[1.5]}; &::after { content: ' '; position: absolute; top: 100%; left: -${size[2.5]}; height: ${size[1.5]}; width: calc(100% + ${size[5]}); } `, 'closeBtn-position-right': css` bottom: ${size[2]}; left: 0; transform: translate(-100%, 0); border-right: none; border-left: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-radius: ${border.radius.sm} 0px 0px ${border.radius.sm}; padding: ${size[1.5]} ${size[0.5]} ${size[1.5]} ${size[1]}; &::after { content: ' '; position: absolute; left: 100%; height: calc(100% + ${size[5]}); width: ${size[1.5]}; } & svg { transform: rotate(-90deg); } `, 'closeBtn-position-left': css` bottom: ${size[2]}; right: 0; transform: translate(100%, 0); border-left: none; border-right: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid; border-radius: 0px ${border.radius.sm} ${border.radius.sm} 0px; padding: ${size[1.5]} ${size[1]} ${size[1.5]} ${size[0.5]}; &::after { content: ' '; position: absolute; right: 100%; height: calc(100% + ${size[5]}); width: ${size[1.5]}; } & svg { transform: rotate(90deg); } `, queriesContainer: css` flex: 1 1 700px; background-color: ${t(colors.gray[50], colors.darkGray[700])}; display: flex; flex-direction: column; & * { font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; } `, dragHandle: css` position: absolute; transition: background-color 0.125s ease; &:hover { background-color: ${colors.purple[400]}${t('', alpha[90])}; } &:focus { outline: none; background-color: ${colors.purple[400]}${t('', alpha[90])}; } &:focus-visible { outline: 2px solid ${colors.blue[800]}; outline-offset: -2px; background-color: ${colors.purple[400]}${t('', alpha[90])}; } z-index: 4; `, 'dragHandle-position-top': css` bottom: 0; width: 100%; height: 3px; cursor: ns-resize; `, 'dragHandle-position-bottom': css` top: 0; width: 100%; height: 3px; cursor: ns-resize; `, 'dragHandle-position-right': css` left: 0; width: 3px; height: 100%; cursor: ew-resize; `, 'dragHandle-position-left': css` right: 0; width: 3px; height: 100%; cursor: ew-resize; `, row: css` display: flex; justify-content: space-between; align-items: center; padding: ${tokens.size[2]} ${tokens.size[2.5]}; gap: ${tokens.size[2.5]}; border-bottom: ${t(colors.gray[300], colors.darkGray[500])} 1px solid; align-items: center; & > button { padding: 0; background: transparent; border: none; display: flex; gap: ${size[0.5]}; flex-direction: column; } `, logoAndToggleContainer: css` display: flex; gap: ${tokens.size[3]}; align-items: center; `, logo: css` cursor: pointer; display: flex; flex-direction: column; background-color: transparent; border: none; gap: ${tokens.size[0.5]}; padding: 0px; &:hover { opacity: 0.7; } &:focus-visible { outline-offset: 4px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, tanstackLogo: css` font-size: ${font.size.md}; font-weight: ${font.weight.bold}; line-height: ${font.lineHeight.xs}; white-space: nowrap; color: ${t(colors.gray[600], colors.gray[300])}; `, queryFlavorLogo: css` font-weight: ${font.weight.semibold}; font-size: ${font.size.xs}; background: linear-gradient( to right, ${t('#ea4037, #ff9b11', '#dd524b, #e9a03b')} ); background-clip: text; -webkit-background-clip: text; line-height: 1; -webkit-text-fill-color: transparent; white-space: nowrap; `, queryStatusContainer: css` display: flex; gap: ${tokens.size[2]}; height: min-content; `, queryStatusTag: css` display: flex; gap: ${tokens.size[1.5]}; box-sizing: border-box; height: ${tokens.size[6.5]}; background: ${t(colors.gray[50], colors.darkGray[500])}; color: ${t(colors.gray[700], colors.gray[300])}; border-radius: ${tokens.border.radius.sm}; font-size: ${font.size.sm}; padding: ${tokens.size[1]}; padding-left: ${tokens.size[1.5]}; align-items: center; font-weight: ${font.weight.medium}; border: ${t('1px solid ' + colors.gray[300], '1px solid transparent')}; user-select: none; position: relative; &:focus-visible { outline-offset: 2px; outline: 2px solid ${colors.blue[800]}; } `, queryStatusTagLabel: css` font-size: ${font.size.xs}; `, queryStatusCount: css` font-size: ${font.size.xs}; padding: 0 5px; display: flex; align-items: center; justify-content: center; color: ${t(colors.gray[500], colors.gray[400])}; background-color: ${t(colors.gray[200], colors.darkGray[300])}; border-radius: 2px; font-variant-numeric: tabular-nums; height: ${tokens.size[4.5]}; `, statusTooltip: css` position: absolute; z-index: 1; background-color: ${t(colors.gray[50], colors.darkGray[500])}; top: 100%; left: 50%; transform: translate(-50%, calc(${tokens.size[2]})); padding: ${tokens.size[0.5]} ${tokens.size[2]}; border-radius: ${tokens.border.radius.sm}; font-size: ${font.size.xs}; border: 1px solid ${t(colors.gray[400], colors.gray[600])}; color: ${t(colors['gray'][600], colors['gray'][300])}; &::before { top: 0px; content: ' '; display: block; left: 50%; transform: translate(-50%, -100%); position: absolute; border-color: transparent transparent ${t(colors.gray[400], colors.gray[600])} transparent; border-style: solid; border-width: 7px; /* transform: rotate(180deg); */ } &::after { top: 0px; content: ' '; display: block; left: 50%; transform: translate(-50%, calc(-100% + 2px)); position: absolute; border-color: transparent transparent ${t(colors.gray[100], colors.darkGray[500])} transparent; border-style: solid; border-width: 7px; } `, filtersContainer: css` display: flex; gap: ${tokens.size[2]}; & > button { cursor: pointer; padding: ${tokens.size[0.5]} ${tokens.size[1.5]} ${tokens.size[0.5]} ${tokens.size[2]}; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; color: ${t(colors.gray[700], colors.gray[300])}; font-size: ${font.size.xs}; display: flex; align-items: center; line-height: ${font.lineHeight.sm}; gap: ${tokens.size[1.5]}; max-width: 160px; &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } & svg { width: ${tokens.size[3]}; height: ${tokens.size[3]}; color: ${t(colors.gray[500], colors.gray[400])}; } } `, filterInput: css` padding: ${size[0.5]} ${size[2]}; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; display: flex; box-sizing: content-box; align-items: center; gap: ${tokens.size[1.5]}; max-width: 160px; min-width: 100px; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; height: min-content; color: ${t(colors.gray[600], colors.gray[400])}; & > svg { width: ${size[3]}; height: ${size[3]}; } & input { font-size: ${font.size.xs}; width: 100%; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: none; padding: 0; line-height: ${font.lineHeight.sm}; color: ${t(colors.gray[700], colors.gray[300])}; &::placeholder { color: ${t(colors.gray[700], colors.gray[300])}; } &:focus { outline: none; } } &:focus-within { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, filterSelect: css` padding: ${tokens.size[0.5]} ${tokens.size[2]}; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; display: flex; align-items: center; gap: ${tokens.size[1.5]}; box-sizing: content-box; max-width: 160px; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; height: min-content; & > svg { color: ${t(colors.gray[600], colors.gray[400])}; width: ${tokens.size[2]}; height: ${tokens.size[2]}; } & > select { appearance: none; color: ${t(colors.gray[700], colors.gray[300])}; min-width: 100px; line-height: ${font.lineHeight.sm}; font-size: ${font.size.xs}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: none; &:focus { outline: none; } } &:focus-within { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, actionsContainer: css` display: flex; gap: ${tokens.size[2]}; `, actionsBtn: css` border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[400])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; width: ${tokens.size[6.5]}; height: ${tokens.size[6.5]}; justify-content: center; display: flex; align-items: center; gap: ${tokens.size[1.5]}; max-width: 160px; cursor: pointer; padding: 0; &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } & svg { color: ${t(colors.gray[700], colors.gray[300])}; width: ${tokens.size[3]}; height: ${tokens.size[3]}; } &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } `, actionsBtnOffline: css` & svg { stroke: ${t(colors.yellow[700], colors.yellow[500])}; fill: ${t(colors.yellow[700], colors.yellow[500])}; } `, overflowQueryContainer: css` flex: 1; overflow-y: auto; & > div { display: flex; flex-direction: column; } `, queryRow: css` display: flex; align-items: center; padding: 0; border: none; cursor: pointer; color: ${t(colors.gray[700], colors.gray[300])}; background-color: ${t(colors.gray[50], colors.darkGray[700])}; line-height: 1; &:focus { outline: none; } &:focus-visible { outline-offset: -2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } &:hover .tsqd-query-hash { background-color: ${t(colors.gray[200], colors.darkGray[600])}; } & .tsqd-query-observer-count { padding: 0 ${tokens.size[1]}; user-select: none; min-width: ${tokens.size[6.5]}; align-self: stretch; display: flex; align-items: center; justify-content: center; font-size: ${font.size.xs}; font-weight: ${font.weight.medium}; border-bottom-width: 1px; border-bottom-style: solid; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[700])}; } & .tsqd-query-hash { user-select: text; font-size: ${font.size.xs}; display: flex; align-items: center; min-height: ${tokens.size[6]}; flex: 1; padding: ${tokens.size[1]} ${tokens.size[2]}; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; text-align: left; text-overflow: clip; word-break: break-word; } & .tsqd-query-disabled-indicator { align-self: stretch; display: flex; align-items: center; padding: 0 ${tokens.size[2]}; color: ${t(colors.gray[800], colors.gray[300])}; background-color: ${t(colors.gray[300], colors.darkGray[600])}; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; font-size: ${font.size.xs}; } & .tsqd-query-static-indicator { align-self: stretch; display: flex; align-items: center; padding: 0 ${tokens.size[2]}; color: ${t(colors.teal[800], colors.teal[300])}; background-color: ${t(colors.teal[100], colors.teal[900])}; border-bottom: 1px solid ${t(colors.teal[300], colors.teal[700])}; font-size: ${font.size.xs}; } `, selectedQueryRow: css` background-color: ${t(colors.gray[200], colors.darkGray[500])}; `, detailsContainer: css` flex: 1 1 700px; background-color: ${t(colors.gray[50], colors.darkGray[700])}; color: ${t(colors.gray[700], colors.gray[300])}; font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; display: flex; flex-direction: column; overflow-y: auto; display: flex; text-align: left; `, detailsHeader: css` font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; position: sticky; top: 0; z-index: 2; background-color: ${t(colors.gray[200], colors.darkGray[600])}; padding: ${tokens.size[1.5]} ${tokens.size[2]}; font-weight: ${font.weight.medium}; font-size: ${font.size.xs}; line-height: ${font.lineHeight.xs}; text-align: left; `, detailsBody: css` margin: ${tokens.size[1.5]} 0px ${tokens.size[2]} 0px; & > div { display: flex; align-items: stretch; padding: 0 ${tokens.size[2]}; line-height: ${font.lineHeight.sm}; justify-content: space-between; & > span { font-size: ${font.size.xs}; } & > span:nth-child(2) { font-variant-numeric: tabular-nums; } } & > div:first-child { margin-bottom: ${tokens.size[1.5]}; } & code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; margin: 0; font-size: ${font.size.xs}; line-height: ${font.lineHeight.xs}; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } & pre { margin: 0; display: flex; align-items: center; } `, queryDetailsStatus: css` border: 1px solid ${colors.darkGray[200]}; border-radius: ${tokens.border.radius.sm}; font-weight: ${font.weight.medium}; padding: ${tokens.size[1]} ${tokens.size[2.5]}; `, actionsBody: css` flex-wrap: wrap; margin: ${tokens.size[2]} 0px ${tokens.size[2]} 0px; display: flex; gap: ${tokens.size[2]}; padding: 0px ${tokens.size[2]}; & > button { font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; font-size: ${font.size.xs}; padding: ${tokens.size[1]} ${tokens.size[2]}; display: flex; border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; align-items: center; gap: ${tokens.size[2]}; font-weight: ${font.weight.medium}; line-height: ${font.lineHeight.xs}; cursor: pointer; &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:disabled { opacity: 0.6; cursor: not-allowed; } & > span { width: ${size[1.5]}; height: ${size[1.5]}; border-radius: ${tokens.border.radius.full}; } } `, actionsSelect: css` font-size: ${font.size.xs}; padding: ${tokens.size[0.5]} ${tokens.size[2]}; display: flex; border-radius: ${tokens.border.radius.sm}; overflow: hidden; background-color: ${t(colors.gray[100], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; align-items: center; gap: ${tokens.size[2]}; font-weight: ${font.weight.medium}; line-height: ${font.lineHeight.sm}; color: ${t(colors.red[500], colors.red[400])}; cursor: pointer; position: relative; &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } & > span { width: ${size[1.5]}; height: ${size[1.5]}; border-radius: ${tokens.border.radius.full}; } &:focus-within { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } & select { position: absolute; top: 0; left: 0; width: 100%; height: 100%; appearance: none; background-color: transparent; border: none; color: transparent; outline: none; } & svg path { stroke: ${tokens.colors.red[400]}; } & svg { width: ${tokens.size[2]}; height: ${tokens.size[2]}; } `, settingsMenu: css` display: flex; & * { font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; } flex-direction: column; gap: ${size[0.5]}; border-radius: ${tokens.border.radius.sm}; border: 1px solid ${t(colors.gray[300], colors.gray[700])}; background-color: ${t(colors.gray[50], colors.darkGray[600])}; font-size: ${font.size.xs}; color: ${t(colors.gray[700], colors.gray[300])}; z-index: 99999; min-width: 120px; padding: ${size[0.5]}; `, settingsSubTrigger: css` display: flex; align-items: center; justify-content: space-between; border-radius: ${tokens.border.radius.xs}; padding: ${tokens.size[1]} ${tokens.size[1]}; cursor: pointer; background-color: transparent; border: none; color: ${t(colors.gray[700], colors.gray[300])}; & svg { color: ${t(colors.gray[600], colors.gray[400])}; transform: rotate(-90deg); width: ${tokens.size[2]}; height: ${tokens.size[2]}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:focus-visible { outline-offset: 2px; outline: 2px solid ${colors.blue[800]}; } &.data-disabled { opacity: 0.6; cursor: not-allowed; } `, settingsMenuHeader: css` padding: ${tokens.size[1]} ${tokens.size[1]}; font-weight: ${font.weight.medium}; border-bottom: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; color: ${t(colors.gray[500], colors.gray[400])}; font-size: ${font.size['xs']}; `, settingsSubButton: css` display: flex; align-items: center; justify-content: space-between; color: ${t(colors.gray[700], colors.gray[300])}; font-size: ${font.size['xs']}; border-radius: ${tokens.border.radius.xs}; padding: ${tokens.size[1]} ${tokens.size[1]}; cursor: pointer; background-color: transparent; border: none; & svg { color: ${t(colors.gray[600], colors.gray[400])}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:focus-visible { outline-offset: 2px; outline: 2px solid ${colors.blue[800]}; } &[data-checked] { background-color: ${t(colors.purple[100], colors.purple[900])}; color: ${t(colors.purple[700], colors.purple[300])}; & svg { color: ${t(colors.purple[700], colors.purple[300])}; } &:hover { background-color: ${t(colors.purple[100], colors.purple[900])}; } } `, viewToggle: css` border-radius: ${tokens.border.radius.sm}; background-color: ${t(colors.gray[200], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; display: flex; padding: 0; font-size: ${font.size.xs}; color: ${t(colors.gray[700], colors.gray[300])}; overflow: hidden; &:has(:focus-visible) { outline: 2px solid ${colors.blue[800]}; } & .tsqd-radio-toggle { opacity: 0.5; display: flex; & label { display: flex; align-items: center; cursor: pointer; line-height: ${font.lineHeight.md}; } & label:hover { background-color: ${t(colors.gray[100], colors.darkGray[500])}; } } & > [data-checked] { opacity: 1; background-color: ${t(colors.gray[100], colors.darkGray[400])}; & label:hover { background-color: ${t(colors.gray[100], colors.darkGray[400])}; } } & .tsqd-radio-toggle:first-child { & label { padding: 0 ${tokens.size[1.5]} 0 ${tokens.size[2]}; } border-right: 1px solid ${t(colors.gray[300], colors.darkGray[200])}; } & .tsqd-radio-toggle:nth-child(2) { & label { padding: 0 ${tokens.size[2]} 0 ${tokens.size[1.5]}; } } `, devtoolsEditForm: css` padding: ${size[2]}; & > [data-error='true'] { outline: 2px solid ${t(colors.red[200], colors.red[800])}; outline-offset: 2px; border-radius: ${border.radius.xs}; } `, devtoolsEditTextarea: css` width: 100%; max-height: 500px; font-family: 'Fira Code', monospace; font-size: ${font.size.xs}; border-radius: ${border.radius.sm}; field-sizing: content; padding: ${size[2]}; background-color: ${t(colors.gray[100], colors.darkGray[800])}; color: ${t(colors.gray[900], colors.gray[100])}; border: 1px solid ${t(colors.gray[200], colors.gray[700])}; resize: none; &:focus { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${t(colors.blue[200], colors.blue[800])}; } `, devtoolsEditFormActions: css` display: flex; justify-content: space-between; gap: ${size[2]}; align-items: center; padding-top: ${size[1]}; font-size: ${font.size.xs}; `, devtoolsEditFormError: css` color: ${t(colors.red[700], colors.red[500])}; `, devtoolsEditFormActionContainer: css` display: flex; gap: ${size[2]}; `, devtoolsEditFormAction: css` font-family: ui-sans-serif, Inter, system-ui, sans-serif, sans-serif; font-size: ${font.size.xs}; padding: ${size[1]} ${tokens.size[2]}; display: flex; border-radius: ${border.radius.sm}; background-color: ${t(colors.gray[100], colors.darkGray[600])}; border: 1px solid ${t(colors.gray[300], colors.darkGray[400])}; align-items: center; gap: ${size[2]}; font-weight: ${font.weight.medium}; line-height: ${font.lineHeight.xs}; cursor: pointer; &:focus-visible { outline-offset: 2px; border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } &:hover { background-color: ${t(colors.gray[200], colors.darkGray[500])}; } &:disabled { opacity: 0.6; cursor: not-allowed; } `, } } const lightStyles = (css: (typeof goober)['css']) => stylesFactory('light', css) const darkStyles = (css: (typeof goober)['css']) => stylesFactory('dark', css) ================================================ FILE: packages/query-devtools/src/DevtoolsComponent.tsx ================================================ import { createLocalStorage } from '@solid-primitives/storage' import { createMemo } from 'solid-js' import { Devtools } from './Devtools' import { getPreferredColorScheme } from './utils' import { THEME_PREFERENCE } from './constants' import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts' import type { Theme } from './contexts' import type { DevtoolsComponentType } from './Devtools' const DevtoolsComponent: DevtoolsComponentType = (props) => { const [localStore, setLocalStore] = createLocalStorage({ prefix: 'TanstackQueryDevtools', }) const colorScheme = getPreferredColorScheme() const theme = createMemo(() => { const preference = (props.theme || localStore.theme_preference || THEME_PREFERENCE) as Theme if (preference !== 'system') return preference return colorScheme() }) return ( ) } export default DevtoolsComponent ================================================ FILE: packages/query-devtools/src/DevtoolsPanelComponent.tsx ================================================ import { createLocalStorage } from '@solid-primitives/storage' import { createMemo } from 'solid-js' import { ContentView, ParentPanel } from './Devtools' import { getPreferredColorScheme } from './utils' import { THEME_PREFERENCE } from './constants' import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts' import type { Theme } from './contexts' import type { DevtoolsComponentType } from './Devtools' const DevtoolsPanelComponent: DevtoolsComponentType = (props) => { const [localStore, setLocalStore] = createLocalStorage({ prefix: 'TanstackQueryDevtools', }) const colorScheme = getPreferredColorScheme() const theme = createMemo(() => { const preference = (props.theme || localStore.theme_preference || THEME_PREFERENCE) as Theme if (preference !== 'system') return preference return colorScheme() }) return ( ) } export default DevtoolsPanelComponent ================================================ FILE: packages/query-devtools/src/Explorer.tsx ================================================ import { serialize, stringify } from 'superjson' import { clsx as cx } from 'clsx' import { Index, Match, Show, Switch, createMemo, createSignal, createUniqueId, } from 'solid-js' import { Key } from '@solid-primitives/keyed' import * as goober from 'goober' import { tokens } from './theme' import { deleteNestedDataByPath, displayValue, updateNestedDataByPath, } from './utils' import { Check, CopiedCopier, Copier, ErrorCopier, List, Pencil, Trash, } from './icons' import { useQueryDevtoolsContext, useTheme } from './contexts' import type { Query } from '@tanstack/query-core' /** * Chunk elements in the array by size * * when the array cannot be chunked evenly by size, the last chunk will be * filled with the remaining elements * * @example * chunkArray(['a','b', 'c', 'd', 'e'], 2) // returns [['a','b'], ['c', 'd'], ['e']] */ function chunkArray( array: Array, size: number, ): Array> { if (size < 1) return [] let i = 0 const result: Array> = [] while (i < array.length) { result.push(array.slice(i, i + size)) i = i + size } return result } const Expander = (props: { expanded: boolean }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) return ( ) } type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy' const CopyButton = (props: { value: unknown }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const [copyState, setCopyState] = createSignal('NoCopy') return ( ) } const ClearArrayButton = (props: { dataPath: Array activeQuery: Query }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client return ( ) } const DeleteItemButton = (props: { dataPath: Array activeQuery: Query }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client return ( ) } const ToggleValueButton = (props: { dataPath: Array activeQuery: Query value: boolean }) => { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client return ( ) } type ExplorerProps = { editable?: boolean label: string value: unknown defaultExpanded?: Array dataPath?: Array activeQuery?: Query itemsDeletable?: boolean onEdit?: () => void } function isIterable(x: any): x is Iterable { return Symbol.iterator in x } export default function Explorer(props: ExplorerProps) { const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget ? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget }) : goober.css const styles = createMemo(() => { return theme() === 'dark' ? darkStyles(css) : lightStyles(css) }) const queryClient = useQueryDevtoolsContext().client const [expanded, setExpanded] = createSignal( (props.defaultExpanded || []).includes(props.label), ) const toggleExpanded = () => setExpanded((old) => !old) const [expandedPages, setExpandedPages] = createSignal>([]) const subEntries = createMemo(() => { if (Array.isArray(props.value)) { return props.value.map((d, i) => ({ label: i.toString(), value: d, })) } else if ( props.value !== null && typeof props.value === 'object' && isIterable(props.value) && typeof props.value[Symbol.iterator] === 'function' ) { if (props.value instanceof Map) { return Array.from(props.value, ([key, val]) => ({ label: key, value: val, })) } return Array.from(props.value, (val, i) => ({ label: i.toString(), value: val, })) } else if (typeof props.value === 'object' && props.value !== null) { return Object.entries(props.value).map(([key, val]) => ({ label: key, value: val, })) } return [] }) const type = createMemo(() => { if (Array.isArray(props.value)) { return 'array' } else if ( props.value !== null && typeof props.value === 'object' && isIterable(props.value) && typeof props.value[Symbol.iterator] === 'function' ) { return 'Iterable' } else if (typeof props.value === 'object' && props.value !== null) { return 'object' } return typeof props.value }) const subEntryPages = createMemo(() => chunkArray(subEntries(), 100)) const currentDataPath = props.dataPath ?? [] const inputId = createUniqueId() return (
    item.label}> {(entry) => { return ( ) }}
    1}>
    {(entries, index) => (
    entry.label}> {(entry) => ( )}
    )}
    {displayValue(props.value)} } > { const oldData = props.activeQuery!.state.data const newData = updateNestedDataByPath( oldData, currentDataPath, type() === 'number' ? changeEvent.target.valueAsNumber : changeEvent.target.value, ) queryClient.setQueryData(props.activeQuery!.queryKey, newData) }} /> {displayValue(props.value)}
    ) } const stylesFactory = ( theme: 'light' | 'dark', css: (typeof goober)['css'], ) => { const { colors, font, size, border } = tokens const t = (light: string, dark: string) => (theme === 'light' ? light : dark) return { entry: css` & * { font-size: ${font.size.xs}; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; } position: relative; outline: none; word-break: break-word; `, subEntry: css` margin: 0 0 0 0.5em; padding-left: 0.75em; border-left: 2px solid ${t(colors.gray[300], colors.darkGray[400])}; /* outline: 1px solid ${colors.teal[400]}; */ `, expander: css` & path { stroke: ${colors.gray[400]}; } & svg { width: ${size[3]}; height: ${size[3]}; } display: inline-flex; align-items: center; transition: all 0.1s ease; /* outline: 1px solid ${colors.blue[400]}; */ `, expanderButtonContainer: css` display: flex; align-items: center; line-height: ${size[4]}; min-height: ${size[4]}; gap: ${size[2]}; `, expanderButton: css` cursor: pointer; color: inherit; font: inherit; outline: inherit; height: ${size[5]}; background: transparent; border: none; padding: 0; display: inline-flex; align-items: center; gap: ${size[1]}; position: relative; /* outline: 1px solid ${colors.green[400]}; */ &:focus-visible { border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; } & svg { position: relative; left: 1px; } `, info: css` color: ${t(colors.gray[500], colors.gray[500])}; font-size: ${font.size.xs}; margin-left: ${size[1]}; /* outline: 1px solid ${colors.yellow[400]}; */ `, label: css` color: ${t(colors.gray[700], colors.gray[300])}; white-space: nowrap; `, value: css` color: ${t(colors.purple[600], colors.purple[400])}; flex-grow: 1; `, actions: css` display: inline-flex; gap: ${size[2]}; align-items: center; `, row: css` display: inline-flex; gap: ${size[2]}; width: 100%; margin: ${size[0.25]} 0px; line-height: ${size[4.5]}; align-items: center; `, editableInput: css` border: none; padding: ${size[0.5]} ${size[1]} ${size[0.5]} ${size[1.5]}; flex-grow: 1; border-radius: ${border.radius.xs}; background-color: ${t(colors.gray[200], colors.darkGray[500])}; &:hover { background-color: ${t(colors.gray[300], colors.darkGray[600])}; } `, actionButton: css` background-color: transparent; color: ${t(colors.gray[500], colors.gray[500])}; border: none; display: inline-flex; padding: 0px; align-items: center; justify-content: center; cursor: pointer; width: ${size[3]}; height: ${size[3]}; position: relative; z-index: 1; &:hover svg { color: ${t(colors.gray[600], colors.gray[400])}; } &:focus-visible { border-radius: ${border.radius.xs}; outline: 2px solid ${colors.blue[800]}; outline-offset: 2px; } `, } } const lightStyles = (css: (typeof goober)['css']) => stylesFactory('light', css) const darkStyles = (css: (typeof goober)['css']) => stylesFactory('dark', css) ================================================ FILE: packages/query-devtools/src/TanstackQueryDevtools.tsx ================================================ import { render } from 'solid-js/web' import { createSignal, lazy } from 'solid-js' import { setupStyleSheet } from './utils' import type { QueryClient, onlineManager as TOnlineManager, } from '@tanstack/query-core' import type { DevtoolsComponentType } from './Devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, QueryDevtoolsProps, Theme, } from './contexts' import type { Signal } from 'solid-js' export interface TanstackQueryDevtoolsConfig extends QueryDevtoolsProps { styleNonce?: string shadowDOMTarget?: ShadowRoot } class TanstackQueryDevtools { #client: Signal #onlineManager: typeof TOnlineManager #queryFlavor: string #version: string #isMounted = false #styleNonce?: string #shadowDOMTarget?: ShadowRoot #buttonPosition: Signal #position: Signal #initialIsOpen: Signal #errorTypes: Signal | undefined> #hideDisabledQueries: Signal #Component: DevtoolsComponentType | undefined #theme: Signal #dispose?: () => void constructor(config: TanstackQueryDevtoolsConfig) { const { client, queryFlavor, version, onlineManager, buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, theme, } = config this.#client = createSignal(client) this.#queryFlavor = queryFlavor this.#version = version this.#onlineManager = onlineManager this.#styleNonce = styleNonce this.#shadowDOMTarget = shadowDOMTarget this.#buttonPosition = createSignal(buttonPosition) this.#position = createSignal(position) this.#initialIsOpen = createSignal(initialIsOpen) this.#errorTypes = createSignal(errorTypes) this.#hideDisabledQueries = createSignal(hideDisabledQueries) this.#theme = createSignal(theme) } setButtonPosition(position: DevtoolsButtonPosition) { this.#buttonPosition[1](position) } setPosition(position: DevtoolsPosition) { this.#position[1](position) } setInitialIsOpen(isOpen: boolean) { this.#initialIsOpen[1](isOpen) } setErrorTypes(errorTypes: Array) { this.#errorTypes[1](errorTypes) } setClient(client: QueryClient) { this.#client[1](client) } setTheme(theme?: Theme) { this.#theme[1](theme) } mount(el: T) { if (this.#isMounted) { throw new Error('Devtools is already mounted') } const dispose = render(() => { const [btnPosition] = this.#buttonPosition const [pos] = this.#position const [isOpen] = this.#initialIsOpen const [errors] = this.#errorTypes const [hideDisabledQueries] = this.#hideDisabledQueries const [queryClient] = this.#client const [theme] = this.#theme let Devtools: DevtoolsComponentType if (this.#Component) { Devtools = this.#Component } else { Devtools = lazy(() => import('./DevtoolsComponent')) this.#Component = Devtools } setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget) return ( ) }, el) this.#isMounted = true this.#dispose = dispose } unmount() { if (!this.#isMounted) { throw new Error('Devtools is not mounted') } this.#dispose?.() this.#isMounted = false } } export { TanstackQueryDevtools } ================================================ FILE: packages/query-devtools/src/TanstackQueryDevtoolsPanel.tsx ================================================ import { render } from 'solid-js/web' import { createSignal, lazy } from 'solid-js' import { setupStyleSheet } from './utils' import type { QueryClient, onlineManager as TOnlineManager, } from '@tanstack/query-core' import type { DevtoolsComponentType } from './Devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, QueryDevtoolsProps, Theme, } from './contexts' import type { Signal } from 'solid-js' export interface TanstackQueryDevtoolsPanelConfig extends QueryDevtoolsProps { styleNonce?: string shadowDOMTarget?: ShadowRoot onClose?: () => unknown } class TanstackQueryDevtoolsPanel { #client: Signal #onlineManager: typeof TOnlineManager #queryFlavor: string #version: string #isMounted = false #styleNonce?: string #shadowDOMTarget?: ShadowRoot #buttonPosition: Signal #position: Signal #initialIsOpen: Signal #errorTypes: Signal | undefined> #hideDisabledQueries: Signal #onClose: Signal<(() => unknown) | undefined> #Component: DevtoolsComponentType | undefined #theme: Signal #dispose?: () => void constructor(config: TanstackQueryDevtoolsPanelConfig) { const { client, queryFlavor, version, onlineManager, buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, onClose, hideDisabledQueries, theme, } = config this.#client = createSignal(client) this.#queryFlavor = queryFlavor this.#version = version this.#onlineManager = onlineManager this.#styleNonce = styleNonce this.#shadowDOMTarget = shadowDOMTarget this.#buttonPosition = createSignal(buttonPosition) this.#position = createSignal(position) this.#initialIsOpen = createSignal(initialIsOpen) this.#errorTypes = createSignal(errorTypes) this.#hideDisabledQueries = createSignal(hideDisabledQueries) this.#onClose = createSignal(onClose) this.#theme = createSignal(theme) } setButtonPosition(position: DevtoolsButtonPosition) { this.#buttonPosition[1](position) } setPosition(position: DevtoolsPosition) { this.#position[1](position) } setInitialIsOpen(isOpen: boolean) { this.#initialIsOpen[1](isOpen) } setErrorTypes(errorTypes: Array) { this.#errorTypes[1](errorTypes) } setClient(client: QueryClient) { this.#client[1](client) } setOnClose(onClose: () => unknown) { this.#onClose[1](() => onClose) } setTheme(theme?: Theme) { this.#theme[1](theme) } mount(el: T) { if (this.#isMounted) { throw new Error('Devtools is already mounted') } const dispose = render(() => { const [btnPosition] = this.#buttonPosition const [pos] = this.#position const [isOpen] = this.#initialIsOpen const [errors] = this.#errorTypes const [hideDisabledQueries] = this.#hideDisabledQueries const [queryClient] = this.#client const [onClose] = this.#onClose const [theme] = this.#theme let Devtools: DevtoolsComponentType if (this.#Component) { Devtools = this.#Component } else { Devtools = lazy(() => import('./DevtoolsPanelComponent')) this.#Component = Devtools } setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget) return ( ) }, el) this.#isMounted = true this.#dispose = dispose } unmount() { if (!this.#isMounted) { throw new Error('Devtools is not mounted') } this.#dispose?.() this.#isMounted = false } } export { TanstackQueryDevtoolsPanel } ================================================ FILE: packages/query-devtools/src/__tests__/devtools.test.tsx ================================================ import { describe, expect, it } from 'vitest' describe('ReactQueryDevtools', () => { it('should be able to open and close devtools', () => { expect(1).toBe(1) }) }) ================================================ FILE: packages/query-devtools/src/__tests__/utils.test.ts ================================================ import { describe, expect, it } from 'vitest' import { deleteNestedDataByPath, updateNestedDataByPath } from '../utils' describe('Utils tests', () => { describe('updatedNestedDataByPath', () => { describe('array', () => { it('should update data correctly', () => { const oldData = ['one', 'two', 'three'] const newData = updateNestedDataByPath(oldData, ['1'], 'new') expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` [ "one", "two", "three", ] `) expect(newData).toMatchInlineSnapshot(` [ "one", "new", "three", ] `) }) }) describe('object', () => { it('should update data correctly', () => { const oldData = { title: 'Hello world', id: 1, createdAt: '2021-01-01' } const newData = updateNestedDataByPath( oldData, ['title'], 'Brave new world', ) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` { "createdAt": "2021-01-01", "id": 1, "title": "Hello world", } `) expect(newData).toMatchInlineSnapshot(` { "createdAt": "2021-01-01", "id": 1, "title": "Brave new world", } `) }) }) describe('set', () => { it('should update data correctly', () => { const oldData = new Set([123, 321, 'hello', 'world']) const newData = updateNestedDataByPath(oldData, ['2'], 'hi') expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Set { 123, 321, "hello", "world", } `) expect(newData).toMatchInlineSnapshot(` Set { 123, 321, "hi", "world", } `) }) }) describe('map', () => { it('should update data correctly', () => { const oldData = new Map([ ['en', 'hello'], ['fr', 'bonjour'], ]) /* eslint-disable cspell/spellchecker */ const newData = updateNestedDataByPath(oldData, ['fr'], 'salut') expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Map { "en" => "hello", "fr" => "bonjour", } `) expect(newData).toMatchInlineSnapshot(` Map { "en" => "hello", "fr" => "salut", } `) }) /* eslint-enable */ }) describe('nested data', () => { it('should update data correctly', () => { /* eslint-disable cspell/spellchecker */ const oldData = new Map([ [ 'pumpkin-pie', { id: 1, title: 'Pumpkin pie', ingredients: new Set(['pumpkin', 'sugar', 'spices']), steps: ['mix', 'bake', 'eat'], translations: new Map([ ['en', 'Pumpkin pie'], ['fr', 'Tarte à la citrouille'], ]), }, ], [ 'spaghetti-bolonese', { id: 2, title: 'Spaghetti bolonese', ingredients: new Set([ 'spaghetti', 'tomato sauce', 'minced meat', ]), steps: ['cook', 'eat'], translations: new Map([ ['en', 'Spaghetti bolonese'], ['fr', 'Spaghetti bolonaise'], ]), }, ], ]) const updatedObject = updateNestedDataByPath( oldData, ['pumpkin-pie', 'title'], 'Pumpkin pie with whipped cream', ) const updatedArray = updateNestedDataByPath( oldData, ['spaghetti-bolonese', 'steps', '0'], 'prepare', ) const updatedSet = updateNestedDataByPath( oldData, ['pumpkin-pie', 'ingredients', '1'], 'honey', ) const updatedMap = updateNestedDataByPath( oldData, ['pumpkin-pie', 'translations', 'en'], 'Best pie ever', ) expect(oldData).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedObject).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie with whipped cream", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedArray).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "prepare", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedSet).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "honey", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(updatedMap).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Best pie ever", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) /* eslint-enable */ }) }) }) describe('deleteNestedDataByPath', () => { it('should delete item from array correctly', () => { const oldData = ['one', 'two', 'three'] const newData = deleteNestedDataByPath(oldData, ['1']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` [ "one", "two", "three", ] `) expect(newData).toMatchInlineSnapshot(` [ "one", "three", ] `) }) it('should delete item from object correctly', () => { const oldData = { title: 'Hello world', id: 1, createdAt: '2021-01-01' } const newData = deleteNestedDataByPath(oldData, ['createdAt']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` { "createdAt": "2021-01-01", "id": 1, "title": "Hello world", } `) expect(newData).toMatchInlineSnapshot(` { "id": 1, "title": "Hello world", } `) }) it('should delete item from set', () => { const oldData = new Set([123, 321, false, true]) const newData = deleteNestedDataByPath(oldData, ['1']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Set { 123, 321, false, true, } `) expect(newData).toMatchInlineSnapshot(` Set { 123, false, true, } `) }) it('should delete item from map', () => { const oldData = new Map([ ['123', 'one'], ['hello', 'two'], ['world', 'three'], ]) const newData = deleteNestedDataByPath(oldData, ['world']) expect(newData).not.toBe(oldData) // should not be the same reference expect(oldData).toMatchInlineSnapshot(` Map { "123" => "one", "hello" => "two", "world" => "three", } `) expect(newData).toMatchInlineSnapshot(` Map { "123" => "one", "hello" => "two", } `) }) describe('nested data', () => { it('should delete nested items correctly', () => { /* eslint-disable cspell/spellchecker */ const oldData = new Map([ [ 'pumpkin-pie', { id: 1, title: 'Pumpkin pie', ingredients: new Set(['pumpkin', 'sugar', 'spices']), steps: ['mix', 'bake', 'eat'], translations: new Map([ ['en', 'Pumpkin pie'], ['fr', 'Tarte à la citrouille'], ]), }, ], [ 'spaghetti-bolonese', { id: 2, title: 'Spaghetti bolonese', ingredients: new Set([ 'spaghetti', 'tomato sauce', 'minced meat', ]), steps: ['cook', 'eat'], translations: new Map([ ['en', 'Spaghetti bolonese'], ['fr', 'Spaghetti bolonaise'], ]), }, ], ]) const deletedFromSet = deleteNestedDataByPath(oldData, [ 'spaghetti-bolonese', 'ingredients', '0', ]) const deletedFromArray = deleteNestedDataByPath(oldData, [ 'pumpkin-pie', 'steps', '1', ]) const deletedFromObject = deleteNestedDataByPath(oldData, [ 'pumpkin-pie', 'title', ]) const deletedFromMap = deleteNestedDataByPath(oldData, [ 'spaghetti-bolonese', 'translations', 'fr', ]) expect(oldData).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromSet).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromArray).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromObject).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", "fr" => "Spaghetti bolonaise", }, }, } `) expect(deletedFromMap).toMatchInlineSnapshot(` Map { "pumpkin-pie" => { "id": 1, "ingredients": Set { "pumpkin", "sugar", "spices", }, "steps": [ "mix", "bake", "eat", ], "title": "Pumpkin pie", "translations": Map { "en" => "Pumpkin pie", "fr" => "Tarte à la citrouille", }, }, "spaghetti-bolonese" => { "id": 2, "ingredients": Set { "spaghetti", "tomato sauce", "minced meat", }, "steps": [ "cook", "eat", ], "title": "Spaghetti bolonese", "translations": Map { "en" => "Spaghetti bolonese", }, }, } `) /* eslint-enable */ }) }) }) }) ================================================ FILE: packages/query-devtools/src/constants.ts ================================================ import { mutationSortFns, sortFns } from './utils' import type { DevtoolsButtonPosition, DevtoolsPosition } from './contexts' export const firstBreakpoint = 1024 export const secondBreakpoint = 796 export const thirdBreakpoint = 700 export const BUTTON_POSITION: DevtoolsButtonPosition = 'bottom-right' export const POSITION: DevtoolsPosition = 'bottom' export const THEME_PREFERENCE = 'system' export const INITIAL_IS_OPEN = false export const DEFAULT_HEIGHT = 500 export const PIP_DEFAULT_HEIGHT = 500 export const DEFAULT_WIDTH = 500 export const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0] export const DEFAULT_SORT_ORDER = 1 export const DEFAULT_MUTATION_SORT_FN_NAME = Object.keys(mutationSortFns)[0] ================================================ FILE: packages/query-devtools/src/contexts/PiPContext.tsx ================================================ import { createContext, createEffect, createMemo, createSignal, onCleanup, useContext, } from 'solid-js' import { clearDelegatedEvents, delegateEvents } from 'solid-js/web' import { PIP_DEFAULT_HEIGHT } from '../constants' import { useQueryDevtoolsContext } from './QueryDevtoolsContext' import type { Accessor, JSX } from 'solid-js' import type { StorageObject, StorageSetter } from '@solid-primitives/storage' interface PiPProviderProps { children: JSX.Element localStore: StorageObject setLocalStore: StorageSetter disabled?: boolean } type PiPContextType = { pipWindow: Window | null requestPipWindow: (width: number, height: number) => void closePipWindow: () => void disabled: boolean } class PipOpenError extends Error {} const PiPContext = createContext | undefined>( undefined, ) export const PiPProvider = (props: PiPProviderProps) => { // Expose pipWindow that is currently active const [pipWindow, setPipWindow] = createSignal(null) // Close pipWindow programmatically const closePipWindow = () => { const w = pipWindow() if (w != null) { w.close() setPipWindow(null) } } // Open new pipWindow const requestPipWindow = (width: number, height: number) => { // We don't want to allow multiple requests. if (pipWindow() != null) { return } const pip = window.open( '', 'TSQD-Devtools-Panel', `width=${width},height=${height},popup`, ) if (!pip) { throw new PipOpenError( 'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.', ) } // Remove existing styles pip.document.head.innerHTML = '' // Remove existing body pip.document.body.innerHTML = '' // Clear Delegated Events clearDelegatedEvents(pip.document) pip.document.title = 'TanStack Query Devtools' pip.document.body.style.margin = '0' // Detect when window is closed by user pip.addEventListener('pagehide', () => { props.setLocalStore('pip_open', 'false') setPipWindow(null) }) // It is important to copy all parent window styles. Otherwise, there would be no CSS available at all // https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window ;[ ...(useQueryDevtoolsContext().shadowDOMTarget || document).styleSheets, ].forEach((styleSheet) => { try { const cssRules = [...styleSheet.cssRules] .map((rule) => rule.cssText) .join('') const style = document.createElement('style') const style_node = styleSheet.ownerNode let style_id = '' if (style_node && 'id' in style_node) { style_id = style_node.id } if (style_id) { style.setAttribute('id', style_id) } style.textContent = cssRules pip.document.head.appendChild(style) } catch (e) { const link = document.createElement('link') if (styleSheet.href == null) { return } link.rel = 'stylesheet' link.type = styleSheet.type link.media = styleSheet.media.toString() link.href = styleSheet.href pip.document.head.appendChild(link) } }) delegateEvents( [ 'focusin', 'focusout', 'pointermove', 'keydown', 'pointerdown', 'pointerup', 'click', 'mousedown', 'input', ], pip.document, ) props.setLocalStore('pip_open', 'true') setPipWindow(pip) } createEffect(() => { const pip_open = (props.localStore.pip_open ?? 'false') as 'true' | 'false' if (pip_open === 'true' && !props.disabled) { try { requestPipWindow( Number(window.innerWidth), Number(props.localStore.height || PIP_DEFAULT_HEIGHT), ) } catch (error) { if (error instanceof PipOpenError) { console.error(error.message) props.setLocalStore('pip_open', 'false') props.setLocalStore('open', 'false') return } throw error } } }) createEffect(() => { // Setup mutation observer for goober styles with id `_goober const gooberStyles = ( useQueryDevtoolsContext().shadowDOMTarget || document ).querySelector('#_goober') const w = pipWindow() if (gooberStyles && w) { const observer = new MutationObserver(() => { const pip_style = ( useQueryDevtoolsContext().shadowDOMTarget || w.document ).querySelector('#_goober') if (pip_style) { pip_style.textContent = gooberStyles.textContent } }) observer.observe(gooberStyles, { childList: true, // observe direct children subtree: true, // and lower descendants too characterDataOldValue: true, // pass old data to callback }) onCleanup(() => { observer.disconnect() }) } }) const value = createMemo(() => ({ pipWindow: pipWindow(), requestPipWindow, closePipWindow, disabled: props.disabled ?? false, })) return ( {props.children} ) } export const usePiPWindow = () => { const context = createMemo(() => { const ctx = useContext(PiPContext) if (!ctx) { throw new Error('usePiPWindow must be used within a PiPProvider') } return ctx() }) return context } ================================================ FILE: packages/query-devtools/src/contexts/QueryDevtoolsContext.ts ================================================ import { createContext, useContext } from 'solid-js' import type { Query, QueryClient, onlineManager } from '@tanstack/query-core' type XPosition = 'left' | 'right' type YPosition = 'top' | 'bottom' export type DevtoolsPosition = XPosition | YPosition export type DevtoolsButtonPosition = `${YPosition}-${XPosition}` | 'relative' export type Theme = 'dark' | 'light' | 'system' export interface DevtoolsErrorType { /** * The name of the error. */ name: string /** * How the error is initialized. */ initializer: (query: Query) => Error } export interface QueryDevtoolsProps { readonly client: QueryClient queryFlavor: string version: string onlineManager: typeof onlineManager buttonPosition?: DevtoolsButtonPosition position?: DevtoolsPosition initialIsOpen?: boolean errorTypes?: Array shadowDOMTarget?: ShadowRoot onClose?: () => unknown hideDisabledQueries?: boolean theme?: Theme } export const QueryDevtoolsContext = createContext({ client: undefined as unknown as QueryClient, onlineManager: undefined as unknown as typeof onlineManager, queryFlavor: '', version: '', shadowDOMTarget: undefined, }) export function useQueryDevtoolsContext() { return useContext(QueryDevtoolsContext) } ================================================ FILE: packages/query-devtools/src/contexts/ThemeContext.ts ================================================ import { createContext, useContext } from 'solid-js' import type { Accessor } from 'solid-js' export const ThemeContext = createContext>( () => 'dark' as const, ) export function useTheme() { return useContext(ThemeContext) } ================================================ FILE: packages/query-devtools/src/contexts/index.ts ================================================ export * from './PiPContext' export * from './QueryDevtoolsContext' export * from './ThemeContext' ================================================ FILE: packages/query-devtools/src/icons/index.tsx ================================================ import { Show, createUniqueId } from 'solid-js' export function Search() { return ( ) } export function Trash() { return ( ) } export function ChevronDown() { return ( ) } export function ArrowUp() { return ( ) } export function ArrowDown() { return ( ) } export function ArrowLeft() { return ( ) } export function ArrowRight() { return ( ) } export function Sun() { return ( ) } export function Moon() { return ( ) } export function Monitor() { return ( ) } export function Wifi() { return ( ) } export function Offline() { return ( ) } export function Settings() { return ( ) } export function PiPIcon() { return ( ) } export function Copier() { return ( ) } export function Pencil() { return ( ) } export function CopiedCopier(props: { theme: 'light' | 'dark' }) { return ( ) } export function ErrorCopier() { return ( ) } export function List() { return ( ) } export function Check(props: { checked: boolean; theme: 'light' | 'dark' }) { return ( <> ) } export function CheckCircle() { return ( ) } export function LoadingCircle() { return ( ) } export function XCircle() { return ( ) } export function PauseCircle() { return ( ) } export function TanstackLogo() { const id = createUniqueId() return ( ) } ================================================ FILE: packages/query-devtools/src/index.ts ================================================ export type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, Theme, } from './contexts' export { TanstackQueryDevtools, type TanstackQueryDevtoolsConfig, } from './TanstackQueryDevtools' export { TanstackQueryDevtoolsPanel, type TanstackQueryDevtoolsPanelConfig, } from './TanstackQueryDevtoolsPanel' ================================================ FILE: packages/query-devtools/src/theme.ts ================================================ export const tokens = { colors: { inherit: 'inherit', current: 'currentColor', transparent: 'transparent', black: '#000000', white: '#ffffff', neutral: { 50: '#f9fafb', 100: '#f2f4f7', 200: '#eaecf0', 300: '#d0d5dd', 400: '#98a2b3', 500: '#667085', 600: '#475467', 700: '#344054', 800: '#1d2939', 900: '#101828', }, darkGray: { 50: '#525c7a', 100: '#49536e', 200: '#414962', 300: '#394056', 400: '#313749', 500: '#292e3d', 600: '#212530', 700: '#191c24', 800: '#111318', 900: '#0b0d10', }, gray: { 50: '#f9fafb', 100: '#f2f4f7', 200: '#eaecf0', 300: '#d0d5dd', 400: '#98a2b3', 500: '#667085', 600: '#475467', 700: '#344054', 800: '#1d2939', 900: '#101828', }, blue: { 25: '#F5FAFF', 50: '#EFF8FF', 100: '#D1E9FF', 200: '#B2DDFF', 300: '#84CAFF', 400: '#53B1FD', 500: '#2E90FA', 600: '#1570EF', 700: '#175CD3', 800: '#1849A9', 900: '#194185', }, green: { 25: '#F6FEF9', 50: '#ECFDF3', 100: '#D1FADF', 200: '#A6F4C5', 300: '#6CE9A6', 400: '#32D583', 500: '#12B76A', 600: '#039855', 700: '#027A48', 800: '#05603A', 900: '#054F31', }, red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a', }, yellow: { 25: '#FFFCF5', 50: '#FFFAEB', 100: '#FEF0C7', 200: '#FEDF89', 300: '#FEC84B', 400: '#FDB022', 500: '#F79009', 600: '#DC6803', 700: '#B54708', 800: '#93370D', 900: '#7A2E0E', }, purple: { 25: '#FAFAFF', 50: '#F4F3FF', 100: '#EBE9FE', 200: '#D9D6FE', 300: '#BDB4FE', 400: '#9B8AFB', 500: '#7A5AF8', 600: '#6938EF', 700: '#5925DC', 800: '#4A1FB8', 900: '#3E1C96', }, teal: { 25: '#F6FEFC', 50: '#F0FDF9', 100: '#CCFBEF', 200: '#99F6E0', 300: '#5FE9D0', 400: '#2ED3B7', 500: '#15B79E', 600: '#0E9384', 700: '#107569', 800: '#125D56', 900: '#134E48', }, pink: { 25: '#fdf2f8', 50: '#fce7f3', 100: '#fbcfe8', 200: '#f9a8d4', 300: '#f472b6', 400: '#ec4899', 500: '#db2777', 600: '#be185d', 700: '#9d174d', 800: '#831843', 900: '#500724', }, cyan: { 25: '#ecfeff', 50: '#cffafe', 100: '#a5f3fc', 200: '#67e8f9', 300: '#22d3ee', 400: '#06b6d4', 500: '#0891b2', 600: '#0e7490', 700: '#155e75', 800: '#164e63', 900: '#083344', }, }, alpha: { 100: 'ff', 90: 'e5', 80: 'cc', 70: 'b3', 60: '99', 50: '80', 40: '66', 30: '4d', 20: '33', 10: '1a', 0: '00', }, font: { size: { '2xs': 'calc(var(--tsqd-font-size) * 0.625)', xs: 'calc(var(--tsqd-font-size) * 0.75)', sm: 'calc(var(--tsqd-font-size) * 0.875)', md: 'var(--tsqd-font-size)', lg: 'calc(var(--tsqd-font-size) * 1.125)', xl: 'calc(var(--tsqd-font-size) * 1.25)', '2xl': 'calc(var(--tsqd-font-size) * 1.5)', '3xl': 'calc(var(--tsqd-font-size) * 1.875)', '4xl': 'calc(var(--tsqd-font-size) * 2.25)', '5xl': 'calc(var(--tsqd-font-size) * 3)', '6xl': 'calc(var(--tsqd-font-size) * 3.75)', '7xl': 'calc(var(--tsqd-font-size) * 4.5)', '8xl': 'calc(var(--tsqd-font-size) * 6)', '9xl': 'calc(var(--tsqd-font-size) * 8)', }, lineHeight: { xs: 'calc(var(--tsqd-font-size) * 1)', sm: 'calc(var(--tsqd-font-size) * 1.25)', md: 'calc(var(--tsqd-font-size) * 1.5)', lg: 'calc(var(--tsqd-font-size) * 1.75)', xl: 'calc(var(--tsqd-font-size) * 2)', '2xl': 'calc(var(--tsqd-font-size) * 2.25)', '3xl': 'calc(var(--tsqd-font-size) * 2.5)', '4xl': 'calc(var(--tsqd-font-size) * 2.75)', '5xl': 'calc(var(--tsqd-font-size) * 3)', '6xl': 'calc(var(--tsqd-font-size) * 3.25)', '7xl': 'calc(var(--tsqd-font-size) * 3.5)', '8xl': 'calc(var(--tsqd-font-size) * 3.75)', '9xl': 'calc(var(--tsqd-font-size) * 4)', }, weight: { thin: '100', extralight: '200', light: '300', normal: '400', medium: '500', semibold: '600', bold: '700', extrabold: '800', black: '900', }, }, breakpoints: { xs: '320px', sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px', }, border: { radius: { none: '0px', xs: 'calc(var(--tsqd-font-size) * 0.125)', sm: 'calc(var(--tsqd-font-size) * 0.25)', md: 'calc(var(--tsqd-font-size) * 0.375)', lg: 'calc(var(--tsqd-font-size) * 0.5)', xl: 'calc(var(--tsqd-font-size) * 0.75)', '2xl': 'calc(var(--tsqd-font-size) * 1)', '3xl': 'calc(var(--tsqd-font-size) * 1.5)', full: '9999px', }, }, size: { 0: '0px', 0.25: 'calc(var(--tsqd-font-size) * 0.0625)', 0.5: 'calc(var(--tsqd-font-size) * 0.125)', 1: 'calc(var(--tsqd-font-size) * 0.25)', 1.5: 'calc(var(--tsqd-font-size) * 0.375)', 2: 'calc(var(--tsqd-font-size) * 0.5)', 2.5: 'calc(var(--tsqd-font-size) * 0.625)', 3: 'calc(var(--tsqd-font-size) * 0.75)', 3.5: 'calc(var(--tsqd-font-size) * 0.875)', 4: 'calc(var(--tsqd-font-size) * 1)', 4.5: 'calc(var(--tsqd-font-size) * 1.125)', 5: 'calc(var(--tsqd-font-size) * 1.25)', 5.5: 'calc(var(--tsqd-font-size) * 1.375)', 6: 'calc(var(--tsqd-font-size) * 1.5)', 6.5: 'calc(var(--tsqd-font-size) * 1.625)', 7: 'calc(var(--tsqd-font-size) * 1.75)', 8: 'calc(var(--tsqd-font-size) * 2)', 9: 'calc(var(--tsqd-font-size) * 2.25)', 10: 'calc(var(--tsqd-font-size) * 2.5)', 11: 'calc(var(--tsqd-font-size) * 2.75)', 12: 'calc(var(--tsqd-font-size) * 3)', 14: 'calc(var(--tsqd-font-size) * 3.5)', 16: 'calc(var(--tsqd-font-size) * 4)', 20: 'calc(var(--tsqd-font-size) * 5)', 24: 'calc(var(--tsqd-font-size) * 6)', 28: 'calc(var(--tsqd-font-size) * 7)', 32: 'calc(var(--tsqd-font-size) * 8)', 36: 'calc(var(--tsqd-font-size) * 9)', 40: 'calc(var(--tsqd-font-size) * 10)', 44: 'calc(var(--tsqd-font-size) * 11)', 48: 'calc(var(--tsqd-font-size) * 12)', 52: 'calc(var(--tsqd-font-size) * 13)', 56: 'calc(var(--tsqd-font-size) * 14)', 60: 'calc(var(--tsqd-font-size) * 15)', 64: 'calc(var(--tsqd-font-size) * 16)', 72: 'calc(var(--tsqd-font-size) * 18)', 80: 'calc(var(--tsqd-font-size) * 20)', 96: 'calc(var(--tsqd-font-size) * 24)', }, shadow: { xs: (_: string = 'rgb(0 0 0 / 0.1)') => `0 1px 2px 0 rgb(0 0 0 / 0.05)` as const, sm: (color: string = 'rgb(0 0 0 / 0.1)') => `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}` as const, md: (color: string = 'rgb(0 0 0 / 0.1)') => `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}` as const, lg: (color: string = 'rgb(0 0 0 / 0.1)') => `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}` as const, xl: (color: string = 'rgb(0 0 0 / 0.1)') => `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}` as const, '2xl': (color: string = 'rgb(0 0 0 / 0.25)') => `0 25px 50px -12px ${color}` as const, inner: (color: string = 'rgb(0 0 0 / 0.05)') => `inset 0 2px 4px 0 ${color}` as const, none: () => `none` as const, }, zIndices: { hide: -1, auto: 'auto', base: 0, docked: 10, dropdown: 1000, sticky: 1100, banner: 1200, overlay: 1300, modal: 1400, popover: 1500, skipLink: 1600, toast: 1700, tooltip: 1800, }, } as const ================================================ FILE: packages/query-devtools/src/utils.tsx ================================================ import { serialize } from 'superjson' import { createSignal, onCleanup, onMount } from 'solid-js' import type { Mutation, Query } from '@tanstack/query-core' import type { DevtoolsPosition } from './contexts' export function getQueryStatusLabel(query: Query) { return query.state.fetchStatus === 'fetching' ? 'fetching' : !query.getObserversCount() ? 'inactive' : query.state.fetchStatus === 'paused' ? 'paused' : query.isStale() ? 'stale' : 'fresh' } type QueryStatusLabel = 'fresh' | 'stale' | 'paused' | 'inactive' | 'fetching' export function getSidedProp( prop: T, side: DevtoolsPosition, ) { return `${prop}${ side.charAt(0).toUpperCase() + side.slice(1) }` as `${T}${Capitalize}` } export function getQueryStatusColor({ queryState, observerCount, isStale, }: { queryState: Query['state'] observerCount: number isStale: boolean }) { return queryState.fetchStatus === 'fetching' ? 'blue' : !observerCount ? 'gray' : queryState.fetchStatus === 'paused' ? 'purple' : isStale ? 'yellow' : 'green' } export function getMutationStatusColor({ status, isPaused, }: { status: Mutation['state']['status'] isPaused: boolean }) { return isPaused ? 'purple' : status === 'error' ? 'red' : status === 'pending' ? 'yellow' : status === 'success' ? 'green' : 'gray' } export function getQueryStatusColorByLabel(label: QueryStatusLabel) { return label === 'fresh' ? 'green' : label === 'stale' ? 'yellow' : label === 'paused' ? 'purple' : label === 'inactive' ? 'gray' : 'blue' } /** * Displays a string regardless the type of the data * @param {unknown} value Value to be stringified * @param {boolean} beautify Formats json to multiline */ export const displayValue = (value: unknown, beautify: boolean = false) => { const { json } = serialize(value) return JSON.stringify(json, null, beautify ? 2 : undefined) } // Sorting functions type SortFn = (a: Query, b: Query) => number const getStatusRank = (q: Query) => q.state.fetchStatus !== 'idle' ? 0 : !q.getObserversCount() ? 3 : q.isStale() ? 2 : 1 const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash) const dateSort: SortFn = (a, b) => a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1 const statusAndDateSort: SortFn = (a, b) => { if (getStatusRank(a) === getStatusRank(b)) { return dateSort(a, b) } return getStatusRank(a) > getStatusRank(b) ? 1 : -1 } export const sortFns: Record = { status: statusAndDateSort, 'query hash': queryHashSort, 'last updated': dateSort, } type MutationSortFn = (a: Mutation, b: Mutation) => number const getMutationStatusRank = (m: Mutation) => m.state.isPaused ? 0 : m.state.status === 'error' ? 2 : m.state.status === 'pending' ? 1 : 3 const mutationDateSort: MutationSortFn = (a, b) => a.state.submittedAt < b.state.submittedAt ? 1 : -1 const mutationStatusSort: MutationSortFn = (a, b) => { if (getMutationStatusRank(a) === getMutationStatusRank(b)) { return mutationDateSort(a, b) } return getMutationStatusRank(a) > getMutationStatusRank(b) ? 1 : -1 } export const mutationSortFns: Record = { status: mutationStatusSort, 'last updated': mutationDateSort, } export const convertRemToPixels = (rem: number) => { return rem * parseFloat(getComputedStyle(document.documentElement).fontSize) } export const getPreferredColorScheme = () => { const [colorScheme, setColorScheme] = createSignal<'light' | 'dark'>('dark') onMount(() => { const query = window.matchMedia('(prefers-color-scheme: dark)') setColorScheme(query.matches ? 'dark' : 'light') const listener = (e: MediaQueryListEvent) => { setColorScheme(e.matches ? 'dark' : 'light') } query.addEventListener('change', listener) onCleanup(() => query.removeEventListener('change', listener)) }) return colorScheme } /** * updates nested data by path * * @param {unknown} oldData Data to be updated * @param {Array} updatePath Path to the data to be updated * @param {unknown} value New value */ export const updateNestedDataByPath = ( oldData: unknown, updatePath: Array, value: unknown, ): any => { if (updatePath.length === 0) { return value } if (oldData instanceof Map) { const newData = new Map(oldData) if (updatePath.length === 1) { newData.set(updatePath[0], value) return newData } const [head, ...tail] = updatePath newData.set(head, updateNestedDataByPath(newData.get(head), tail, value)) return newData } if (oldData instanceof Set) { const setAsArray = updateNestedDataByPath( Array.from(oldData), updatePath, value, ) return new Set(setAsArray) } if (Array.isArray(oldData)) { const newData = [...oldData] if (updatePath.length === 1) { // @ts-expect-error newData[updatePath[0]] = value return newData } const [head, ...tail] = updatePath // @ts-expect-error newData[head] = updateNestedDataByPath(newData[head], tail, value) return newData } if (oldData instanceof Object) { const newData = { ...oldData } if (updatePath.length === 1) { // @ts-expect-error newData[updatePath[0]] = value return newData } const [head, ...tail] = updatePath // @ts-expect-error newData[head] = updateNestedDataByPath(newData[head], tail, value) return newData } return oldData } /** * Deletes nested data by path * * @param {unknown} oldData Data to be updated * @param {Array} deletePath Path to the data to be deleted * @returns newData without the deleted items by path */ export const deleteNestedDataByPath = ( oldData: unknown, deletePath: Array, ): any => { if (oldData instanceof Map) { const newData = new Map(oldData) if (deletePath.length === 1) { newData.delete(deletePath[0]) return newData } const [head, ...tail] = deletePath newData.set(head, deleteNestedDataByPath(newData.get(head), tail)) return newData } if (oldData instanceof Set) { const setAsArray = deleteNestedDataByPath(Array.from(oldData), deletePath) return new Set(setAsArray) } if (Array.isArray(oldData)) { const newData = [...oldData] if (deletePath.length === 1) { return newData.filter((_, idx) => idx.toString() !== deletePath[0]) } const [head, ...tail] = deletePath // @ts-expect-error newData[head] = deleteNestedDataByPath(newData[head], tail) return newData } if (oldData instanceof Object) { const newData = { ...oldData } if (deletePath.length === 1) { // @ts-expect-error delete newData[deletePath[0]] return newData } const [head, ...tail] = deletePath // @ts-expect-error newData[head] = deleteNestedDataByPath(newData[head], tail) return newData } return oldData } // Sets up the goober stylesheet // Adds a nonce to the style tag if needed export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => { if (!nonce) return const styleExists = document.querySelector('#_goober') || target?.querySelector('#_goober') if (styleExists) return const styleTag = document.createElement('style') const textNode = document.createTextNode('') styleTag.appendChild(textNode) styleTag.id = '_goober' styleTag.setAttribute('nonce', nonce) if (target) { target.appendChild(styleTag) } else { document.head.appendChild(styleTag) } } ================================================ FILE: packages/query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "preserve", "jsxImportSource": "solid-js" }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/query-devtools/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-devtools/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { generateTsupOptions, parsePresetOptions } from 'tsup-preset-solid' const preset_options = { entries: { entry: 'src/index.ts', dev_entry: true, }, cjs: true, drop_console: true, } export default defineConfig(() => { const parsed_data = parsePresetOptions(preset_options) const tsup_options = generateTsupOptions(parsed_data) tsup_options.forEach((tsup_option) => { tsup_option.outDir = 'build' tsup_option.experimentalDts = true delete tsup_option.dts }) return tsup_options }) ================================================ FILE: packages/query-devtools/vite.config.ts ================================================ import solid from 'vite-plugin-solid' import { defineConfig } from 'vitest/config' 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', coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], exclude: ['src/__tests__/**'], }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/query-persist-client-core/CHANGELOG.md ================================================ # @tanstack/query-persist-client-core ## 5.92.4 ### 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.92.3 ### Patch Changes - Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]: - @tanstack/query-core@5.91.1 ## 5.92.2 ### Patch Changes - Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]: - @tanstack/query-core@5.91.0 ## 5.92.1 ### Patch Changes - removeItem if deserialize fails ([#10190](https://github.com/TanStack/query/pull/10190)) ## 5.92.0 ### Minor Changes - Add removeQueries to experimental_createQueryPersister ([#10186](https://github.com/TanStack/query/pull/10186)) ## 5.91.19 ### Patch Changes - Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]: - @tanstack/query-core@5.90.20 ## 5.91.18 ### Patch Changes - Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]: - @tanstack/query-core@5.90.19 ## 5.91.17 ### Patch Changes - Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]: - @tanstack/query-core@5.90.18 ## 5.91.16 ### Patch Changes - Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]: - @tanstack/query-core@5.90.17 ## 5.91.15 ### Patch Changes - Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]: - @tanstack/query-core@5.90.16 ## 5.91.14 ### Patch Changes - Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]: - @tanstack/query-core@5.90.15 ## 5.91.13 ### Patch Changes - Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]: - @tanstack/query-core@5.90.14 ## 5.91.12 ### Patch Changes - Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]: - @tanstack/query-core@5.90.13 ## 5.91.11 ### Patch Changes - Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]: - @tanstack/query-core@5.90.12 ## 5.91.10 ### Patch Changes - Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]: - @tanstack/query-core@5.90.11 ## 5.91.9 ### 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.91.8 ### Patch Changes - Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]: - @tanstack/query-core@5.90.9 ## 5.91.7 ### Patch Changes - Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]: - @tanstack/query-core@5.90.8 ## 5.91.6 ### Patch Changes - Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]: - @tanstack/query-core@5.90.7 ## 5.91.5 ### Patch Changes - Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]: - @tanstack/query-core@5.90.6 ## 5.91.4 ### Patch Changes - Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]: - @tanstack/query-core@5.90.5 ## 5.91.3 ### Patch Changes - Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]: - @tanstack/query-core@5.90.4 ## 5.91.2 ### Patch Changes - Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]: - @tanstack/query-core@5.90.3 ## 5.91.1 ### Patch Changes - allow restoring null values ([#9747](https://github.com/TanStack/query/pull/9747)) ## 5.91.0 ### Minor Changes - Added a refetchOnRestore setting to control refetching after restoring persisted queries ([#9745](https://github.com/TanStack/query/pull/9745)) ================================================ FILE: packages/query-persist-client-core/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-persist-client-core/package.json ================================================ { "name": "@tanstack/query-persist-client-core", "version": "5.92.4", "description": "Set of utilities for interacting with persisters, which can save your queryClient for later use", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/query-persist-client-core" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build", "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-persist-client-core/src/__tests__/createPersister.test.ts ================================================ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' import { Query, QueryClient, hashKey } from '@tanstack/query-core' import { PERSISTER_KEY_PREFIX, experimental_createQueryPersister, } from '../createPersister' import type { QueryFunctionContext, QueryKey } from '@tanstack/query-core' import type { StoragePersisterOptions } from '../createPersister' function getFreshStorage() { const storage = new Map() return { getItem: (key: string) => Promise.resolve(storage.get(key)), setItem: (key: string, value: unknown) => { storage.set(key, value) return Promise.resolve() }, removeItem: (key: string) => { storage.delete(key) return Promise.resolve() }, entries: () => { return Promise.resolve(Array.from(storage.entries())) }, } } function setupPersister( queryKey: QueryKey, persisterOptions: StoragePersisterOptions, ) { const client = new QueryClient() const context = { meta: { foo: 'bar' }, client, queryKey, // @ts-expect-error signal: undefined as AbortSignal, } satisfies QueryFunctionContext const queryHash = hashKey(queryKey) const storageKey = `${PERSISTER_KEY_PREFIX}-${queryHash}` const queryFn = vi.fn() const persister = experimental_createQueryPersister(persisterOptions) const query = new Query({ client, queryHash, queryKey, }) return { client, context, persister, query, queryFn, queryHash, queryKey, storageKey, } } describe('createPersister', () => { beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) test('should fetch if storage is not provided', async () => { const { context, persister, query, queryFn } = setupPersister(['foo'], { storage: undefined, }) await persister.persisterFn(queryFn, context, query) expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should fetch if there is no stored data', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn } = setupPersister(['foo'], { storage, }) await persister.persisterFn(queryFn, context, query) expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should fetch if query already has data', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn } = setupPersister(['foo'], { storage, }) query.state.data = 'baz' await persister.persisterFn(queryFn, context, query) expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should fetch if deserialization fails', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem(storageKey, '{invalid[item') await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should remove stored item if `dataUpdatedAt` is empty', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: undefined }, }), ) await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should remove stored item if its expired', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, maxAge: 100, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now() - 200 }, }), ) await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should remove stored item if its busted', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: 'bust', state: { dataUpdatedAt: Date.now() }, }), ) await persister.persisterFn(queryFn, context, query) expect(await storage.getItem(storageKey)).toBeUndefined() expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) }) test('should restore item from the storage and set proper `updatedAt` values', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) const dataUpdatedAt = Date.now() await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt, data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.data = 'data0' query.fetch = vi.fn() expect(query.state.dataUpdatedAt).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(0) expect(query.state.dataUpdatedAt).toEqual(dataUpdatedAt) }) test('should restore item from the storage and refetch when `stale`', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now(), data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.isInvalidated = true query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(1) }) test('should restore item from the storage and refetch when `refetchOnRestore` is set to `always`', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, refetchOnRestore: 'always', }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now() + 1000, data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.isInvalidated = true query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(1) }) test('should restore item from the storage and NOT refetch when `refetchOnRestore` is set to false', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, refetchOnRestore: false, }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now(), data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.isInvalidated = true query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(0) }) test('should store item after successful fetch', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, queryHash, queryKey, storageKey, } = setupPersister(['foo'], { storage, }) await persister.persisterFn(queryFn, context, query) query.setData('baz') await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { data: 'baz', }, }) }) test('should skip stored item if not matched by filters', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, filters: { predicate: () => { return false }, }, }, ) const dataUpdatedAt = Date.now() await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt }, }), ) await persister.persisterFn(queryFn, context, query) query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(1) expect(query.fetch).toHaveBeenCalledTimes(0) }) test('should restore item from the storage with async deserializer', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, storageKey } = setupPersister( ['foo'], { storage, deserialize: (cachedString: string) => new Promise((resolve) => resolve(JSON.parse(cachedString))), }, ) await storage.setItem( storageKey, JSON.stringify({ buster: '', state: { dataUpdatedAt: Date.now(), data: '' }, }), ) await persister.persisterFn(queryFn, context, query) query.state.isInvalidated = true query.fetch = vi.fn() await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(0) expect(query.fetch).toHaveBeenCalledTimes(1) }) test('should store item after successful fetch with async serializer', async () => { const storage = getFreshStorage() const { context, persister, query, queryFn, queryHash, queryKey, storageKey, } = setupPersister(['foo'], { storage, serialize: (persistedQuery) => new Promise((resolve) => resolve(JSON.stringify(persistedQuery))), }) await persister.persisterFn(queryFn, context, query) query.setData('baz') await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledExactlyOnceWith(context) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { data: 'baz', }, }) }) describe('persistQuery', () => { test('Should properly persiste basic query', async () => { const storage = getFreshStorage() const { persister, query, queryHash, queryKey, storageKey } = setupPersister(['foo'], { storage, }) query.setData('baz') await persister.persistQuery(query) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { dataUpdateCount: 1, data: 'baz', status: 'success', }, }) }) test('Should skip persistance if storage is not provided', async () => { const serializeMock = vi.fn() const { persister, query } = setupPersister(['foo'], { storage: null, serialize: serializeMock, }) query.setData('baz') await persister.persistQuery(query) expect(serializeMock).toHaveBeenCalledTimes(0) }) }) describe('persistQueryByKey', () => { test('Should skip persistance if storage is not provided', async () => { const serializeMock = vi.fn() const { persister, client, queryKey } = setupPersister(['foo'], { storage: null, serialize: serializeMock, }) client.setQueryData(queryKey, 'baz') await persister.persistQueryByKey(queryKey, client) expect(serializeMock).toHaveBeenCalledTimes(0) }) test('should skip persistance if query was not found', async () => { const serializeMock = vi.fn() const storage = getFreshStorage() const { client, persister, queryKey } = setupPersister(['foo'], { storage, serialize: serializeMock, }) client.setQueryData(queryKey, 'baz') await persister.persistQueryByKey(['foo2'], client) expect(serializeMock).toHaveBeenCalledTimes(0) }) test('Should properly persiste basic query', async () => { const storage = getFreshStorage() const { persister, client, queryHash, queryKey, storageKey } = setupPersister(['foo'], { storage, }) client.setQueryData(queryKey, 'baz') await persister.persistQueryByKey(queryKey, client) expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({ buster: '', queryHash, queryKey, state: { dataUpdateCount: 1, data: 'baz', status: 'success', }, }) }) }) describe('persisterGc', () => { test('should properly clean storage from busted entries', async () => { const storage = getFreshStorage() const { persister, client, query, queryKey } = setupPersister(['foo'], { storage, }) query.setState({ dataUpdatedAt: 1, data: 'f', }) client.getQueryCache().add(query) await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.persisterGc() expect(await storage.entries()).toHaveLength(0) }) }) describe('restoreQueries', () => { test('should properly clean storage from busted entries', async () => { const storage = getFreshStorage() const { persister, client, query, queryKey } = setupPersister(['foo'], { storage, }) query.setState({ dataUpdatedAt: 1, data: 'f', }) client.getQueryCache().add(query) await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.restoreQueries(client) expect(await storage.entries()).toHaveLength(0) }) test('should properly restore queries from cache without filters', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client) expect(client.getQueryCache().getAll()).toHaveLength(1) expect(client.getQueryData(queryKey)).toEqual('foo') }) test('should properly restore queries from cache', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey }) expect(client.getQueryCache().getAll()).toHaveLength(1) expect(client.getQueryData(queryKey)).toEqual('foo') }) test('should not restore queries from cache if there is no match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: ['bar'] }) expect(client.getQueryCache().getAll()).toHaveLength(0) }) test('should properly restore queries from cache with partial match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: ['foo'] }) expect(client.getQueryCache().getAll()).toHaveLength(1) expect(client.getQueryData(queryKey)).toEqual('foo') }) test('should not restore queries from cache with exact match if there is no match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: ['foo'], exact: true }) expect(client.getQueryCache().getAll()).toHaveLength(0) }) test('should restore queries from cache with exact match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) client.clear() expect(client.getQueryCache().getAll()).toHaveLength(0) await persister.restoreQueries(client, { queryKey: queryKey, exact: true, }) expect(client.getQueryCache().getAll()).toHaveLength(1) }) }) describe('removeQueries', () => { test('should remove restore queries from storage without filters', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.removeQueries() expect(await storage.entries()).toHaveLength(0) }) test('should remove queries from storage', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.removeQueries({ queryKey }) expect(await storage.entries()).toHaveLength(0) }) test('should not remove queries from storage if there is no match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.removeQueries({ queryKey: ['bar'] }) expect(await storage.entries()).toHaveLength(1) }) test('should properly remove queries from storage with partial match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.removeQueries({ queryKey: ['foo'] }) expect(await storage.entries()).toHaveLength(0) }) test('should not remove queries from storage with exact match if there is no match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.removeQueries({ queryKey: ['foo'], exact: true }) expect(await storage.entries()).toHaveLength(1) }) test('should remove queries from storage with exact match', async () => { const storage = getFreshStorage() const { persister, client, queryKey } = setupPersister(['foo', 'bar'], { storage, }) client.setQueryData(queryKey, 'foo') await persister.persistQueryByKey(queryKey, client) expect(await storage.entries()).toHaveLength(1) await persister.removeQueries({ queryKey: queryKey, exact: true, }) expect(await storage.entries()).toHaveLength(0) }) }) }) ================================================ FILE: packages/query-persist-client-core/src/__tests__/persist.test.ts ================================================ import { describe, expect, test, vi } from 'vitest' import { QueriesObserver, QueryClient } from '@tanstack/query-core' import { persistQueryClientRestore, persistQueryClientSubscribe, } from '../persist' import { createMockPersister, createSpyPersister } from './utils' describe('persistQueryClientSubscribe', () => { test('should persist mutations', async () => { const queryClient = new QueryClient() const persister = createMockPersister() const unsubscribe = persistQueryClientSubscribe({ queryClient, persister, dehydrateOptions: { shouldDehydrateMutation: () => true }, }) queryClient.getMutationCache().build(queryClient, { mutationFn: (text: string) => Promise.resolve(text), }) const result = await persister.restoreClient() expect(result?.clientState.mutations).toHaveLength(1) unsubscribe() }) }) describe('persistQueryClientSave', () => { test('should not be triggered on observer type events', () => { const queryClient = new QueryClient() const persister = createSpyPersister() const unsubscribe = persistQueryClientSubscribe({ queryClient, persister, }) const queryKey = ['test'] const queryFn = vi.fn().mockReturnValue(1) const observer = new QueriesObserver(queryClient, [{ queryKey, queryFn }]) const unsubscribeObserver = observer.subscribe(vi.fn()) observer .getObservers()[0] ?.setOptions({ queryKey, refetchOnWindowFocus: false }) unsubscribeObserver() queryClient.setQueryData(queryKey, 2) // persistClient should be called 3 times: // 1. When query is added // 2. When queryFn is resolved // 3. When setQueryData is called // All events fired by manipulating observers are ignored expect(persister.persistClient).toHaveBeenCalledTimes(3) unsubscribe() }) }) describe('persistQueryClientRestore', () => { test('should rethrow exceptions in `restoreClient`', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const consoleWarn = vi .spyOn(console, 'warn') .mockImplementation(() => undefined) const queryClient = new QueryClient() const restoreError = new Error('Error restoring client') const persister = createSpyPersister() persister.restoreClient = () => Promise.reject(restoreError) await expect( persistQueryClientRestore({ queryClient, persister, }), ).rejects.toBe(restoreError) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleWarn).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) consoleMock.mockRestore() consoleWarn.mockRestore() }) test('should rethrow exceptions in `removeClient` before `restoreClient`', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const consoleWarn = vi .spyOn(console, 'warn') .mockImplementation(() => undefined) const queryClient = new QueryClient() const restoreError = new Error('Error restoring client') const removeError = new Error('Error removing client') const persister = createSpyPersister() persister.restoreClient = () => Promise.reject(restoreError) persister.removeClient = () => Promise.reject(removeError) await expect( persistQueryClientRestore({ queryClient, persister, }), ).rejects.toBe(removeError) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleWarn).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError) consoleMock.mockRestore() consoleWarn.mockRestore() }) test('should rethrow error in `removeClient`', async () => { const queryClient = new QueryClient() const persister = createSpyPersister() const removeError = new Error('Error removing client') persister.removeClient = () => Promise.reject(removeError) persister.restoreClient = () => { return Promise.resolve({ buster: 'random-buster', clientState: { mutations: [], queries: [], }, timestamp: new Date().getTime(), }) } await expect( persistQueryClientRestore({ queryClient, persister, }), ).rejects.toBe(removeError) }) }) ================================================ FILE: packages/query-persist-client-core/src/__tests__/utils.ts ================================================ import { vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import type { PersistedClient, Persister } from '../persist' export function createMockPersister(): Persister { let storedState: PersistedClient | undefined return { persistClient(persistClient: PersistedClient) { storedState = persistClient }, async restoreClient() { await sleep(10) return storedState }, removeClient() { storedState = undefined }, } } export function createSpyPersister(): Persister { return { persistClient: vi.fn(), restoreClient: vi.fn(), removeClient: vi.fn(), } } ================================================ FILE: packages/query-persist-client-core/src/createPersister.ts ================================================ import { hashKey, matchQuery, notifyManager, partialMatchKey, } from '@tanstack/query-core' import type { Query, QueryClient, QueryFilters, QueryFunctionContext, QueryKey, QueryState, } from '@tanstack/query-core' export interface PersistedQuery { buster: string queryHash: string queryKey: QueryKey state: QueryState } export type MaybePromise = T | Promise export interface AsyncStorage { getItem: (key: string) => MaybePromise setItem: (key: string, value: TStorageValue) => MaybePromise removeItem: (key: string) => MaybePromise entries?: () => MaybePromise> } export interface StoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. */ storage: AsyncStorage | undefined | null /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (persistedQuery: PersistedQuery) => MaybePromise /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: TStorageValue) => MaybePromise /** * A unique string that can be used to forcefully invalidate existing caches, * if they do not share the same buster string */ buster?: string /** * The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be discarded * @default 24 hours */ maxAge?: number /** * Prefix to be used for storage key. * Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`. * @default 'tanstack-query' */ prefix?: string /** * If set to `true`, the query will refetch on successful query restoration if the data is stale. * If set to `false`, the query will not refetch on successful query restoration. * If set to `'always'`, the query will always refetch on successful query restoration. * Defaults to `true`. */ refetchOnRestore?: boolean | 'always' /** * Filters to narrow down which Queries should be persisted. */ filters?: QueryFilters } export const PERSISTER_KEY_PREFIX = 'tanstack-query' /** * Warning: experimental feature. * This utility function enables fine-grained query persistence. * Simple add it as a `persister` parameter to `useQuery` or `defaultOptions` on `queryClient`. * * ``` * useQuery({ queryKey: ['myKey'], queryFn: fetcher, persister: createPersister({ storage: localStorage, }), }) ``` */ export function experimental_createQueryPersister({ storage, buster = '', maxAge = 1000 * 60 * 60 * 24, serialize = JSON.stringify as Required< StoragePersisterOptions >['serialize'], deserialize = JSON.parse as Required< StoragePersisterOptions >['deserialize'], prefix = PERSISTER_KEY_PREFIX, refetchOnRestore = true, filters, }: StoragePersisterOptions) { function isExpiredOrBusted(persistedQuery: PersistedQuery) { if (persistedQuery.state.dataUpdatedAt) { const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt const expired = queryAge > maxAge const busted = persistedQuery.buster !== buster if (expired || busted) { return true } return false } return true } async function retrieveQuery( queryHash: string, afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void, ) { if (storage != null) { const storageKey = `${prefix}-${queryHash}` try { const storedData = await storage.getItem(storageKey) if (storedData) { let persistedQuery: PersistedQuery try { persistedQuery = await deserialize(storedData) } catch { await storage.removeItem(storageKey) return } if (isExpiredOrBusted(persistedQuery)) { await storage.removeItem(storageKey) } else { if (afterRestoreMacroTask) { // Just after restoring we want to get fresh data from the server if it's stale notifyManager.schedule(() => afterRestoreMacroTask(persistedQuery), ) } // We must resolve the promise here, as otherwise we will have `loading` state in the app until `queryFn` resolves return persistedQuery.state.data as T } } } catch (err) { if (process.env.NODE_ENV === 'development') { console.error(err) console.warn( 'Encountered an error attempting to restore query cache from persisted location.', ) } await storage.removeItem(storageKey) } } return } async function persistQueryByKey( queryKey: QueryKey, queryClient: QueryClient, ) { if (storage != null) { const query = queryClient.getQueryCache().find({ queryKey }) if (query) { await persistQuery(query) } else { if (process.env.NODE_ENV === 'development') { console.warn( 'Could not find query to be persisted. QueryKey:', JSON.stringify(queryKey), ) } } } } async function persistQuery(query: Query) { if (storage != null) { const storageKey = `${prefix}-${query.queryHash}` storage.setItem( storageKey, await serialize({ state: query.state, queryKey: query.queryKey, queryHash: query.queryHash, buster: buster, }), ) } } async function persisterFn( queryFn: (context: QueryFunctionContext) => T | Promise, ctx: QueryFunctionContext, query: Query, ) { const matchesFilter = filters ? matchQuery(filters, query) : true // Try to restore only if we do not have any data in the cache and we have persister defined if (matchesFilter && query.state.data === undefined && storage != null) { const restoredData = await retrieveQuery( query.queryHash, (persistedQuery: PersistedQuery) => { // Set proper updatedAt, since resolving in the first pass overrides those values query.setState({ dataUpdatedAt: persistedQuery.state.dataUpdatedAt, errorUpdatedAt: persistedQuery.state.errorUpdatedAt, }) if ( refetchOnRestore === 'always' || (refetchOnRestore === true && query.isStale()) ) { query.fetch() } }, ) if (restoredData !== undefined) { return Promise.resolve(restoredData as T) } } // If we did not restore, or restoration failed - fetch const queryFnResult = await queryFn(ctx) if (matchesFilter && storage != null) { // Persist if we have storage defined, we use timeout to get proper state to be persisted notifyManager.schedule(() => { persistQuery(query) }) } return Promise.resolve(queryFnResult) } async function persisterGc() { if (storage?.entries) { const storageKeyPrefix = `${prefix}-` const entries = await storage.entries() for (const [key, value] of entries) { if (key.startsWith(storageKeyPrefix)) { let persistedQuery: PersistedQuery try { persistedQuery = await deserialize(value) } catch { await storage.removeItem(key) continue } if (isExpiredOrBusted(persistedQuery)) { await storage.removeItem(key) } } } } else if (process.env.NODE_ENV === 'development') { throw new Error( 'Provided storage does not implement `entries` method. Garbage collection is not possible without ability to iterate over storage items.', ) } } async function restoreQueries( queryClient: QueryClient, filters: Pick = {}, ): Promise { const { exact, queryKey } = filters if (storage?.entries) { const storageKeyPrefix = `${prefix}-` const entries = await storage.entries() for (const [key, value] of entries) { if (key.startsWith(storageKeyPrefix)) { let persistedQuery: PersistedQuery try { persistedQuery = await deserialize(value) } catch { await storage.removeItem(key) continue } if (isExpiredOrBusted(persistedQuery)) { await storage.removeItem(key) continue } if (queryKey) { if (exact) { if (persistedQuery.queryHash !== hashKey(queryKey)) { continue } } else if (!partialMatchKey(persistedQuery.queryKey, queryKey)) { continue } } queryClient.setQueryData( persistedQuery.queryKey, persistedQuery.state.data, { updatedAt: persistedQuery.state.dataUpdatedAt, }, ) } } } else if (process.env.NODE_ENV === 'development') { throw new Error( 'Provided storage does not implement `entries` method. Restoration of all stored entries is not possible without ability to iterate over storage items.', ) } } async function removeQueries( filters: Pick = {}, ): Promise { const { exact, queryKey } = filters if (storage?.entries) { const entries = await storage.entries() const storageKeyPrefix = `${prefix}-` for (const [key, value] of entries) { if (key.startsWith(storageKeyPrefix)) { if (!queryKey) { await storage.removeItem(key) continue } let persistedQuery: PersistedQuery try { persistedQuery = await deserialize(value) } catch { await storage.removeItem(key) continue } if (exact) { if (persistedQuery.queryHash !== hashKey(queryKey)) { continue } } else if (!partialMatchKey(persistedQuery.queryKey, queryKey)) { continue } await storage.removeItem(key) } } } else if (process.env.NODE_ENV === 'development') { throw new Error( 'Provided storage does not implement `entries` method. Removal of stored entries is not possible without ability to iterate over storage items.', ) } } return { persisterFn, persistQuery, persistQueryByKey, retrieveQuery, persisterGc, restoreQueries, removeQueries, } } ================================================ FILE: packages/query-persist-client-core/src/index.ts ================================================ /* istanbul ignore file */ export * from './persist' export * from './retryStrategies' export * from './createPersister' ================================================ FILE: packages/query-persist-client-core/src/persist.ts ================================================ import { dehydrate, hydrate } from '@tanstack/query-core' import type { DehydrateOptions, DehydratedState, HydrateOptions, NotifyEventType, QueryClient, } from '@tanstack/query-core' export type Promisable = T | PromiseLike export interface Persister { persistClient: (persistClient: PersistedClient) => Promisable restoreClient: () => Promisable removeClient: () => Promisable } export interface PersistedClient { timestamp: number buster: string clientState: DehydratedState } export interface PersistQueryClientRootOptions { /** The QueryClient to persist */ queryClient: QueryClient /** The Persister interface for storing and restoring the cache * to/from a persisted location */ persister: Persister /** A unique string that can be used to forcefully * invalidate existing caches if they do not share the same buster string */ buster?: string } export interface PersistedQueryClientRestoreOptions extends PersistQueryClientRootOptions { /** The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this * time, it will be discarded */ maxAge?: number /** The options passed to the hydrate function */ hydrateOptions?: HydrateOptions } export interface PersistedQueryClientSaveOptions extends PersistQueryClientRootOptions { /** The options passed to the dehydrate function */ dehydrateOptions?: DehydrateOptions } export interface PersistQueryClientOptions extends PersistedQueryClientRestoreOptions, PersistedQueryClientSaveOptions, PersistQueryClientRootOptions {} /** * Checks if emitted event is about cache change and not about observers. * Useful for persist, where we only want to trigger save when cache is changed. */ const cacheEventTypes: Array = ['added', 'removed', 'updated'] function isCacheEventType(eventType: NotifyEventType) { return cacheEventTypes.includes(eventType) } /** * Restores persisted data to the QueryCache * - data obtained from persister.restoreClient * - data is hydrated using hydrateOptions * If data is expired, busted, empty, or throws, it runs persister.removeClient */ export async function persistQueryClientRestore({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, buster = '', hydrateOptions, }: PersistedQueryClientRestoreOptions) { try { const persistedClient = await persister.restoreClient() if (persistedClient) { if (persistedClient.timestamp) { const expired = Date.now() - persistedClient.timestamp > maxAge const busted = persistedClient.buster !== buster if (expired || busted) { return persister.removeClient() } else { hydrate(queryClient, persistedClient.clientState, hydrateOptions) } } else { return persister.removeClient() } } } catch (err) { if (process.env.NODE_ENV !== 'production') { console.error(err) console.warn( 'Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded.', ) } await persister.removeClient() throw err } } /** * Persists data from the QueryCache * - data dehydrated using dehydrateOptions * - data is persisted using persister.persistClient */ export async function persistQueryClientSave({ queryClient, persister, buster = '', dehydrateOptions, }: PersistedQueryClientSaveOptions) { const persistClient: PersistedClient = { buster, timestamp: Date.now(), clientState: dehydrate(queryClient, dehydrateOptions), } await persister.persistClient(persistClient) } /** * Subscribe to QueryCache and MutationCache updates (for persisting) * @returns an unsubscribe function (to discontinue monitoring) */ export function persistQueryClientSubscribe( props: PersistedQueryClientSaveOptions, ) { const unsubscribeQueryCache = props.queryClient .getQueryCache() .subscribe((event) => { if (isCacheEventType(event.type)) { persistQueryClientSave(props) } }) const unsubscribeMutationCache = props.queryClient .getMutationCache() .subscribe((event) => { if (isCacheEventType(event.type)) { persistQueryClientSave(props) } }) return () => { unsubscribeQueryCache() unsubscribeMutationCache() } } /** * Restores persisted data to QueryCache and persists further changes. */ export function persistQueryClient( props: PersistQueryClientOptions, ): [() => void, Promise] { let hasUnsubscribed = false let persistQueryClientUnsubscribe: (() => void) | undefined const unsubscribe = () => { hasUnsubscribed = true persistQueryClientUnsubscribe?.() } // Attempt restore const restorePromise = persistQueryClientRestore(props).then(() => { if (!hasUnsubscribed) { // Subscribe to changes in the query cache to trigger the save persistQueryClientUnsubscribe = persistQueryClientSubscribe(props) } }) return [unsubscribe, restorePromise] } ================================================ FILE: packages/query-persist-client-core/src/retryStrategies.ts ================================================ import type { PersistedClient } from './persist' export type PersistRetryer = (props: { persistedClient: PersistedClient error: Error errorCount: number }) => PersistedClient | undefined export const removeOldestQuery: PersistRetryer = ({ persistedClient }) => { const mutations = [...persistedClient.clientState.mutations] const queries = [...persistedClient.clientState.queries] const client: PersistedClient = { ...persistedClient, clientState: { mutations, queries }, } // sort queries by dataUpdatedAt (oldest first) const sortedQueries = [...queries].sort( (a, b) => a.state.dataUpdatedAt - b.state.dataUpdatedAt, ) // clean oldest query if (sortedQueries.length > 0) { const oldestData = sortedQueries.shift() client.clientState.queries = queries.filter((q) => q !== oldestData) return client } return undefined } ================================================ FILE: packages/query-persist-client-core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/query-persist-client-core/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-persist-client-core/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-persist-client-core/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default defineConfig({ // 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], exclude: ['src/__tests__/**'], }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/query-sync-storage-persister/CHANGELOG.md ================================================ # @tanstack/query-sync-storage-persister ## 5.90.27 ### 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-persist-client-core@5.92.4 - @tanstack/query-core@5.91.2 ## 5.90.26 ### Patch Changes - Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]: - @tanstack/query-core@5.91.1 - @tanstack/query-persist-client-core@5.92.3 ## 5.90.25 ### Patch Changes - Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]: - @tanstack/query-core@5.91.0 - @tanstack/query-persist-client-core@5.92.2 ## 5.90.24 ### Patch Changes - Updated dependencies [[`e505568`](https://github.com/TanStack/query/commit/e505568f4d51c8281d38e9687091094b7d32a405)]: - @tanstack/query-persist-client-core@5.92.1 ## 5.90.23 ### Patch Changes - Updated dependencies [[`978fc52`](https://github.com/TanStack/query/commit/978fc52728a8b9eb33f0a82f4ddf42a95815bd7f)]: - @tanstack/query-persist-client-core@5.92.0 ## 5.90.22 ### Patch Changes - Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]: - @tanstack/query-core@5.90.20 - @tanstack/query-persist-client-core@5.91.19 ## 5.90.21 ### Patch Changes - Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]: - @tanstack/query-core@5.90.19 - @tanstack/query-persist-client-core@5.91.18 ## 5.90.20 ### Patch Changes - Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]: - @tanstack/query-core@5.90.18 - @tanstack/query-persist-client-core@5.91.17 ## 5.90.19 ### Patch Changes - Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]: - @tanstack/query-core@5.90.17 - @tanstack/query-persist-client-core@5.91.16 ## 5.90.18 ### Patch Changes - Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]: - @tanstack/query-core@5.90.16 - @tanstack/query-persist-client-core@5.91.15 ## 5.90.17 ### Patch Changes - Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]: - @tanstack/query-core@5.90.15 - @tanstack/query-persist-client-core@5.91.14 ## 5.90.16 ### Patch Changes - Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]: - @tanstack/query-core@5.90.14 - @tanstack/query-persist-client-core@5.91.13 ## 5.90.15 ### Patch Changes - Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]: - @tanstack/query-core@5.90.13 - @tanstack/query-persist-client-core@5.91.12 ## 5.90.14 ### Patch Changes - Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]: - @tanstack/query-core@5.90.12 - @tanstack/query-persist-client-core@5.91.11 ## 5.90.13 ### Patch Changes - Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]: - @tanstack/query-core@5.90.11 - @tanstack/query-persist-client-core@5.91.10 ## 5.90.12 ### 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 - @tanstack/query-persist-client-core@5.91.9 ## 5.90.11 ### Patch Changes - Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]: - @tanstack/query-core@5.90.9 - @tanstack/query-persist-client-core@5.91.8 ## 5.90.10 ### Patch Changes - Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]: - @tanstack/query-core@5.90.8 - @tanstack/query-persist-client-core@5.91.7 ## 5.90.9 ### Patch Changes - Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]: - @tanstack/query-core@5.90.7 - @tanstack/query-persist-client-core@5.91.6 ## 5.90.8 ### Patch Changes - Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]: - @tanstack/query-core@5.90.6 - @tanstack/query-persist-client-core@5.91.5 ## 5.90.7 ### Patch Changes - Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]: - @tanstack/query-core@5.90.5 - @tanstack/query-persist-client-core@5.91.4 ## 5.90.6 ### Patch Changes - Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]: - @tanstack/query-core@5.90.4 - @tanstack/query-persist-client-core@5.91.3 ## 5.90.5 ### Patch Changes - Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]: - @tanstack/query-core@5.90.3 - @tanstack/query-persist-client-core@5.91.2 ## 5.90.4 ### Patch Changes - Updated dependencies [[`846d53d`](https://github.com/TanStack/query/commit/846d53d98992d50606c40634efa43dea9965b787)]: - @tanstack/query-persist-client-core@5.91.1 ## 5.90.3 ### Patch Changes - Updated dependencies [[`5cd86c6`](https://github.com/TanStack/query/commit/5cd86c6ef1720b87b13e1ab70ee823616f1f029a)]: - @tanstack/query-persist-client-core@5.91.0 ================================================ FILE: packages/query-sync-storage-persister/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-sync-storage-persister/package.json ================================================ { "name": "@tanstack/query-sync-storage-persister", "version": "5.90.27", "description": "A persister for synchronous storages, to be used with TanStack/Query", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/query-sync-storage-persister" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build", "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*", "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-sync-storage-persister/src/__tests__/storageIsFull.test.ts ================================================ import { describe, expect, test } from 'vitest' import { MutationCache, QueryCache, QueryClient, dehydrate, } from '@tanstack/query-core' import { removeOldestQuery } from '@tanstack/query-persist-client-core' import { sleep } from '@tanstack/query-test-utils' import { createSyncStoragePersister } from '../index' function getMockStorage(limitSize?: number) { const dataSet = new Map() return { getItem(key: string): string | null { const value = dataSet.get(key) return value === undefined ? null : value }, setItem(key: string, value: string) { if (limitSize !== undefined) { const currentSize = Array.from(dataSet.entries()).reduce( (n, d) => d[0].length + d[1].length + n, 0, ) if ( currentSize - (dataSet.get(key)?.length || 0) + value.length > limitSize ) { throw Error( `Failed to execute 'setItem' on 'Storage': Setting the value of '${key}' exceeded the quota.`, ) } } return dataSet.set(key, value) }, removeItem(key: string) { return dataSet.delete(key) }, } as any as Storage } describe('create persister', () => { test('basic store and recover', async () => { const queryCache = new QueryCache() const mutationCache = new MutationCache() const queryClient = new QueryClient({ queryCache, mutationCache }) const storage = getMockStorage() const persister = createSyncStoragePersister({ throttleTime: 0, storage, }) await queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => Promise.resolve('string'), }) await queryClient.prefetchQuery({ queryKey: ['number'], queryFn: () => Promise.resolve(1), }) await queryClient.prefetchQuery({ queryKey: ['boolean'], queryFn: () => Promise.resolve(true), }) await queryClient.prefetchQuery({ queryKey: ['null'], queryFn: () => Promise.resolve(null), }) await queryClient.prefetchQuery({ queryKey: ['array'], queryFn: () => Promise.resolve(['string', 0]), }) const persistClient = { buster: 'test-buster', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(persistClient) await sleep(1) const restoredClient = await persister.restoreClient() expect(restoredClient).toEqual(persistClient) }) test('should clean the old queries when storage full', async () => { const queryCache = new QueryCache() const mutationCache = new MutationCache() const queryClient = new QueryClient({ queryCache, mutationCache }) const N = 2000 const storage = getMockStorage(N * 5) // can save 4 items; const persister = createSyncStoragePersister({ throttleTime: 0, storage, retry: removeOldestQuery, }) await queryClient.prefetchQuery({ queryKey: ['A'], queryFn: () => Promise.resolve('A'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['B'], queryFn: () => Promise.resolve('B'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['C'], queryFn: () => Promise.resolve('C'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['D'], queryFn: () => Promise.resolve('D'.repeat(N)), }) await sleep(1) await queryClient.prefetchQuery({ queryKey: ['E'], queryFn: () => Promise.resolve('E'.repeat(N)), }) const persistClient = { buster: 'test-limit', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(persistClient) await sleep(10) const restoredClient = await persister.restoreClient() expect(restoredClient?.clientState.queries.length).toEqual(4) expect( restoredClient?.clientState.queries.find((q) => q.queryKey[0] === 'A'), ).toBeUndefined() expect( restoredClient?.clientState.queries.find((q) => q.queryKey[0] === 'B'), ).not.toBeUndefined() // update query Data await queryClient.prefetchQuery({ queryKey: ['A'], queryFn: () => Promise.resolve('a'.repeat(N)), }) const updatedPersistClient = { buster: 'test-limit', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(updatedPersistClient) await sleep(10) const restoredClient2 = await persister.restoreClient() expect(restoredClient2?.clientState.queries.length).toEqual(4) expect( restoredClient2?.clientState.queries.find((q) => q.queryKey[0] === 'A'), ).toHaveProperty('state.data', 'a'.repeat(N)) expect( restoredClient2?.clientState.queries.find((q) => q.queryKey[0] === 'B'), ).toBeUndefined() }) test('should clear storage as default error handling', async () => { const queryCache = new QueryCache() const mutationCache = new MutationCache() const queryClient = new QueryClient({ queryCache, mutationCache }) const N = 2000 const storage = getMockStorage(0) const persister = createSyncStoragePersister({ throttleTime: 0, storage, retry: removeOldestQuery, }) await queryClient.prefetchQuery({ queryKey: ['A'], queryFn: () => Promise.resolve('A'.repeat(N)), }) await sleep(1) const persistClient = { buster: 'test-limit', timestamp: Date.now(), clientState: dehydrate(queryClient), } persister.persistClient(persistClient) await sleep(10) const restoredClient = await persister.restoreClient() expect(restoredClient).toEqual(undefined) }) }) ================================================ FILE: packages/query-sync-storage-persister/src/index.ts ================================================ import { timeoutManager } from '@tanstack/query-core' import { noop } from './utils' import type { ManagedTimerId } from '@tanstack/query-core' import type { PersistRetryer, PersistedClient, Persister, } from '@tanstack/query-persist-client-core' interface Storage { getItem: (key: string) => string | null setItem: (key: string, value: string) => void removeItem: (key: string) => void } interface CreateSyncStoragePersisterOptions { /** The storage client used for setting and retrieving items from cache. * For SSR pass in `undefined`. Note that window.localStorage can be * `null` in Android WebViews depending on how they are configured. */ storage: Storage | undefined | null /** The key to use when storing the cache */ key?: string /** To avoid spamming, * pass a time in ms to throttle saving the cache to disk */ throttleTime?: number /** * How to serialize the data to storage. * @default `JSON.stringify` */ serialize?: (client: PersistedClient) => string /** * How to deserialize the data from storage. * @default `JSON.parse` */ deserialize?: (cachedString: string) => PersistedClient retry?: PersistRetryer } /** * @deprecated use `createAsyncStoragePersister` from `@tanstack/query-async-storage-persister` instead. */ export function createSyncStoragePersister({ storage, key = `REACT_QUERY_OFFLINE_CACHE`, throttleTime = 1000, serialize = JSON.stringify, deserialize = JSON.parse, retry, }: CreateSyncStoragePersisterOptions): Persister { if (storage) { const trySave = (persistedClient: PersistedClient): Error | undefined => { try { storage.setItem(key, serialize(persistedClient)) return } catch (error) { return error as Error } } return { persistClient: throttle((persistedClient) => { let client: PersistedClient | undefined = persistedClient let error = trySave(client) let errorCount = 0 while (error && client) { errorCount++ client = retry?.({ persistedClient: client, error, errorCount, }) if (client) { error = trySave(client) } } }, throttleTime), restoreClient: () => { const cacheString = storage.getItem(key) if (!cacheString) { return } return deserialize(cacheString) }, removeClient: () => { storage.removeItem(key) }, } } return { persistClient: noop, restoreClient: noop, removeClient: noop, } } function throttle>( func: (...args: TArgs) => any, wait = 100, ) { let timer: ManagedTimerId | null = null let params: TArgs return function (...args: TArgs) { params = args if (timer === null) { timer = timeoutManager.setTimeout(() => { func(...params) timer = null }, wait) } } } ================================================ FILE: packages/query-sync-storage-persister/src/utils.ts ================================================ export function noop(): void export function noop(): undefined export function noop() {} ================================================ FILE: packages/query-sync-storage-persister/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [ { "path": "../query-core" }, { "path": "../query-persist-client-core" } ] } ================================================ FILE: packages/query-sync-storage-persister/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/query-sync-storage-persister/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts'] }), legacyConfig({ entry: ['src/*.ts'] }), ]) ================================================ FILE: packages/query-sync-storage-persister/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default defineConfig({ // 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'], exclude: ['src/__tests__/**'], }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/query-test-utils/eslint.config.js ================================================ // @ts-check import rootConfig from './root.eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/query-test-utils/package.json ================================================ { "name": "@tanstack/query-test-utils", "version": "0.0.0", "description": "Internal test utilities for TanStack Query", "author": "Jonghyeon Ko ", "private": true, "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/query-test-utils" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "main": "src/index.ts", "types": "src/index.ts", "module": "src/index.ts", "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" }, "./package.json": "./package.json" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch" }, "type": "module", "devDependencies": { "npm-run-all2": "^5.0.0" } } ================================================ FILE: packages/query-test-utils/src/__test__/queryKey.test.ts ================================================ import { describe, expect, it } from 'vitest' import { queryKey } from '../queryKey' describe('queryKey', () => { it('should return a query key', () => { const key = queryKey() expect(key).toEqual(['query_1']) }) it('should return a new query key each time', () => { expect(queryKey()).not.toEqual(queryKey()) }) }) ================================================ FILE: packages/query-test-utils/src/__test__/sleep.test.ts ================================================ import { describe, expect, it } from 'vitest' import { sleep } from '../sleep' describe('sleep', () => { it('should sleep for the given amount of time', async () => { const start = Date.now() await sleep(100) const end = Date.now() expect(end - start).toBeGreaterThanOrEqual(100) }) }) ================================================ FILE: packages/query-test-utils/src/index.ts ================================================ export { sleep } from './sleep' export { queryKey } from './queryKey' export { mockVisibilityState } from './mockVisibilityState' ================================================ FILE: packages/query-test-utils/src/mockVisibilityState.ts ================================================ import { vi } from 'vitest' import type { MockInstance } from 'vitest' export const mockVisibilityState = ( value: DocumentVisibilityState, ): MockInstance<() => DocumentVisibilityState> => vi.spyOn(document, 'visibilityState', 'get').mockReturnValue(value) ================================================ FILE: packages/query-test-utils/src/queryKey.ts ================================================ let queryKeyCount = 0 export const queryKey = (): Array => { queryKeyCount++ return [`query_${queryKeyCount}`] } ================================================ FILE: packages/query-test-utils/src/sleep.ts ================================================ export const sleep = (ms: number): Promise => new Promise((resolve) => { setTimeout(resolve, ms) }) ================================================ FILE: packages/query-test-utils/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"] } ================================================ FILE: packages/query-test-utils/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import packageJson from './package.json' export default defineConfig({ // 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, coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/react-query/CHANGELOG.md ================================================ # @tanstack/react-query ## 5.91.2 ### 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.1 ### 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.0 ### Minor Changes - feat: environmentManager ([#10199](https://github.com/TanStack/query/pull/10199)) ### Patch Changes - Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]: - @tanstack/query-core@5.91.0 ## 5.90.21 ### Patch Changes - refactor(react-query/useQueries): remove unreachable 'willFetch' branch in suspense promise collection ([#10082](https://github.com/TanStack/query/pull/10082)) ## 5.90.20 ### Patch Changes - Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]: - @tanstack/query-core@5.90.20 ## 5.90.19 ### Patch Changes - Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]: - @tanstack/query-core@5.90.19 ## 5.90.18 ### Patch Changes - Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]: - @tanstack/query-core@5.90.18 ## 5.90.17 ### Patch Changes - Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]: - @tanstack/query-core@5.90.17 ## 5.90.16 ### Patch Changes - fix(react-query): allow retryOnMount when throwOnError is function ([#9338](https://github.com/TanStack/query/pull/9338)) - Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]: - @tanstack/query-core@5.90.16 ## 5.90.15 ### Patch Changes - Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]: - @tanstack/query-core@5.90.15 ## 5.90.14 ### Patch Changes - Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]: - @tanstack/query-core@5.90.14 ## 5.90.13 ### Patch Changes - Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]: - @tanstack/query-core@5.90.13 ## 5.90.12 ### Patch Changes - Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]: - @tanstack/query-core@5.90.12 ## 5.90.11 ### Patch Changes - Prevent infinite render loops when useSuspenseQueries has duplicate queryKeys ([#9886](https://github.com/TanStack/query/pull/9886)) - Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]: - @tanstack/query-core@5.90.11 ## 5.90.10 ### 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.9 ### Patch Changes - Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]: - @tanstack/query-core@5.90.9 ## 5.90.8 ### Patch Changes - Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]: - @tanstack/query-core@5.90.8 ## 5.90.7 ### Patch Changes - Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]: - @tanstack/query-core@5.90.7 ## 5.90.6 ### Patch Changes - Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]: - @tanstack/query-core@5.90.6 ## 5.90.5 ### Patch Changes - Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]: - @tanstack/query-core@5.90.5 ## 5.90.4 ### Patch Changes - Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]: - @tanstack/query-core@5.90.4 ## 5.90.3 ### Patch Changes - Avoid unhandled promise rejection errors during de/rehydration of pending queries. ([#9752](https://github.com/TanStack/query/pull/9752)) - Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]: - @tanstack/query-core@5.90.3 ================================================ FILE: packages/react-query/README.md ================================================ ![TanStack Query Header](https://github.com/TanStack/query/raw/main/media/repo-header.png) Hooks for fetching, caching and updating asynchronous data in React #TanStack semantic-release Join the discussion on Github Best of JS Gitpod Ready-to-Code 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), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger) ## 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 - [React Suspense](https://react.dev/reference/react/Suspense) + Fetch-As-You-Render Query Prefetching - Dedicated Devtools ### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) ================================================ FILE: packages/react-query/eslint.config.js ================================================ // @ts-check import pluginReact from '@eslint-react/eslint-plugin' import reactHooks from 'eslint-plugin-react-hooks' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, // @ts-expect-error wtf ...reactHooks.configs['recommended-latest'], { files: ['**/*.{ts,tsx}'], ...pluginReact.configs.recommended, rules: { '@eslint-react/no-context-provider': 'off', // We need to be React 18 compatible 'react-hooks/exhaustive-deps': 'error', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/unsupported-syntax': 'error', 'react-hooks/incompatible-library': 'error', }, }, { files: ['**/__tests__/**'], rules: { '@eslint-react/dom/no-missing-button-type': 'off', '@typescript-eslint/no-unnecessary-condition': 'off', }, }, ] ================================================ FILE: packages/react-query/package.json ================================================ { "name": "@tanstack/react-query", "version": "5.91.2", "description": "Hooks for managing, caching and syncing asynchronous and remote data in React", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/react-query" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "pnpm build:tsup && pnpm build:codemods", "build:tsup": "tsup --tsconfig tsconfig.prod.json", "build:codemods": "cpy ../query-codemods/* ./build/codemods" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "react-native": "src/index.ts", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__", "!build/codemods/node_modules", "!build/codemods/vite.config.ts", "!build/codemods/**/__testfixtures__", "!build/codemods/**/__tests__" ], "dependencies": { "@tanstack/query-core": "workspace:*" }, "devDependencies": { "@tanstack/query-persist-client-core": "workspace:*", "@tanstack/query-test-utils": "workspace:*", "@testing-library/react": "^16.1.0", "@testing-library/react-render-stream": "^2.0.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.3.4", "cpy-cli": "^5.0.0", "npm-run-all2": "^5.0.0", "react": "^19.2.1", "react-dom": "^19.2.1", "react-error-boundary": "^4.1.2" }, "peerDependencies": { "react": "^18 || ^19" } } ================================================ FILE: packages/react-query/src/HydrationBoundary.tsx ================================================ 'use client' import * as React from 'react' import { hydrate } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { DehydratedState, HydrateOptions, OmitKeyof, QueryClient, } from '@tanstack/query-core' export interface HydrationBoundaryProps { state: DehydratedState | null | undefined options?: OmitKeyof & { defaultOptions?: OmitKeyof< Exclude, 'mutations' > } children?: React.ReactNode queryClient?: QueryClient } export const HydrationBoundary = ({ children, options = {}, state, queryClient, }: HydrationBoundaryProps) => { const client = useQueryClient(queryClient) const optionsRef = React.useRef(options) React.useEffect(() => { optionsRef.current = options }) // This useMemo is for performance reasons only, everything inside it must // be safe to run in every render and code here should be read as "in render". // // This code needs to happen during the render phase, because after initial // SSR, hydration needs to happen _before_ children render. Also, if hydrating // during a transition, we want to hydrate as much as is safe in render so // we can prerender as much as possible. // // For any queries that already exist in the cache, we want to hold back on // hydrating until _after_ the render phase. The reason for this is that during // transitions, we don't want the existing queries and observers to update to // the new data on the current page, only _after_ the transition is committed. // If the transition is aborted, we will have hydrated any _new_ queries, but // we throw away the fresh data for any existing ones to avoid unexpectedly // updating the UI. const hydrationQueue: DehydratedState['queries'] | undefined = React.useMemo(() => { if (state) { if (typeof state !== 'object') { return } const queryCache = client.getQueryCache() // State is supplied from the outside and we might as well fail // gracefully if it has the wrong shape, so while we type `queries` // as required, we still provide a fallback. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const queries = state.queries || [] const newQueries: DehydratedState['queries'] = [] const existingQueries: DehydratedState['queries'] = [] for (const dehydratedQuery of queries) { const existingQuery = queryCache.get(dehydratedQuery.queryHash) if (!existingQuery) { newQueries.push(dehydratedQuery) } else { const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt || (dehydratedQuery.promise && existingQuery.state.status !== 'pending' && existingQuery.state.fetchStatus !== 'fetching' && dehydratedQuery.dehydratedAt !== undefined && dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt) if (hydrationIsNewer) { existingQueries.push(dehydratedQuery) } } } if (newQueries.length > 0) { // It's actually fine to call this with queries/state that already exists // in the cache, or is older. hydrate() is idempotent for queries. // eslint-disable-next-line react-hooks/refs hydrate(client, { queries: newQueries }, optionsRef.current) } if (existingQueries.length > 0) { return existingQueries } } return undefined }, [client, state]) React.useEffect(() => { if (hydrationQueue) { hydrate(client, { queries: hydrationQueue }, optionsRef.current) } }, [client, hydrationQueue]) return children as React.ReactElement } ================================================ FILE: packages/react-query/src/IsRestoringProvider.ts ================================================ 'use client' import * as React from 'react' const IsRestoringContext = React.createContext(false) export const useIsRestoring = () => React.useContext(IsRestoringContext) export const IsRestoringProvider = IsRestoringContext.Provider ================================================ FILE: packages/react-query/src/QueryClientProvider.tsx ================================================ 'use client' import * as React from 'react' import type { QueryClient } from '@tanstack/query-core' export const QueryClientContext = React.createContext( undefined, ) export const useQueryClient = (queryClient?: QueryClient) => { const client = React.useContext(QueryClientContext) if (queryClient) { return queryClient } if (!client) { throw new Error('No QueryClient set, use QueryClientProvider to set one') } return client } export type QueryClientProviderProps = { client: QueryClient children?: React.ReactNode } export const QueryClientProvider = ({ client, children, }: QueryClientProviderProps): React.JSX.Element => { React.useEffect(() => { client.mount() return () => { client.unmount() } }, [client]) return ( {children} ) } ================================================ FILE: packages/react-query/src/QueryErrorResetBoundary.tsx ================================================ 'use client' import * as React from 'react' // CONTEXT export type QueryErrorResetFunction = () => void export type QueryErrorIsResetFunction = () => boolean export type QueryErrorClearResetFunction = () => void export interface QueryErrorResetBoundaryValue { clearReset: QueryErrorClearResetFunction isReset: QueryErrorIsResetFunction reset: QueryErrorResetFunction } function createValue(): QueryErrorResetBoundaryValue { let isReset = false return { clearReset: () => { isReset = false }, reset: () => { isReset = true }, isReset: () => { return isReset }, } } const QueryErrorResetBoundaryContext = React.createContext(createValue()) // HOOK export const useQueryErrorResetBoundary = () => React.useContext(QueryErrorResetBoundaryContext) // COMPONENT export type QueryErrorResetBoundaryFunction = ( value: QueryErrorResetBoundaryValue, ) => React.ReactNode export interface QueryErrorResetBoundaryProps { children: QueryErrorResetBoundaryFunction | React.ReactNode } export const QueryErrorResetBoundary = ({ children, }: QueryErrorResetBoundaryProps) => { const [value] = React.useState(() => createValue()) return ( {typeof children === 'function' ? children(value) : children} ) } ================================================ FILE: packages/react-query/src/__tests__/HydrationBoundary.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import * as React from 'react' import { render } from '@testing-library/react' import * as coreModule from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { HydrationBoundary, QueryClient, QueryClientProvider, dehydrate, useQuery, } from '..' import type { hydrate } from '@tanstack/query-core' describe('React hydration', () => { let stringifiedState: string beforeEach(async () => { vi.useFakeTimers() const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(10).then(() => ['stringCached']), }) await vi.advanceTimersByTimeAsync(10) const dehydrated = dehydrate(queryClient) stringifiedState = JSON.stringify(dehydrated) queryClient.clear() }) afterEach(() => { vi.useRealTimers() }) test('should hydrate queries to the cache on context', async () => { const dehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['string']), }) return (

    {data}

    ) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() queryClient.clear() }) test('should hydrate queries to the cache on custom context', async () => { const queryClientInner = new QueryClient() const queryClientOuter = new QueryClient() const dehydratedState = JSON.parse(stringifiedState) function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['string']), }) return (

    {data}

    ) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() queryClientInner.clear() queryClientOuter.clear() }) describe('ReactQueryCacheProvider with hydration support', () => { test('should hydrate new queries if queries change', async () => { const dehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page({ queryKey }: { queryKey: [string] }) { const { data } = useQuery({ queryKey, queryFn: () => sleep(20).then(() => queryKey), }) return (

    {data}

    ) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() const intermediateClient = new QueryClient() intermediateClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['should change']), }) intermediateClient.prefetchQuery({ queryKey: ['added'], queryFn: () => sleep(20).then(() => ['added']), }) await vi.advanceTimersByTimeAsync(20) const dehydrated = dehydrate(intermediateClient) intermediateClient.clear() rendered.rerender( , ) // Existing observer should not have updated at this point, // as that would indicate a side effect in the render phase expect(rendered.getByText('string')).toBeInTheDocument() // New query data should be available immediately expect(rendered.getByText('added')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(0) // After effects phase has had time to run, the observer should have updated expect(rendered.queryByText('string')).not.toBeInTheDocument() expect(rendered.getByText('should change')).toBeInTheDocument() queryClient.clear() }) // When we hydrate in transitions that are later aborted, it could be // confusing to both developers and users if we suddenly updated existing // state on the screen (why did this update when it was not stale, nothing // remounted, I didn't change tabs etc?). // Any queries that does not exist in the cache yet can still be hydrated // since they don't have any observers on the current page that would update. test('should hydrate new but not existing queries if transition is aborted', async () => { const initialDehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page({ queryKey }: { queryKey: [string] }) { const { data } = useQuery({ queryKey, queryFn: () => sleep(20).then(() => queryKey), }) return (

    {data}

    ) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() const intermediateClient = new QueryClient() intermediateClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['should not change']), }) intermediateClient.prefetchQuery({ queryKey: ['added'], queryFn: () => sleep(20).then(() => ['added']), }) await vi.advanceTimersByTimeAsync(20) const newDehydratedState = dehydrate(intermediateClient) intermediateClient.clear() function Thrower(): never { throw new Promise(() => { // Never resolve }) } React.startTransition(() => { rendered.rerender( , ) expect(rendered.getByText('loading')).toBeInTheDocument() }) React.startTransition(() => { rendered.rerender( , ) // This query existed before the transition so it should stay the same expect(rendered.getByText('string')).toBeInTheDocument() expect( rendered.queryByText('should not change'), ).not.toBeInTheDocument() // New query data should be available immediately because it was // hydrated in the previous transition, even though the new dehydrated // state did not contain it expect(rendered.getByText('added')).toBeInTheDocument() }) await vi.advanceTimersByTimeAsync(20) // It should stay the same even after effects have had a chance to run expect(rendered.getByText('string')).toBeInTheDocument() expect(rendered.queryByText('should not change')).not.toBeInTheDocument() queryClient.clear() }) test('should hydrate queries to new cache if cache changes', async () => { const dehydratedState = JSON.parse(stringifiedState) const queryClient = new QueryClient() function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn: () => sleep(20).then(() => ['string']), }) return (

    {data}

    ) } const rendered = render( , ) expect(rendered.getByText('stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('string')).toBeInTheDocument() const newClientQueryClient = new QueryClient() rendered.rerender( , ) await vi.advanceTimersByTimeAsync(20) expect(rendered.getByText('string')).toBeInTheDocument() queryClient.clear() newClientQueryClient.clear() }) }) test('should not hydrate queries if state is null', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await Promise.all( Array.from({ length: 1000 }).map(async (_, index) => { await vi.advanceTimersByTimeAsync(index) expect(hydrateSpy).toHaveBeenCalledTimes(0) }), ) hydrateSpy.mockRestore() queryClient.clear() }) test('should not hydrate queries if state is undefined', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await vi.advanceTimersByTimeAsync(0) expect(hydrateSpy).toHaveBeenCalledTimes(0) hydrateSpy.mockRestore() queryClient.clear() }) test('should not hydrate queries if state is not an object', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await vi.advanceTimersByTimeAsync(0) expect(hydrateSpy).toHaveBeenCalledTimes(0) hydrateSpy.mockRestore() queryClient.clear() }) test('should handle state without queries property gracefully', async () => { const queryClient = new QueryClient() const hydrateSpy = vi.spyOn(coreModule, 'hydrate') function Page() { return null } render( , ) await vi.advanceTimersByTimeAsync(0) expect(hydrateSpy).toHaveBeenCalledTimes(0) hydrateSpy.mockRestore() queryClient.clear() }) // https://github.com/TanStack/query/issues/8677 test('should not infinite loop when hydrating promises that resolve to errors', async () => { const originalHydrate = coreModule.hydrate const hydrateSpy = vi.spyOn(coreModule, 'hydrate') let hydrationCount = 0 hydrateSpy.mockImplementation((...args: Parameters) => { hydrationCount++ // Arbitrary number if (hydrationCount > 10) { // This is a rough way to detect it. Calling hydrate multiple times with // the same data is usually fine, but in this case it indicates the // logic in HydrationBoundary is not working as expected. throw new Error('Too many hydrations detected') } return originalHydrate(...args) }) // For the bug to trigger, there needs to already be a query in the cache, // with a dataUpdatedAt earlier than the dehydratedAt of the next query const clientQueryClient = new QueryClient() clientQueryClient.prefetchQuery({ queryKey: ['promise'], queryFn: () => sleep(20).then(() => 'existing'), }) await vi.advanceTimersByTimeAsync(20) const prefetchQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, }, }, }) prefetchQueryClient.prefetchQuery({ queryKey: ['promise'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Query failed'))), }) const dehydratedState = dehydrate(prefetchQueryClient) // Mimic what React/our synchronous thenable does for already rejected promises // @ts-expect-error dehydratedState.queries[0].promise.status = 'failure' function Page() { const { data } = useQuery({ queryKey: ['promise'], queryFn: () => sleep(20).then(() => ['new']), }) return (

    {data}

    ) } const rendered = render( , ) expect(rendered.getByText('existing')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('new')).toBeInTheDocument() hydrateSpy.mockRestore() prefetchQueryClient.clear() clientQueryClient.clear() }) test('should not refetch when query has enabled set to false', async () => { const queryFn = vi.fn() const queryClient = new QueryClient() function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn, enabled: false, }) return
    {JSON.stringify(data)}
    } const rendered = render( , ) expect(rendered.getByText('["stringCached"]')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(queryFn).toHaveBeenCalledTimes(0) expect(rendered.getByText('["stringCached"]')).toBeInTheDocument() queryClient.clear() }) test('should not refetch when query has staleTime set to Infinity', async () => { const queryFn = vi.fn() const queryClient = new QueryClient() function Page() { const { data } = useQuery({ queryKey: ['string'], queryFn, staleTime: Infinity, }) return
    {JSON.stringify(data)}
    } const rendered = render( , ) expect(rendered.getByText('["stringCached"]')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(queryFn).toHaveBeenCalledTimes(0) expect(rendered.getByText('["stringCached"]')).toBeInTheDocument() queryClient.clear() }) }) ================================================ FILE: packages/react-query/src/__tests__/QueryClientProvider.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { render } from '@testing-library/react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryClientProvider, useQuery, useQueryClient, } from '..' describe('QueryClientProvider', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('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 { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) return (

    {data}

    ) } const rendered = render( , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() expect(queryCache.find({ queryKey: key })).toBeDefined() }) test('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 { data } = useQuery({ queryKey: key1, queryFn: () => sleep(10).then(() => 'test1'), }) return (

    {data}

    ) } function Page2() { const { data } = useQuery({ queryKey: key2, queryFn: () => sleep(10).then(() => 'test2'), }) return (

    {data}

    ) } const rendered = render( <> , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test1')).toBeInTheDocument() expect(rendered.getByText('test2')).toBeInTheDocument() expect(queryCache1.find({ queryKey: key1 })).toBeDefined() expect(queryCache1.find({ queryKey: key2 })).not.toBeDefined() expect(queryCache2.find({ queryKey: key1 })).not.toBeDefined() expect(queryCache2.find({ queryKey: key2 })).toBeDefined() }) test("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 { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) return (

    {data}

    ) } const rendered = render( , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() expect(queryCache.find({ queryKey: key })).toBeDefined() expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity) }) describe('useQueryClient', () => { test('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()).toThrow( 'No QueryClient set, use QueryClientProvider to set one', ) consoleMock.mockRestore() }) }) }) ================================================ FILE: packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import * as React from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryErrorResetBoundary, useQueries, useQuery, useSuspenseQueries, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' describe('QueryErrorResetBoundary', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) describe('useQuery', () => { it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not throw error if query is disabled', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data, status } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, enabled: !succeed, throwOnError: true, }) return (
    status: {status}
    {data}
    ) } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not throw error if query is disabled, and refetch if query becomes enabled again', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const [enabled, setEnabled] = React.useState(false) const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, enabled, throwOnError: true, }) React.useEffect(() => { setEnabled(true) }, []) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw error if query is disabled and manually refetch', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { data, refetch, status, fetchStatus } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => Promise.reject(new Error('Error'))), retry: false, enabled: false, throwOnError: true, }) return (
    status: {status}, fetchStatus: {fetchStatus}
    {data}
    ) } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) expect( rendered.getByText('status: pending, fetchStatus: idle'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('status: pending, fetchStatus: idle'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not retry fetch if the reset error boundary has not been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
    {data}
    } const rendered = renderWithClient( queryClient, {() => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should retry fetch if the reset error boundary has been reset and the query contains data from a previous fetch', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, initialData: 'initial', }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) expect(rendered.getByText('initial')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should not retry fetch if the reset error boundary has not been reset after a previous reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false let shouldReset = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( { if (shouldReset) { reset() } }} fallbackRender={({ resetErrorBoundary }) => (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = false shouldReset = true await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true shouldReset = false fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() succeed = true shouldReset = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw again on error after the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let fetchCount = 0 function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { fetchCount++ throw new Error('Error') }), retry: false, throwOnError: true, }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(fetchCount).toBe(3) consoleMock.mockRestore() }) it('should never render the component while the query is in error state', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let fetchCount = 0 let renders = 0 function Page() { const { data } = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { fetchCount++ if (fetchCount > 2) return 'data' throw new Error('Error') }), retry: false, }) renders++ return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} > loading}>
    )}
    , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => 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 act(() => 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 act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data')).toBeInTheDocument() expect(fetchCount).toBe(3) expect(renders).toBe(1) consoleMock.mockRestore() }) it('should render children', () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) function Page() { return (
    page
    ) } const rendered = renderWithClient( queryClient, , ) expect(rendered.queryByText('page')).not.toBeNull() consoleMock.mockRestore() }) it('should show error boundary when using tracked queries even though we do not track the error field', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('should refetch after error when staleTime is Infinity and previous data exists (#9728)', async () => { const key = queryKey() const queryFn = vi.fn() let count = 0 queryFn.mockImplementation(async () => { await sleep(10) count++ if (count === 2) { throw new Error('Error ' + count) } return 'Success ' + count }) function Page() { const [_, forceUpdate] = React.useState(0) React.useEffect(() => { forceUpdate(1) }, []) const { data, refetch } = useQuery({ queryKey: key, queryFn, retry: false, staleTime: Infinity, throwOnError: true, }) return (
    Data: {data}
    ) } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    Status: error
    )} >
    )}
    , ) // 1. First mount -> fetching -> Success await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: Success 1')).toBeInTheDocument() expect(queryFn).toHaveBeenCalledTimes(1) // 2. Click Refetch -> Triggers fetch -> Fails (Error 2) -> ErrorBoundary fireEvent.click(rendered.getByText('Refetch')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: error')).toBeInTheDocument() expect(queryFn).toHaveBeenCalledTimes(2) // 3. Click Retry -> Remounts // Because staleTime is Infinity and we have Data from (1), // AND we are in Error state. fireEvent.click(rendered.getByText('Retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: Success 3')).toBeInTheDocument() expect(queryFn).toHaveBeenCalledTimes(3) }) }) describe('useQueries', () => { it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { const [{ data }] = useQueries({ queries: [ { queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, throwOnError: true, retryOnMount: true, }, ], }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() succeed = true fireEvent.click(rendered.getByText('retry')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) it('with suspense should retry fetch if the reset error boundary has been reset', async () => { const key = queryKey() const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let succeed = false function Page() { const [{ data }] = useSuspenseQueries({ queries: [ { queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Error') return 'data' }), retry: false, retryOnMount: true, }, ], }) return
    {data}
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => 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 act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data')).toBeInTheDocument() consoleMock.mockRestore() }) }) }) ================================================ FILE: packages/react-query/src/__tests__/fine-grained-persister.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import * as React from 'react' import { PERSISTER_KEY_PREFIX, experimental_createQueryPersister, } from '@tanstack/query-persist-client-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, hashKey, useQuery } from '..' import { renderWithClient } from './utils' describe('fine grained persister', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should restore query state from persister and not refetch', async () => { const key = queryKey() const hash = hashKey(key) const spy = vi.fn(() => Promise.resolve('Works from queryFn')) const mapStorage = new Map() const storage = { getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)), setItem: (itemKey: string, value: unknown) => { mapStorage.set(itemKey, value) return Promise.resolve() }, removeItem: (itemKey: string) => { mapStorage.delete(itemKey) return Promise.resolve() }, } await storage.setItem( `${PERSISTER_KEY_PREFIX}-${hash}`, JSON.stringify({ buster: '', queryHash: hash, queryKey: key, state: { dataUpdatedAt: Date.now(), data: 'Works from persister', }, }), ) function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: key, queryFn: spy, persister: experimental_createQueryPersister({ storage, }).persisterFn, staleTime: 5000, }) return
    setRef(value)}>{data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Works from persister')).toBeInTheDocument() expect(spy).not.toHaveBeenCalled() }) it('should restore query state from persister and refetch', async () => { const key = queryKey() const hash = hashKey(key) const spy = vi.fn(async () => { await sleep(5) return 'Works from queryFn' }) const mapStorage = new Map() const storage = { getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)), setItem: (itemKey: string, value: unknown) => { mapStorage.set(itemKey, value) return Promise.resolve() }, removeItem: (itemKey: string) => { mapStorage.delete(itemKey) return Promise.resolve() }, } await storage.setItem( `${PERSISTER_KEY_PREFIX}-${hash}`, JSON.stringify({ buster: '', queryHash: hash, queryKey: key, state: { dataUpdatedAt: Date.now(), data: 'Works from persister', }, }), ) function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: key, queryFn: spy, persister: experimental_createQueryPersister({ storage, }).persisterFn, }) return
    setRef(value)}>{data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Works from persister')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(6) expect(rendered.getByText('Works from queryFn')).toBeInTheDocument() expect(spy).toHaveBeenCalledTimes(1) }) it('should store query state to persister after fetch', async () => { const key = queryKey() const hash = hashKey(key) const spy = vi.fn(() => Promise.resolve('Works from queryFn')) const mapStorage = new Map() const storage = { getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)), setItem: (itemKey: string, value: unknown) => { mapStorage.set(itemKey, value) return Promise.resolve() }, removeItem: (itemKey: string) => { mapStorage.delete(itemKey) return Promise.resolve() }, } function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: key, queryFn: spy, persister: experimental_createQueryPersister({ storage, }).persisterFn, }) return
    setRef(value)}>{data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Works from queryFn')).toBeInTheDocument() expect(spy).toHaveBeenCalledTimes(1) const storedItem = await storage.getItem(`${PERSISTER_KEY_PREFIX}-${hash}`) expect(JSON.parse(storedItem)).toMatchObject({ state: { data: 'Works from queryFn', }, }) }) }) ================================================ FILE: packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it, test } from 'vitest' import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core' import { infiniteQueryOptions } from '../infiniteQueryOptions' import { useInfiniteQuery } from '../useInfiniteQuery' import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery' import { useQuery } from '../useQuery' import type { DataTag, InfiniteData, InitialDataFunction, } from '@tanstack/query-core' describe('infiniteQueryOptions', () => { it('should not allow excess properties', () => { assertType( infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), getNextPageParam: () => 1, initialPageParam: 1, // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }), ) }) it('should infer types for callbacks', () => { infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), staleTime: 1000, getNextPageParam: () => 1, initialPageParam: 1, select: (data) => { expectTypeOf(data).toEqualTypeOf>() }, }) }) it('should work when passed to useInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const { data } = useInfiniteQuery(options) // known issue: type of pageParams is unknown when returned from useInfiniteQuery expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) it('should work when passed to useSuspenseInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const { data } = useSuspenseInfiniteQuery(options) expectTypeOf(data).toEqualTypeOf>() }) it('should work when passed to fetchInfiniteQuery', async () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const data = await new QueryClient().fetchInfiniteQuery(options) expectTypeOf(data).toEqualTypeOf>() }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => 'string', getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>() }) it('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), select: (data) => data.pages, getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>() }) it('should return the proper type when passed to getQueryData', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) it('should properly type when passed to setQueryData', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf< InfiniteData | undefined >() return prev }) expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) it('should throw a type error when using queryFn with skipToken in a suspense query', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) // @ts-expect-error TS2345 const { data } = useSuspenseInfiniteQuery(options) expectTypeOf(data).toEqualTypeOf>() }) test('should not be allowed to be passed to non-infinite query functions', () => { const queryClient = new QueryClient() const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) assertType( // @ts-expect-error cannot pass infinite options to non-infinite query functions useQuery(options), ) assertType( // @ts-expect-error cannot pass infinite options to non-infinite query functions queryClient.ensureQueryData(options), ) assertType( // @ts-expect-error cannot pass infinite options to non-infinite query functions queryClient.fetchQuery(options), ) assertType( // @ts-expect-error cannot pass infinite options to non-infinite query functions queryClient.prefetchQuery(options), ) }) test('allow optional initialData function', () => { const initialData: { example: boolean } | undefined = { example: true } const queryOptions = infiniteQueryOptions({ queryKey: ['example'], queryFn: () => initialData, initialData: initialData ? () => ({ pages: [initialData], pageParams: [] }) : undefined, getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryOptions.initialData).toMatchTypeOf< | InitialDataFunction> | InfiniteData<{ example: boolean }, number> | undefined >() }) test('allow optional initialData object', () => { const initialData: { example: boolean } | undefined = { example: true } const queryOptions = infiniteQueryOptions({ queryKey: ['example'], queryFn: () => initialData, initialData: initialData ? { pages: [initialData], pageParams: [] } : undefined, getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryOptions.initialData).toMatchTypeOf< | InitialDataFunction> | InfiniteData<{ example: boolean }, number> | undefined >() }) it('should return a custom query key type', () => { type MyQueryKey = [Array, { type: 'foo' }] const options = infiniteQueryOptions({ queryKey: [['key'], { type: 'foo' }] as MyQueryKey, queryFn: () => Promise.resolve(1), getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(options.queryKey).toEqualTypeOf< DataTag, Error> >() }) it('should return a custom query key type with datatag', () => { type MyQueryKey = DataTag< [Array, { type: 'foo' }], number, Error & { myMessage: string } > const options = infiniteQueryOptions({ queryKey: [['key'], { type: 'foo' }] as MyQueryKey, queryFn: () => Promise.resolve(1), getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(options.queryKey).toEqualTypeOf< DataTag, Error & { myMessage: string }> >() }) }) ================================================ FILE: packages/react-query/src/__tests__/infiniteQueryOptions.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { infiniteQueryOptions } from '../infiniteQueryOptions' import type { UseInfiniteQueryOptions } from '../types' describe('infiniteQueryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: UseInfiniteQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), getNextPageParam: () => null, initialPageParam: null, } expect(infiniteQueryOptions(object)).toBe(object) }) }) ================================================ FILE: packages/react-query/src/__tests__/mutationOptions.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { useIsMutating, useMutation, useMutationState } from '..' import { mutationOptions } from '../mutationOptions' import type { DefaultError, MutationFunctionContext, MutationState, WithRequired, } from '@tanstack/query-core' import type { UseMutationOptions, UseMutationResult } from '../types' describe('mutationOptions', () => { it('should not allow excess properties', () => { // @ts-expect-error this is a good error, because onMutates does not exist! mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutates: 1000, onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer types for callbacks', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should infer types for onError callback', () => { mutationOptions({ mutationFn: () => { throw new Error('fail') }, mutationKey: ['key'], onError: (error) => { expectTypeOf(error).toEqualTypeOf() }, }) }) it('should infer types for variables', () => { mutationOptions({ mutationFn: (vars) => { expectTypeOf(vars).toEqualTypeOf<{ id: string }>() return Promise.resolve(5) }, mutationKey: ['with-vars'], }) }) it('should infer result type correctly', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutate: () => { return { name: 'onMutateResult' } }, onSuccess: (_data, _variables, onMutateResult) => { expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>() }, }) }) it('should infer context type correctly', () => { mutationOptions({ mutationFn: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() return Promise.resolve(5) }, mutationKey: ['key'], onMutate: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() }, onSuccess: (_data, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onError: (_error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onSettled: (_data, _error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, }) }) it('should error if mutationFn return type mismatches TData', () => { assertType( mutationOptions({ // @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() }, }) }) it('should infer all types when not explicitly provided', () => { expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< WithRequired< UseMutationOptions, 'mutationKey' > >() expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< Omit, 'mutationKey'> >() }) it('should infer types when used with useMutation', () => { const mutation = useMutation( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) expectTypeOf(mutation).toEqualTypeOf< UseMutationResult >() useMutation( // should allow when used with useMutation without mutationKey mutationOptions({ mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) }) it('should infer types when used with useIsMutating', () => { const isMutating = useIsMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating).toEqualTypeOf() 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: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating).toEqualTypeOf() 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: ['key'], mutationFn: () => Promise.resolve(5), }), }) expectTypeOf(mutationState).toEqualTypeOf< Array> >() useMutationState({ // @ts-expect-error filters should have mutationKey filters: mutationOptions({ mutationFn: () => Promise.resolve(5), }), }) }) }) ================================================ FILE: packages/react-query/src/__tests__/mutationOptions.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { fireEvent } from '@testing-library/react' import { mutationOptions } from '../mutationOptions' import { useIsMutating, useMutation, useMutationState } from '..' import { renderWithClient } from './utils' import type { MutationState } from '@tanstack/query-core' import type { UseMutationOptions } from '../types' describe('mutationOptions', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => { const object: UseMutationOptions = { 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: UseMutationOptions = { 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 = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data'), }) function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate } = useMutation(mutationOpts) return (
    ) } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) 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(51) 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 = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data'), }) function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate } = useMutation(mutationOpts) return (
    ) } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) 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(51) 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 = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) return (
    ) } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) 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(2) await vi.advanceTimersByTimeAsync(51) expect(isMutatingArray[2]).toEqual(0) 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 = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) function IsMutating() { const isMutating = useIsMutating({ mutationKey: mutationOpts1.mutationKey, }) isMutatingArray.push(isMutating) return null } function Mutation() { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) return (
    ) } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) 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(51) 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 = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data'), }) function Mutation() { const isMutating = queryClient.isMutating(mutationOpts) const { mutate } = useMutation(mutationOpts) isMutatingArray.push(isMutating) return (
    ) } const rendered = renderWithClient(queryClient, ) 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(501) 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 (without mutationKey in mutationOptions)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data'), }) function Mutation() { const isMutating = queryClient.isMutating() const { mutate } = useMutation(mutationOpts) isMutatingArray.push(isMutating) return (
    ) } const rendered = renderWithClient(queryClient, ) 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(501) 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', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) function Mutation() { const isMutating = queryClient.isMutating() const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) isMutatingArray.push(isMutating) return (
    ) } const rendered = renderWithClient(queryClient, ) 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(2) await vi.advanceTimersByTimeAsync(501) 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 (filter mutationOpt1.mutationKey)', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) function Mutation() { const isMutating = queryClient.isMutating({ mutationKey: mutationOpts1.mutationKey, }) const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) isMutatingArray.push(isMutating) return (
    ) } const rendered = renderWithClient(queryClient, ) 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(501) expect(isMutatingArray[2]).toEqual(0) expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0) }) it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data'), }) function Mutation() { const { mutate } = useMutation(mutationOpts) const data = useMutationState({ filters: { mutationKey: mutationOpts.mutationKey, status: 'success' }, }) mutationStateArray.push(...data) return (
    ) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data') }) it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data'), }) function Mutation() { const { mutate } = useMutation(mutationOpts) const data = useMutationState({ filters: { status: 'success' }, }) mutationStateArray.push(...data) return (
    ) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data') }) it('should return the number of fetching mutations when used with useMutationState', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], 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 data = useMutationState({ filters: { status: 'success' }, }) mutationStateArray.push(...data) return (
    ) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(2) expect(mutationStateArray[0]?.data).toEqual('data1') expect(mutationStateArray[1]?.data).toEqual('data2') }) it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => { const mutationStateArray: Array< MutationState > = [] const queryClient = new QueryClient() const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], 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 data = useMutationState({ filters: { mutationKey: mutationOpts1.mutationKey, status: 'success' }, }) mutationStateArray.push(...data) return (
    ) } const rendered = renderWithClient(queryClient, ) expect(mutationStateArray.length).toEqual(0) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect(mutationStateArray.length).toEqual(1) expect(mutationStateArray[0]?.data).toEqual('data1') expect(mutationStateArray[1]).toBeFalsy() }) }) ================================================ FILE: packages/react-query/src/__tests__/queryOptions.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueriesObserver, QueryClient, dataTagSymbol, skipToken, } from '@tanstack/query-core' import { queryOptions } from '../queryOptions' import { useQuery } from '../useQuery' import { useQueries } from '../useQueries' import { useSuspenseQuery } from '../useSuspenseQuery' import type { AnyUseQueryOptions } from '../types' import type { DataTag, InitialDataFunction, QueryObserverResult, } from '@tanstack/query-core' describe('queryOptions', () => { it('should not allow excess properties', () => { assertType( queryOptions({ queryKey: ['key'], 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: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should work when passed to useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = useQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to useSuspenseQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = useSuspenseQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const data = await new QueryClient().fetchQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to useQueries', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const [{ data }] = useQueries({ queries: [options], }) expectTypeOf(data).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should return the proper type when passed to getQueryState', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const state = queryClient.getQueryState(queryKey) expectTypeOf(state?.data).toEqualTypeOf() }) it('should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) it('should infer even if there is a conditional skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer to unknown if we disable a query with just a skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: skipToken, }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should throw a type error when using queryFn with skipToken in a suspense query', () => { const options = queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }) // @ts-expect-error TS2345 const { data } = useSuspenseQuery(options) expectTypeOf(data).toEqualTypeOf() }) it('should return the proper type when passed to QueriesObserver', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const queriesObserver = new QueriesObserver(queryClient, [options]) expectTypeOf(queriesObserver).toEqualTypeOf< QueriesObserver> >() }) it('should allow undefined response in initialData', () => { assertType((id: string | null) => queryOptions({ queryKey: ['todo', id], queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry', }), initialData: () => !id ? undefined : { id, title: 'Initial Data', }, }), ) }) it('should allow optional initialData object', () => { const testFn = (id?: string) => { const options = queryOptions({ queryKey: ['test'], queryFn: () => Promise.resolve('something string'), initialData: id ? 'initial string' : undefined, }) expectTypeOf(options.initialData).toMatchTypeOf< InitialDataFunction | string | undefined >() } testFn('id') testFn() }) it('should be passable to UseQueryOptions', () => { function somethingWithQueryOptions( options: TQueryOpts, ) { return options.queryKey } const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) assertType(somethingWithQueryOptions(options)) }) it('should return a custom query key type', () => { type MyQueryKey = [Array, { type: 'foo' }] const options = queryOptions({ queryKey: [['key'], { type: 'foo' }] as MyQueryKey, queryFn: () => Promise.resolve(1), }) expectTypeOf(options.queryKey).toEqualTypeOf< DataTag >() }) it('should return a custom query key type with datatag', () => { type MyQueryKey = DataTag< [Array, { type: 'foo' }], number, Error & { myMessage: string } > const options = queryOptions({ queryKey: [['key'], { type: 'foo' }] as MyQueryKey, queryFn: () => Promise.resolve(1), }) expectTypeOf(options.queryKey).toEqualTypeOf< DataTag >() }) }) ================================================ FILE: packages/react-query/src/__tests__/queryOptions.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { queryOptions } from '../queryOptions' import type { UseQueryOptions } from '../types' describe('queryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: UseQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), } as const expect(queryOptions(object)).toBe(object) }) }) ================================================ FILE: packages/react-query/src/__tests__/ssr-hydration.test.tsx ================================================ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { hydrateRoot } from 'react-dom/client' import { act } from 'react' import * as ReactDOMServer from 'react-dom/server' import { QueryCache, QueryClient, QueryClientProvider, dehydrate, hydrate, useQuery, } from '..' import { setIsServer } from './utils' const ReactHydrate = (element: React.ReactElement, container: Element) => { let root: any act(() => { root = hydrateRoot(container, element) }) return () => { root.unmount() } } async function fetchData(value: TData, ms?: number): Promise { await vi.advanceTimersByTimeAsync(ms || 1) return value } function PrintStateComponent({ componentName, result }: any): any { return `${componentName} - status:${result.status} fetching:${result.isFetching} data:${result.data}` } describe('Server side rendering with de/rehydration', () => { let previousIsReactActEnvironment: unknown beforeAll(() => { // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist previousIsReactActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT = true vi.useFakeTimers() }) afterAll(() => { // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist globalThis.IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment vi.useRealTimers() }) it('should not mismatch on success', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const fetchDataSuccess = vi.fn(fetchData) // -- Shared part -- function SuccessComponent() { const result = useQuery({ queryKey: ['success'], queryFn: () => fetchDataSuccess('success!'), }) return ( ) } // -- Server part -- setIsServer(true) const prefetchCache = new QueryCache() const prefetchClient = new QueryClient({ queryCache: prefetchCache, }) await prefetchClient.prefetchQuery({ queryKey: ['success'], queryFn: () => fetchDataSuccess('success'), }) const dehydratedStateServer = dehydrate(prefetchClient) const renderCache = new QueryCache() const renderClient = new QueryClient({ queryCache: renderCache, }) hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( , ) const stringifiedState = JSON.stringify(dehydratedStateServer) renderClient.clear() setIsServer(false) const expectedMarkup = 'SuccessComponent - status:success fetching:true data:success' expect(markup).toBe(expectedMarkup) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) // -- Client part -- const el = document.createElement('div') el.innerHTML = markup const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) const unmount = ReactHydrate( , el, ) // Check that we have no React hydration mismatches expect(consoleMock).toHaveBeenCalledTimes(0) expect(fetchDataSuccess).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe(expectedMarkup) unmount() queryClient.clear() consoleMock.mockRestore() }) it('should not mismatch on error', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const fetchDataError = vi.fn(() => { throw new Error('fetchDataError') }) // -- Shared part -- function ErrorComponent() { const result = useQuery({ queryKey: ['error'], queryFn: () => fetchDataError(), retry: false, }) return ( ) } // -- Server part -- setIsServer(true) const prefetchCache = new QueryCache() const prefetchClient = new QueryClient({ queryCache: prefetchCache, }) await prefetchClient.prefetchQuery({ queryKey: ['error'], queryFn: () => fetchDataError(), }) const dehydratedStateServer = dehydrate(prefetchClient) const renderCache = new QueryCache() const renderClient = new QueryClient({ queryCache: renderCache, }) hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( , ) const stringifiedState = JSON.stringify(dehydratedStateServer) renderClient.clear() setIsServer(false) const expectedMarkup = 'ErrorComponent - status:pending fetching:true data:undefined' expect(markup).toBe(expectedMarkup) // -- Client part -- const el = document.createElement('div') el.innerHTML = markup const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) const unmount = ReactHydrate( , el, ) expect(consoleMock).toHaveBeenCalledTimes(0) expect(fetchDataError).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe(expectedMarkup) await vi.advanceTimersByTimeAsync(50) expect(fetchDataError).toHaveBeenCalledTimes(2) expect(el.innerHTML).toBe( 'ErrorComponent - status:error fetching:false data:undefined', ) unmount() queryClient.clear() consoleMock.mockRestore() }) it('should not mismatch on queries that were not prefetched', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const fetchDataSuccess = vi.fn(fetchData) // -- Shared part -- function SuccessComponent() { const result = useQuery({ queryKey: ['success'], queryFn: () => fetchDataSuccess('success!'), }) return ( ) } // -- Server part -- setIsServer(true) const prefetchClient = new QueryClient() const dehydratedStateServer = dehydrate(prefetchClient) const renderClient = new QueryClient() hydrate(renderClient, dehydratedStateServer) const markup = ReactDOMServer.renderToString( , ) const stringifiedState = JSON.stringify(dehydratedStateServer) renderClient.clear() setIsServer(false) const expectedMarkup = 'SuccessComponent - status:pending fetching:true data:undefined' expect(markup).toBe(expectedMarkup) // -- Client part -- const el = document.createElement('div') el.innerHTML = markup const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) hydrate(queryClient, JSON.parse(stringifiedState)) const unmount = ReactHydrate( , el, ) // Check that we have no React hydration mismatches expect(consoleMock).toHaveBeenCalledTimes(0) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) expect(el.innerHTML).toBe(expectedMarkup) await vi.advanceTimersByTimeAsync(50) expect(fetchDataSuccess).toHaveBeenCalledTimes(1) expect(el.innerHTML).toBe( 'SuccessComponent - status:success fetching:false data:success!', ) unmount() queryClient.clear() consoleMock.mockRestore() }) }) ================================================ FILE: packages/react-query/src/__tests__/ssr.test.tsx ================================================ import * as React from 'react' import { renderToString } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryClientProvider, useInfiniteQuery, useIsFetching, useMutation, useMutationState, useQueries, useQuery, } from '..' import { setIsServer } from './utils' describe('Server Side Rendering', () => { setIsServer(true) let queryCache: QueryCache let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryCache = new QueryCache() queryClient = new QueryClient({ queryCache }) }) afterEach(() => { vi.useRealTimers() }) it('should not trigger fetch', () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'data')) function Page() { const query = useQuery({ queryKey: key, queryFn }) const content = `status ${query.status}` return (
    {content}
    ) } const markup = renderToString( , ) expect(markup).toContain('status pending') expect(queryFn).toHaveBeenCalledTimes(0) queryCache.clear() }) it('should add prefetched data to cache', async () => { const key = queryKey() const promise = queryClient.fetchQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'data'), }) await vi.advanceTimersByTimeAsync(10) const data = await promise expect(data).toBe('data') expect(queryCache.find({ queryKey: key })?.state.data).toBe('data') queryCache.clear() }) it('should return existing data from the cache', async () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'data')) function Page() { const query = useQuery({ queryKey: key, queryFn }) const content = `status ${query.status}` return (
    {content}
    ) } queryClient.prefetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(10) const markup = renderToString( , ) expect(markup).toContain('status success') expect(queryFn).toHaveBeenCalledTimes(1) queryCache.clear() }) it('should add initialData to the cache', () => { const key = queryKey() function Page() { const [page, setPage] = React.useState(1) const { data } = useQuery({ queryKey: [key, page], queryFn: () => sleep(10).then(() => page), initialData: 1, }) return (

    {data}

    ) } renderToString( , ) const keys = queryCache.getAll().map((query) => query.queryKey) expect(keys).toEqual([[key, 1]]) queryCache.clear() }) it('useInfiniteQuery should return the correct state', async () => { const key = queryKey() const queryFn = vi.fn(() => sleep(10).then(() => 'page 1')) function Page() { const query = useInfiniteQuery({ queryKey: key, queryFn, getNextPageParam: () => undefined, initialPageParam: 0, }) return (
      {query.data?.pages.map((page) => (
    • {page}
    • ))}
    ) } queryClient.prefetchInfiniteQuery({ queryKey: key, queryFn, initialPageParam: 0, }) await vi.advanceTimersByTimeAsync(10) const markup = renderToString( , ) expect(markup).toContain('page 1') expect(queryFn).toHaveBeenCalledTimes(1) queryCache.clear() }) it('useIsFetching should return 0 after prefetch completes', async () => { const key = queryKey() const queryFn = () => sleep(10).then(() => 'data') function Page() { const { data } = useQuery({ queryKey: key, queryFn }) const isFetching = useIsFetching() return (
    {data}
    {`isFetching: ${isFetching}`}
    ) } queryClient.prefetchQuery({ queryKey: key, queryFn }) await vi.advanceTimersByTimeAsync(10) const markup = renderToString( , ) expect(markup).toContain('data') expect(markup).toContain('isFetching: 0') queryCache.clear() }) it('useQueries should return existing data from the cache', async () => { const key1 = queryKey() const key2 = queryKey() const queryFn1 = () => sleep(10).then(() => 'data1') const queryFn2 = () => sleep(10).then(() => 'data2') function Page() { const queries = useQueries({ queries: [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ], }) return (
    {`status1: ${queries[0].status}`}
    {`status2: ${queries[1].status}`}
    {`data1: ${queries[0].data}`}
    {`data2: ${queries[1].data}`}
    ) } queryClient.prefetchQuery({ queryKey: key1, queryFn: queryFn1 }) queryClient.prefetchQuery({ queryKey: key2, queryFn: queryFn2 }) await vi.advanceTimersByTimeAsync(10) const markup = renderToString( , ) expect(markup).toContain('status1: success') expect(markup).toContain('status2: success') expect(markup).toContain('data1: data1') expect(markup).toContain('data2: data2') queryCache.clear() }) it('useMutation should return idle status', () => { function Page() { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => 'data'), }) return
    {`status: ${mutation.status}`}
    } const markup = renderToString( , ) expect(markup).toContain('status: idle') queryCache.clear() }) it('useMutationState should return empty array', () => { function Page() { const mutationState = useMutationState() return
    {`mutationState: ${mutationState.length}`}
    } const markup = renderToString( , ) expect(markup).toContain('mutationState: 0') queryCache.clear() }) }) ================================================ FILE: packages/react-query/src/__tests__/suspense.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, render } from '@testing-library/react' import { Suspense } from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, QueryClientProvider, useSuspenseQuery } from '..' import type { StaleTime } from '@tanstack/query-core' import type { QueryKey } from '..' function renderWithSuspense(client: QueryClient, ui: React.ReactNode) { return render( {ui} , ) } function createTestQuery(options: { fetchCount: { count: number } queryKey: QueryKey staleTime?: StaleTime | (() => StaleTime) }) { return function TestComponent() { const { data } = useSuspenseQuery({ queryKey: options.queryKey, queryFn: () => sleep(10).then(() => { options.fetchCount.count++ return 'data' }), staleTime: options.staleTime, }) return
    data: {data}
    } } describe('Suspense Timer Tests', () => { let queryClient: QueryClient let fetchCount: { count: number } beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }) fetchCount = { count: 0 } }) afterEach(() => { vi.useRealTimers() }) it('should enforce minimum staleTime of 1000ms when using suspense with number', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: ['test'], staleTime: 10, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(100)) expect(fetchCount.count).toBe(1) }) it('should enforce minimum staleTime of 1000ms when using suspense with function', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: ['test-func'], staleTime: () => 10, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(100)) expect(fetchCount.count).toBe(1) }) it('should respect staleTime when value is greater than 1000ms', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: 2000, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(1500)) expect(fetchCount.count).toBe(1) }) it('should enforce minimum staleTime when undefined is provided', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: undefined, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(500)) expect(fetchCount.count).toBe(1) }) it('should preserve staleTime when value is static', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: 'static', }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(2000)) expect(fetchCount.count).toBe(1) }) it('should preserve staleTime when function returns static', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: () => 'static', }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(2000)) expect(fetchCount.count).toBe(1) }) it('should respect staleTime when function returns value greater than 1000ms', async () => { const TestComponent = createTestQuery({ fetchCount, queryKey: queryKey(), staleTime: () => 3000, }) const rendered = renderWithSuspense(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: data')).toBeInTheDocument() rendered.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(2000)) expect(fetchCount.count).toBe(1) }) }) ================================================ FILE: packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' describe('pageParam', () => { it('initialPageParam should define type of param passed to queryFunctionContext', () => { useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { expectTypeOf(pageParam).toEqualTypeOf() }, initialPageParam: 1, getNextPageParam: () => undefined, }) }) it('direction should be passed to queryFn of useInfiniteQuery', () => { useInfiniteQuery({ queryKey: ['key'], 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() queryClient.fetchInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { expectTypeOf(pageParam).toEqualTypeOf() }, initialPageParam: 1, }) }) it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => { const queryClient = new QueryClient() queryClient.prefetchInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { expectTypeOf(pageParam).toEqualTypeOf() }, initialPageParam: 1, }) }) }) describe('select', () => { it('should still return paginated data if no select result', () => { const infiniteQuery = useInfiniteQuery({ queryKey: ['key'], 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 | undefined >() }) it('should be able to transform data to arbitrary result', () => { const infiniteQuery = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return pageParam * 5 }, initialPageParam: 1, getNextPageParam: () => undefined, select: (data) => { expectTypeOf(data).toEqualTypeOf>() return 'selected' as const }, }) expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>() }) }) describe('getNextPageParam / getPreviousPageParam', () => { it('should get typed params', () => { const infiniteQuery = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return String(pageParam) }, initialPageParam: 1, getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { expectTypeOf(lastPage).toEqualTypeOf() expectTypeOf(allPages).toEqualTypeOf>() expectTypeOf(lastPageParam).toEqualTypeOf() expectTypeOf(allPageParams).toEqualTypeOf>() return undefined }, getPreviousPageParam: ( firstPage, allPages, firstPageParam, allPageParams, ) => { expectTypeOf(firstPage).toEqualTypeOf() expectTypeOf(allPages).toEqualTypeOf>() expectTypeOf(firstPageParam).toEqualTypeOf() expectTypeOf(allPageParams).toEqualTypeOf>() return undefined }, }) // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(infiniteQuery.data).toEqualTypeOf< InfiniteData | undefined >() }) }) describe('error booleans', () => { it('should not be permanently `false`', () => { const { isFetchNextPageError, isFetchPreviousPageError, isLoadingError, isRefetchError, } = useInfiniteQuery({ queryKey: ['key'], queryFn: ({ pageParam }) => { return pageParam * 5 }, initialPageParam: 1, getNextPageParam: () => undefined, }) expectTypeOf(isFetchNextPageError).toEqualTypeOf() expectTypeOf(isFetchPreviousPageError).toEqualTypeOf() expectTypeOf(isLoadingError).toEqualTypeOf() expectTypeOf(isRefetchError).toEqualTypeOf() }) }) ================================================ FILE: packages/react-query/src/__tests__/useInfiniteQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { createRenderStream, useTrackRenders, } from '@testing-library/react-render-stream' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryClientProvider, keepPreviousData, useInfiniteQuery, } from '..' import { renderWithClient, setActTimeout } from './utils' import type { InfiniteData, QueryFunctionContext, UseInfiniteQueryResult, } from '..' import type { Mock } from 'vitest' interface Result { items: Array nextId?: number prevId?: number ts: number } const pageSize = 10 const fetchItems = async ( page: number, ts: number, noNext?: boolean, noPrev?: boolean, ): Promise => { await sleep(10) return { items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d), nextId: noNext ? undefined : page + 1, prevId: noPrev ? undefined : page - 1, ts, } } describe('useInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) it('should return the correct states for a successful query', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: false, hasPreviousPage: false, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: true, isPending: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: { pages: [0], pageParams: [0] }, dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: true, hasPreviousPage: false, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: false, isPending: false, isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, isEnabled: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }) }) it('should not throw when fetchNextPage returns an error', async () => { const key = queryKey() let noThrow = false function Page() { const start = 1 const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (pageParam === 2) throw new Error('error') return pageParam }), retry: 1, retryDelay: 10, getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: start, }) const { fetchNextPage } = state React.useEffect(() => { setActTimeout(() => { fetchNextPage() .then(() => { noThrow = true }) .catch(() => undefined) }, 20) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(50) expect(noThrow).toBe(true) }) it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: Array>> = [] function Page() { const [order, setOrder] = React.useState('desc') const state = useInfiniteQuery({ queryKey: [key, order], queryFn: ({ pageParam }) => sleep(10).then(() => `${pageParam}-${order}`), getNextPageParam: () => 1, initialPageParam: 0, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? 'null'}
    isFetching: {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0-desc')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0-desc,1-desc')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /order/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0-asc')).toBeInTheDocument() expect(rendered.getByText('isFetching: false')).toBeInTheDocument() expect(states.length).toBe(6) expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchingNextPage: false, isSuccess: false, isPlaceholderData: false, }) expect(states[1]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: false, }) expect(states[2]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: true, isFetchingNextPage: true, isSuccess: true, isPlaceholderData: false, }) expect(states[3]).toMatchObject({ data: { pages: ['0-desc', '1-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[4]).toMatchObject({ data: { pages: ['0-desc', '1-desc'] }, isFetching: true, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: true, }) expect(states[5]).toMatchObject({ data: { pages: ['0-asc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, isPlaceholderData: false, }) }) it('should be able to select a part of the data', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ({ count: 1 })), select: (data) => ({ pages: data.pages.map((x) => `count: ${x.count}`), pageParams: data.pageParams, }), getNextPageParam: () => undefined, initialPageParam: 0, }) states.push(state) return
    {state.data?.pages.join(',')}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('count: 1')).toBeInTheDocument() expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: ['count: 1'] }, isSuccess: true, }) }) it('should be able to select a new result and not cause infinite renders', async () => { const key = queryKey() const states: Array< UseInfiniteQueryResult> > = [] let selectCalled = 0 function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ({ count: 1 })), select: React.useCallback((data: InfiniteData<{ count: number }>) => { selectCalled++ return { pages: data.pages.map((x) => ({ ...x, id: Math.random() })), pageParams: data.pageParams, } }, []), getNextPageParam: () => undefined, initialPageParam: 0, }) states.push(state) return (
    {state.data?.pages.map((page) => (
    count: {page.count}
    ))}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('count: 1')).toBeInTheDocument() expect(states.length).toBe(2) expect(selectCalled).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [{ count: 1 }] }, isSuccess: true, }) }) it('should be able to reverse the data', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), notifyOnChangeProps: 'all', getNextPageParam: () => 1, initialPageParam: 0, }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? 'null'}
    isFetching: {state.isFetching}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1,0')).toBeInTheDocument() expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [0] }, isSuccess: true, }) expect(states[2]).toMatchObject({ data: { pages: [0] }, isSuccess: true, }) expect(states[3]).toMatchObject({ data: { pages: [1, 0] }, isSuccess: true, }) }) it('should be able to fetch a previous page', async () => { const key = queryKey() const states: Array>> = [] function Page() { const start = 10 const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, getPreviousPageParam: (firstPage) => firstPage - 1, notifyOnChangeProps: 'all', }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? null}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click( rendered.getByRole('button', { name: /fetch previous page/i }), ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 9,10')).toBeInTheDocument() expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, hasNextPage: false, hasPreviousPage: false, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, hasPreviousPage: true, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isSuccess: true, }) expect(states[2]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, hasPreviousPage: true, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: true, isSuccess: true, }) expect(states[3]).toMatchObject({ data: { pages: [9, 10] }, hasNextPage: true, hasPreviousPage: true, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isSuccess: true, }) }) it('should be able to refetch when providing page params automatically', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? 'null'}
    isFetching: {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10,11')).toBeInTheDocument() fireEvent.click( rendered.getByRole('button', { name: /fetchPreviousPage/i }), ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 9,10,11')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(31) expect(states.length).toBe(8) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchingNextPage: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchingNextPage: false, isRefetching: false, }) // Fetch next page expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchingNextPage: true, isRefetching: false, }) // Fetch next page done expect(states[3]).toMatchObject({ data: { pages: [10, 11] }, isFetching: false, isFetchingNextPage: false, isRefetching: false, }) // Fetch previous page expect(states[4]).toMatchObject({ data: { pages: [10, 11] }, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: true, isRefetching: false, }) // Fetch previous page done expect(states[5]).toMatchObject({ data: { pages: [9, 10, 11] }, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isRefetching: false, }) // Refetch expect(states[6]).toMatchObject({ data: { pages: [9, 10, 11] }, isFetching: true, isFetchingNextPage: false, isFetchingPreviousPage: false, isRefetching: true, }) // Refetch done expect(states[7]).toMatchObject({ data: { pages: [9, 10, 11] }, isFetching: false, isFetchingNextPage: false, isFetchingPreviousPage: false, isRefetching: false, }) }) it('should return the correct states when refetch fails', async () => { const key = queryKey() const states: Array>> = [] let isRefetch = false function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (isRefetch) throw new Error() return pageParam }), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', retry: false, }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? 'null'}
    isFetching: {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Refetch expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: true, }) // Refetch failed expect(states[3]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: true, isRefetching: false, }) }) it('should return the correct states when fetchNextPage fails', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (pageParam !== 10) throw new Error() return pageParam }), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', retry: false, }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? 'null'}
    isFetching: {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Fetch next page expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: true, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Fetch next page failed expect(states[3]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: true, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) }) it('should return the correct states when fetchPreviousPage fails', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => { if (pageParam !== 10) throw new Error() return pageParam }), initialPageParam: 10, getPreviousPageParam: (firstPage) => firstPage - 1, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', retry: false, }) states.push(state) return (
    data: {state.data?.pages.join(',') ?? 'null'}
    isFetching: {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 10')).toBeInTheDocument() fireEvent.click( rendered.getByRole('button', { name: /fetchPreviousPage/i }), ) expect(rendered.getByText('isFetching: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) // Initial fetch expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Initial fetch done expect(states[1]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) // Fetch previous page expect(states[2]).toMatchObject({ data: { pages: [10] }, isFetching: true, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: true, isRefetchError: false, isRefetching: false, }) // Fetch previous page failed expect(states[3]).toMatchObject({ data: { pages: [10] }, isFetching: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: true, isFetchingPreviousPage: false, isRefetchError: false, isRefetching: false, }) }) it('should silently cancel any ongoing fetch when fetching more', async () => { const key = queryKey() function Page() { const start = 10 const { data, fetchNextPage, refetch, status, fetchStatus } = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(50).then(() => pageParam), initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, }) return (
    data: {JSON.stringify(data)}
    status: {status}, {fetchStatus}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('status: success, idle')).toBeInTheDocument() expect( rendered.getByText('data: {"pages":[10],"pageParams":[10]}'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('status: success, fetching')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('status: success, idle')).toBeInTheDocument() expect( rendered.getByText('data: {"pages":[10,11],"pageParams":[10,11]}'), ).toBeInTheDocument() }) it('should silently cancel an ongoing fetchNextPage request when another fetchNextPage is invoked', async () => { const key = queryKey() const start = 10 const onAborts: Array) => any>> = [] const abortListeners: Array) => any>> = [] const fetchPage = vi.fn< (context: QueryFunctionContext) => Promise >(async ({ pageParam, signal }) => { const onAbort = vi.fn() const abortListener = vi.fn() onAborts.push(onAbort) abortListeners.push(abortListener) signal.onabort = onAbort signal.addEventListener('abort', abortListener) await sleep(50) return pageParam }) function Page() { const { fetchNextPage } = useInfiniteQuery({ queryKey: key, queryFn: fetchPage, initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, }) React.useEffect(() => { setActTimeout(() => { fetchNextPage() }, 100) setActTimeout(() => { fetchNextPage() }, 110) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(160) const expectedCallCount = 3 expect(fetchPage).toBeCalledTimes(expectedCallCount) expect(onAborts).toHaveLength(expectedCallCount) expect(abortListeners).toHaveLength(expectedCallCount) let callIndex = 0 const firstCtx = fetchPage.mock.calls[callIndex]![0] expect(firstCtx.pageParam).toEqual(start) expect(firstCtx.queryKey).toEqual(key) expect(firstCtx.signal).toBeInstanceOf(AbortSignal) expect(firstCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() callIndex = 1 const secondCtx = fetchPage.mock.calls[callIndex]![0] expect(secondCtx.pageParam).toBe(11) expect(secondCtx.queryKey).toEqual(key) expect(secondCtx.signal).toBeInstanceOf(AbortSignal) expect(secondCtx.signal.aborted).toBe(true) expect(onAborts[callIndex]).toHaveBeenCalledTimes(1) expect(abortListeners[callIndex]).toHaveBeenCalledTimes(1) callIndex = 2 const thirdCtx = fetchPage.mock.calls[callIndex]![0] expect(thirdCtx.pageParam).toBe(11) expect(thirdCtx.queryKey).toEqual(key) expect(thirdCtx.signal).toBeInstanceOf(AbortSignal) expect(thirdCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() }) it('should not cancel an ongoing fetchNextPage request when another fetchNextPage is invoked if `cancelRefetch: false` is used', async () => { const key = queryKey() const start = 10 const onAborts: Array) => any>> = [] const abortListeners: Array) => any>> = [] const fetchPage = vi.fn< (context: QueryFunctionContext) => Promise >(async ({ pageParam, signal }) => { const onAbort = vi.fn() const abortListener = vi.fn() onAborts.push(onAbort) abortListeners.push(abortListener) signal.onabort = onAbort signal.addEventListener('abort', abortListener) await sleep(50) return pageParam }) function Page() { const { fetchNextPage } = useInfiniteQuery({ queryKey: key, queryFn: fetchPage, initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, }) React.useEffect(() => { setActTimeout(() => { fetchNextPage() }, 100) setActTimeout(() => { fetchNextPage({ cancelRefetch: false }) }, 110) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(160) const expectedCallCount = 2 expect(fetchPage).toBeCalledTimes(expectedCallCount) expect(onAborts).toHaveLength(expectedCallCount) expect(abortListeners).toHaveLength(expectedCallCount) let callIndex = 0 const firstCtx = fetchPage.mock.calls[callIndex]![0] expect(firstCtx.pageParam).toEqual(start) expect(firstCtx.queryKey).toEqual(key) expect(firstCtx.signal).toBeInstanceOf(AbortSignal) expect(firstCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() callIndex = 1 const secondCtx = fetchPage.mock.calls[callIndex]![0] expect(secondCtx.pageParam).toBe(11) expect(secondCtx.queryKey).toEqual(key) expect(secondCtx.signal).toBeInstanceOf(AbortSignal) expect(secondCtx.signal.aborted).toBe(false) expect(onAborts[callIndex]).not.toHaveBeenCalled() expect(abortListeners[callIndex]).not.toHaveBeenCalled() }) it('should keep fetching first page when not loaded yet and triggering fetch more', async () => { const key = queryKey() const states: Array>> = [] function Page() { const start = 10 const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(50).then(() => pageParam), initialPageParam: start, getNextPageParam: (lastPage) => lastPage + 1, notifyOnChangeProps: 'all', }) states.push(state) const { fetchNextPage } = state React.useEffect(() => { setActTimeout(() => { fetchNextPage() }, 10) }, [fetchNextPage]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(60) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ hasNextPage: false, data: undefined, isFetching: true, isFetchingNextPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ hasNextPage: true, data: { pages: [10] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should stop fetching additional pages when the component is unmounted and AbortSignal is consumed', async () => { const key = queryKey() let fetches = 0 const initialData = { pages: [1, 2, 3, 4], pageParams: [0, 1, 2, 3] } function List() { useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(50).then(() => { fetches++ return pageParam * 10 }), initialData, initialPageParam: 0, getNextPageParam: (_, allPages) => { return allPages.length === 4 ? undefined : allPages.length }, }) return null } function Page() { const [show, setShow] = React.useState(true) React.useEffect(() => { setActTimeout(() => { setShow(false) }, 75) }, []) return show ? : null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(125) expect(fetches).toBe(2) expect(queryClient.getQueryState(key)).toMatchObject({ data: initialData, status: 'success', error: null, }) }) it('should be able to set new pages with the query client', async () => { const key = queryKey() let multiplier = 1 function Page() { const [firstPage, setFirstPage] = React.useState(0) const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => multiplier * pageParam), getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: firstPage, }) return (
    data: {JSON.stringify(state.data)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: {"pages":[0],"pageParams":[0]}'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setPages/i })) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: {"pages":[7,8],"pageParams":[7,8]}'), ).toBeInTheDocument() multiplier = 2 fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: {"pages":[14,30],"pageParams":[7,15]}'), ).toBeInTheDocument() }) it('should only refetch the first page when initialData is provided', async () => { vi.useRealTimers() const key = queryKey() const renderStream = createRenderStream>>() function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialData: { pages: [1], pageParams: [1] }, getNextPageParam: (lastPage) => lastPage + 1, initialPageParam: 0, notifyOnChangeProps: 'all', }) renderStream.replaceSnapshot(state) return ( ) } const rendered = await renderStream.render( , ) { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1] }, hasNextPage: true, isFetching: true, isFetchingNextPage: false, isSuccess: true, }) } { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) } fireEvent.click(rendered.getByText('fetchNextPage')) { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1] }, hasNextPage: true, isFetching: true, isFetchingNextPage: true, isSuccess: true, }) } { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: { pages: [1, 2] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) } }) it('should set hasNextPage to false if getNextPageParam returns undefined', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), getNextPageParam: () => undefined, initialPageParam: 1, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, hasNextPage: false, isFetching: true, isFetchingNextPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: [1] }, hasNextPage: false, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should compute hasNextPage correctly using initialData', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialData: { pages: [10], pageParams: [10] }, getNextPageParam: (lastPage) => (lastPage === 10 ? 11 : undefined), initialPageParam: 10, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, isFetching: true, isFetchingNextPage: false, isSuccess: true, }) expect(states[1]).toMatchObject({ data: { pages: [10] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should compute hasNextPage correctly for falsy getFetchMore return value using initialData', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), initialPageParam: 10, initialData: { pages: [10], pageParams: [10] }, getNextPageParam: () => undefined, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: { pages: [10] }, hasNextPage: false, isFetching: true, isFetchingNextPage: false, isSuccess: true, }) expect(states[1]).toMatchObject({ data: { pages: [10] }, hasNextPage: false, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should not use selected data when computing hasNextPage', async () => { const key = queryKey() const states: Array>> = [] function Page() { const state = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => sleep(10).then(() => pageParam), getNextPageParam: (lastPage) => (lastPage === 1 ? 2 : undefined), select: (data) => ({ pages: data.pages.map((x) => x.toString()), pageParams: data.pageParams, }), initialPageParam: 1, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, hasNextPage: false, isFetching: true, isFetchingNextPage: false, isSuccess: false, }) expect(states[1]).toMatchObject({ data: { pages: ['1'] }, hasNextPage: true, isFetching: false, isFetchingNextPage: false, isSuccess: true, }) }) it('should build fresh cursors on refetch', async () => { const key = queryKey() const genItems = (size: number) => [...new Array(size)].fill(null).map((_, d) => d) const items = genItems(15) const limit = 3 const fetchItemsWithLimit = (cursor = 0, ts: number) => sleep(10).then(() => ({ nextId: cursor + limit, items: items.slice(cursor, cursor + limit), ts, })) function Page() { const fetchCountRef = React.useRef(0) const { status, data, error, isFetchingNextPage, fetchNextPage, hasNextPage, refetch, } = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => fetchItemsWithLimit(pageParam, fetchCountRef.current++), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, }) return (

    Pagination

    {status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    Data:
    {data.pages.map((page, i) => (
    Page {i}: {page.ts}
    {page.items.map((item) => (

    Item: {item}

    ))}
    ))}
    {!isFetchingNextPage ? 'Background Updating...' : null}
    )}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 2')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 5')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 8')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() expect(rendered.getByText('Page 2: 2')).toBeInTheDocument() fireEvent.click(rendered.getByText('Refetch')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Background Updating...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(31) expect(rendered.getByText('Item: 8')).toBeInTheDocument() expect(rendered.getByText('Page 0: 3')).toBeInTheDocument() expect(rendered.getByText('Page 1: 4')).toBeInTheDocument() expect(rendered.getByText('Page 2: 5')).toBeInTheDocument() // ensure that Item: 4 is rendered before removing it expect(rendered.queryAllByText('Item: 4')).toHaveLength(1) // remove Item: 4 fireEvent.click(rendered.getByText('Remove item')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Background Updating...')).toBeInTheDocument() // ensure that an additional item is rendered (it means that cursors were properly rebuilt) await vi.advanceTimersByTimeAsync(31) expect(rendered.getByText('Item: 9')).toBeInTheDocument() expect(rendered.getByText('Page 0: 6')).toBeInTheDocument() expect(rendered.getByText('Page 1: 7')).toBeInTheDocument() expect(rendered.getByText('Page 2: 8')).toBeInTheDocument() // ensure that Item: 4 is no longer rendered expect(rendered.queryAllByText('Item: 4')).toHaveLength(0) }) it('should compute hasNextPage correctly for falsy getFetchMore return value on refetching', async () => { const key = queryKey() const MAX = 2 function Page() { const fetchCountRef = React.useRef(0) const [isRemovedLastPage, setIsRemovedLastPage] = React.useState(false) const { status, data, error, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, refetch, } = useInfiniteQuery({ queryKey: key, queryFn: ({ pageParam }) => fetchItems( pageParam, fetchCountRef.current++, pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage), ), getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, }) return (

    Pagination

    {status === 'pending' ? ( 'Loading...' ) : status === 'error' ? ( Error: {error.message} ) : ( <>
    Data:
    {data.pages.map((page, i) => (
    Page {i}: {page.ts}
    {page.items.map((item) => (

    Item: {item}

    ))}
    ))}
    {isFetching && !isFetchingNextPage ? 'Background Updating...' : null}
    )}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 9')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 19')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() fireEvent.click(rendered.getByText('Load More')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Loading more...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Item: 29')).toBeInTheDocument() expect(rendered.getByText('Page 0: 0')).toBeInTheDocument() expect(rendered.getByText('Page 1: 1')).toBeInTheDocument() expect(rendered.getByText('Page 2: 2')).toBeInTheDocument() expect(rendered.getByText('Nothing more to load')).toBeInTheDocument() fireEvent.click(rendered.getByText('Remove Last Page')) fireEvent.click(rendered.getByText('Refetch')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Background Updating...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.queryByText('Item: 29')).not.toBeInTheDocument() expect(rendered.getByText('Page 0: 3')).toBeInTheDocument() expect(rendered.getByText('Page 1: 4')).toBeInTheDocument() expect(rendered.queryByText('Page 2: 5')).not.toBeInTheDocument() expect(rendered.getByText('Nothing more to load')).toBeInTheDocument() }) it('should cancel the query function when there are no more subscriptions', () => { const key = queryKey() let cancelFn: Mock = vi.fn() const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = vi.fn(() => reject('Cancelled')) signal?.addEventListener('abort', cancelFn) sleep(1000).then(() => resolve('OK')) }) return promise } function Inner() { const state = useInfiniteQuery({ queryKey: key, queryFn, getNextPageParam: () => undefined, initialPageParam: 0, }) return (

    Status: {state.status}

    ) } function Page() { const [isVisible, setIsVisible] = React.useState(true) return ( <> {isVisible && }
    {isVisible ? 'visible' : 'hidden'}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('visible')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: 'hide' })) expect(rendered.getByText('hidden')).toBeInTheDocument() expect(cancelFn).toHaveBeenCalled() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = () => sleep(10).then(() => 'custom client') function Page() { const { data } = useInfiniteQuery( { queryKey: key, queryFn, getNextPageParam: () => undefined, initialPageParam: 0, }, queryClient, ) return
    data: {data?.pages[0]}
    } const rendered = render() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) it('should work with React.use()', async () => { vi.useRealTimers() const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function Loading() { useTrackRenders() return <>loading... } function MyComponent() { useTrackRenders() const fetchCountRef = React.useRef(0) const query = useInfiniteQuery({ queryFn: ({ pageParam }) => fetchItems(pageParam, fetchCountRef.current++), getNextPageParam: (lastPage) => lastPage.nextId, initialPageParam: 0, queryKey: key, }) const data = React.use(query.promise) return ( <> {data.pages.map((page, index) => (
    Page: {index + 1}
    {page.items.map((item) => (

    Item: {item}

    ))}
    ))} ) } function Page() { useTrackRenders() return ( }> ) } const rendered = await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading...') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('Page: 1') withinDOM().getByText('Item: 1') expect(renderedComponents).toEqual([MyComponent]) } // click button rendered.getByRole('button', { name: 'fetchNextPage' }).click() { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('Page: 1') expect(renderedComponents).toEqual([MyComponent]) } }) }) ================================================ FILE: packages/react-query/src/__tests__/useIsFetching.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, useIsFetching, useQuery } from '..' import { renderWithClient, setActTimeout } from './utils' describe('useIsFetching', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) // See https://github.com/tannerlinsley/react-query/issues/105 it('should update as queries start and stop fetching', async () => { const queryClient = new QueryClient() const key = queryKey() function IsFetching() { const isFetching = useIsFetching() return
    isFetching: {isFetching}
    } function Query() { const [ready, setReady] = React.useState(false) useQuery({ queryKey: key, queryFn: () => sleep(50).then(() => 'test'), enabled: ready, }) return } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setReady/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) it('should not update state while rendering', async () => { const queryClient = new QueryClient() const key1 = queryKey() const key2 = queryKey() const isFetchingArray: Array = [] function IsFetching() { const isFetching = useIsFetching() isFetchingArray.push(isFetching) return null } function FirstQuery() { useQuery({ queryKey: key1, queryFn: () => sleep(100).then(() => 'data1'), }) return null } function SecondQuery() { useQuery({ queryKey: key2, queryFn: () => sleep(100).then(() => 'data2'), }) return null } function Page() { const [renderSecond, setRenderSecond] = React.useState(false) React.useEffect(() => { setActTimeout(() => { setRenderSecond(true) }, 50) }, []) return ( <> {renderSecond && } ) } renderWithClient(queryClient, ) expect(isFetchingArray[0]).toEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isFetchingArray[1]).toEqual(1) await vi.advanceTimersByTimeAsync(50) expect(isFetchingArray[2]).toEqual(1) await vi.advanceTimersByTimeAsync(1) expect(isFetchingArray[3]).toEqual(2) await vi.advanceTimersByTimeAsync(50) expect(isFetchingArray[4]).toEqual(1) await vi.advanceTimersByTimeAsync(50) expect(isFetchingArray[5]).toEqual(0) expect(isFetchingArray).toEqual([0, 1, 1, 2, 1, 0]) }) it('should be able to filter', async () => { const queryClient = new QueryClient() const key1 = queryKey() const key2 = queryKey() const isFetchingArray: Array = [] function One() { useQuery({ queryKey: key1, queryFn: () => sleep(10).then(() => 'test1'), }) return null } function Two() { useQuery({ queryKey: key2, queryFn: () => sleep(20).then(() => 'test2'), }) return null } function Page() { const [started, setStarted] = React.useState(false) const isFetching = useIsFetching({ queryKey: key1 }) isFetchingArray.push(isFetching) return (
    isFetching: {isFetching}
    {started ? ( <> ) : null}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setStarted/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) 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 queryClient = new QueryClient() const key = queryKey() function Page() { useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) const isFetching = useIsFetching() return (
    isFetching: {isFetching}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) it('should use provided custom queryClient', async () => { const onSuccess = vi.fn() const queryCache = new QueryCache({ onSuccess }) const queryClient = new QueryClient({ queryCache }) const key = queryKey() function Page() { useQuery( { queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }, queryClient, ) const isFetching = useIsFetching({}, queryClient) return (
    isFetching: {isFetching}
    ) } const rendered = render() expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() expect(onSuccess).toHaveBeenCalledOnce() }) }) ================================================ FILE: packages/react-query/src/__tests__/useMutation.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { MutationCache, QueryCache, QueryClient, useMutation } from '..' import { mockOnlineManagerIsOnline, renderWithClient, setActTimeout, } from './utils' import type { UseMutationResult } from '../types' describe('useMutation', () => { let queryCache: QueryCache let mutationCache: MutationCache let queryClient: QueryClient beforeEach(() => { queryCache = new QueryCache() mutationCache = new MutationCache() queryClient = new QueryClient({ queryCache, mutationCache, }) vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should be able to reset `data`', async () => { function Page() { const { mutate, data = 'empty', reset, } = useMutation({ mutationFn: () => Promise.resolve('mutation') }) return (

    {data}

    ) } const { getByRole } = renderWithClient(queryClient, ) expect(getByRole('heading').textContent).toBe('empty') fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('mutation') fireEvent.click(getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('empty') }) it('should be able to reset `error`', async () => { function Page() { const { mutate, error, reset } = useMutation({ mutationFn: () => { const err = new Error('Expected mock error. All is well!') err.stack = '' return Promise.reject(err) }, }) return (
    {error &&

    {error.message}

    }
    ) } const { getByRole, queryByRole } = renderWithClient(queryClient, ) expect(queryByRole('heading')).toBeNull() fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe( 'Expected mock error. All is well!', ) fireEvent.click(getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(0) expect(queryByRole('heading')).toBeNull() }) it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => { let count = 0 const onSuccessMock = vi.fn() const onSettledMock = vi.fn() function Page() { const { mutate } = useMutation({ mutationFn: (vars: { count: number }) => Promise.resolve(vars.count), onSuccess: (data) => { onSuccessMock(data) }, onSettled: (data) => { onSettledMock(data) }, }) return (

    {count}

    ) } const { getByRole } = renderWithClient(queryClient, ) expect(getByRole('heading').textContent).toBe('0') fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('3') expect(onSuccessMock).toHaveBeenCalledTimes(3) expect(onSuccessMock).toHaveBeenCalledWith(1) expect(onSuccessMock).toHaveBeenCalledWith(2) expect(onSuccessMock).toHaveBeenCalledWith(3) expect(onSettledMock).toHaveBeenCalledTimes(3) expect(onSettledMock).toHaveBeenCalledWith(1) expect(onSettledMock).toHaveBeenCalledWith(2) expect(onSettledMock).toHaveBeenCalledWith(3) }) it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { let count = 0 type Value = { count: number } const mutateFn = vi.fn<(value: Value) => Promise>() mutateFn.mockImplementationOnce(() => { return Promise.reject(new Error('Error test Jonas')) }) mutateFn.mockImplementation(async (value) => { await sleep(10) return Promise.resolve(value) }) function Page() { const { mutate, failureCount, failureReason, data, status } = useMutation( { mutationFn: mutateFn }, ) return (

    Data {data?.count}

    Status {status}

    Failed {failureCount} times

    Failed because {failureReason?.message ?? 'null'}

    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Data')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Status error')).toBeInTheDocument() expect(rendered.getByText('Failed 1 times')).toBeInTheDocument() expect( rendered.getByText('Failed because Error test Jonas'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Status pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status success')).toBeInTheDocument() expect(rendered.getByText('Data 2')).toBeInTheDocument() expect(rendered.getByText('Failed 0 times')).toBeInTheDocument() expect(rendered.getByText('Failed because null')).toBeInTheDocument() }) it('should be able to call `onError` and `onSettled` after each failed mutate', async () => { const onErrorMock = vi.fn() const onSettledMock = vi.fn() let count = 0 function Page() { const { mutate } = useMutation({ mutationFn: (vars: { count: number }) => { const error = new Error( `Expected mock error. All is well! ${vars.count}`, ) error.stack = '' return Promise.reject(error) }, onError: (error: Error) => { onErrorMock(error.message) }, onSettled: (_data, error) => { onSettledMock(error?.message) }, }) return (

    {count}

    ) } const { getByRole } = renderWithClient(queryClient, ) expect(getByRole('heading').textContent).toBe('0') fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) fireEvent.click(getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(getByRole('heading').textContent).toBe('3') expect(onErrorMock).toHaveBeenCalledTimes(3) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 1', ) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 2', ) expect(onErrorMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 3', ) expect(onSettledMock).toHaveBeenCalledTimes(3) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 1', ) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 2', ) expect(onSettledMock).toHaveBeenCalledWith( 'Expected mock error. All is well! 3', ) }) it('should be able to override the useMutation success callbacks', async () => { const callbacks: Array = [] function Page() { const { mutateAsync } = useMutation({ mutationFn: (text: string) => Promise.resolve(text), onSuccess: () => { callbacks.push('useMutation.onSuccess') return Promise.resolve() }, onSettled: () => { callbacks.push('useMutation.onSettled') return Promise.resolve() }, }) React.useEffect(() => { setActTimeout(async () => { try { const result = await mutateAsync('todo', { onSuccess: () => { callbacks.push('mutateAsync.onSuccess') return Promise.resolve() }, onSettled: () => { callbacks.push('mutateAsync.onSettled') return Promise.resolve() }, }) callbacks.push(`mutateAsync.result:${result}`) } catch {} }, 10) }, [mutateAsync]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) expect(callbacks).toEqual([ 'useMutation.onSuccess', 'useMutation.onSettled', 'mutateAsync.onSuccess', 'mutateAsync.onSettled', 'mutateAsync.result:todo', ]) }) it('should be able to override the error callbacks when using mutateAsync', async () => { const callbacks: Array = [] function Page() { const { mutateAsync } = useMutation({ mutationFn: async (_text: string) => Promise.reject(new Error('oops')), onError: () => { callbacks.push('useMutation.onError') return Promise.resolve() }, onSettled: () => { callbacks.push('useMutation.onSettled') return Promise.resolve() }, }) React.useEffect(() => { setActTimeout(async () => { try { await mutateAsync('todo', { onError: () => { callbacks.push('mutateAsync.onError') return Promise.resolve() }, onSettled: () => { callbacks.push('mutateAsync.onSettled') return Promise.resolve() }, }) } catch (error) { callbacks.push(`mutateAsync.error:${(error as Error).message}`) } }, 10) }, [mutateAsync]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) expect(callbacks).toEqual([ 'useMutation.onError', 'useMutation.onSettled', 'mutateAsync.onError', 'mutateAsync.onSettled', 'mutateAsync.error:oops', ]) }) it('should be able to use mutation defaults', async () => { const key = queryKey() queryClient.setMutationDefaults(key, { mutationFn: async (text: string) => { await sleep(10) return text }, }) const states: Array> = [] function Page() { const state = useMutation({ mutationKey: key }) states.push(state) const { mutate } = state React.useEffect(() => { setActTimeout(() => { mutate('todo') }, 10) }, [mutate]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(21) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: undefined, isPending: false }) expect(states[1]).toMatchObject({ data: undefined, isPending: true }) expect(states[2]).toMatchObject({ data: 'todo', isPending: false }) }) it('should be able to retry a failed mutation', async () => { let count = 0 function Page() { const { mutate } = useMutation({ mutationFn: (_text: string) => { count++ return Promise.reject(new Error('oops')) }, retry: 1, retryDelay: 5, }) React.useEffect(() => { setActTimeout(() => { mutate('todo') }, 10) }, [mutate]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) expect(count).toBe(2) }) it('should not retry mutations while offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) let count = 0 function Page() { const mutation = useMutation({ mutationFn: (_text: string) => { count++ return Promise.reject(new Error('oops')) }, retry: 1, retryDelay: 5, }) return (
    error:{' '} {mutation.error instanceof Error ? mutation.error.message : 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
    ) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('error: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('error: null, status: pending, isPaused: true'), ).toBeInTheDocument() expect(count).toBe(0) onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(6) expect( rendered.getByText('error: oops, status: error, isPaused: false'), ).toBeInTheDocument() expect(count).toBe(2) onlineMock.mockRestore() }) it('should call onMutate even if paused', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const onMutate = vi.fn() let count = 0 function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { count++ await sleep(10) return count }, onMutate, }) return (
    data: {mutation.data ?? 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
    ) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('data: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: null, status: pending, isPaused: true'), ).toBeInTheDocument() expect(onMutate).toHaveBeenCalledTimes(1) expect(onMutate).toHaveBeenCalledWith('todo', { client: queryClient, meta: undefined, mutationKey: undefined, }) onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 1, status: success, isPaused: false'), ).toBeInTheDocument() expect(onMutate).toHaveBeenCalledTimes(1) expect(count).toBe(1) onlineMock.mockRestore() }) it('should optimistically go to paused state if offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) let count = 0 const states: Array = [] function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { count++ await sleep(10) return count }, }) states.push(`${mutation.status}, ${mutation.isPaused}`) return (
    data: {mutation.data ?? 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
    ) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('data: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: null, status: pending, isPaused: true'), ).toBeInTheDocument() // no intermediate 'pending, false' state is expected because we don't start mutating! expect(states[0]).toBe('idle, false') expect(states[1]).toBe('pending, true') onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 1, status: success, isPaused: false'), ).toBeInTheDocument() onlineMock.mockRestore() }) it('should be able to retry a mutation when online', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useMutation({ mutationKey: key, mutationFn: async (_text: string) => { await sleep(10) count++ return count > 1 ? Promise.resolve(`data${count}`) : Promise.reject(new Error('oops')) }, retry: 1, retryDelay: 5, networkMode: 'offlineFirst', }) return (
    status: {state.status}
    isPaused: {String(state.isPaused)}
    data: {state.data ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: idle')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(16) expect(rendered.getByText('isPaused: true')).toBeInTheDocument() expect( queryClient.getMutationCache().findAll({ mutationKey: key }).length, ).toBe(1) expect( queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state, ).toMatchObject({ status: 'pending', isPaused: true, failureCount: 1, failureReason: new Error('oops'), }) onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: data2')).toBeInTheDocument() expect( queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state, ).toMatchObject({ status: 'success', isPaused: false, failureCount: 0, failureReason: null, data: 'data2', }) onlineMock.mockRestore() }) // eslint-disable-next-line vitest/expect-expect it('should not change state if unmounted', () => { function Mutates() { const { mutate } = useMutation({ mutationFn: () => sleep(10) }) return } function Page() { const [mounted, setMounted] = React.useState(true) return (
    {mounted && }
    ) } const { getByText } = renderWithClient(queryClient, ) fireEvent.click(getByText('mutate')) fireEvent.click(getByText('unmount')) }) it('should be able to throw an error when throwOnError is set to true', async () => { const err = new Error('Expected mock error. All is well!') err.stack = '' const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) function Page() { const { mutate } = useMutation({ mutationFn: () => { return Promise.reject(err) }, throwOnError: true, }) return (
    ) } const { getByText, queryByText } = renderWithClient( queryClient, (
    error
    )} >
    , ) fireEvent.click(getByText('mutate')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('error')).not.toBeNull() expect(consoleMock.mock.calls[0]?.[1]).toBe(err) consoleMock.mockRestore() }) it('should be able to throw an error when throwOnError is a function that returns true', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let boundary = false function Page() { const { mutate, error } = useMutation({ mutationFn: () => { const err = new Error('mock error') err.stack = '' return Promise.reject(err) }, throwOnError: () => { return boundary }, }) return (
    {error && error.message}
    ) } const { getByText, queryByText } = renderWithClient( queryClient, (
    error boundary
    )} >
    , ) // first error goes to component fireEvent.click(getByText('mutate')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('mock error')).not.toBeNull() // second error goes to boundary boundary = true fireEvent.click(getByText('mutate')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('error boundary')).not.toBeNull() consoleMock.mockRestore() }) it('should pass meta to mutation', async () => { const errorMock = vi.fn() const successMock = vi.fn() const queryClientMutationMeta = new QueryClient({ mutationCache: new MutationCache({ onSuccess: (_, __, ___, mutation) => { successMock(mutation.meta?.metaSuccessMessage) }, onError: (_, __, ___, mutation) => { errorMock(mutation.meta?.metaErrorMessage) }, }), }) const metaSuccessMessage = 'mutation succeeded' const metaErrorMessage = 'mutation failed' function Page() { const { mutate: succeed, isSuccess } = useMutation({ mutationFn: () => Promise.resolve(''), meta: { metaSuccessMessage }, }) const { mutate: error, isError } = useMutation({ mutationFn: () => { return Promise.reject(new Error('')) }, meta: { metaErrorMessage }, }) return (
    {isSuccess &&
    successTest
    } {isError &&
    errorTest
    }
    ) } const { getByText, queryByText } = renderWithClient( queryClientMutationMeta, , ) fireEvent.click(getByText('succeed')) fireEvent.click(getByText('error')) await vi.advanceTimersByTimeAsync(0) expect(queryByText('successTest')).not.toBeNull() expect(queryByText('errorTest')).not.toBeNull() expect(successMock).toHaveBeenCalledTimes(1) expect(successMock).toHaveBeenCalledWith(metaSuccessMessage) expect(errorMock).toHaveBeenCalledTimes(1) expect(errorMock).toHaveBeenCalledWith(metaErrorMessage) }) it('should call cache callbacks when unmounted', async () => { const onSuccess = vi.fn() const onSuccessMutate = vi.fn() const onSettled = vi.fn() const onSettledMutate = vi.fn() const mutationKey = queryKey() let count = 0 function Page() { const [show, setShow] = React.useState(true) return (
    {show && }
    ) } function Component() { const mutation = useMutation({ mutationFn: async (_text: string) => { count++ await sleep(10) return count }, mutationKey, gcTime: 0, onSuccess, onSettled, }) return (
    data: {mutation.data ?? 'null'}, status: {mutation.status}, isPaused: {String(mutation.isPaused)}
    ) } const rendered = renderWithClient(queryClient, ) expect( rendered.getByText('data: null, status: idle, isPaused: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await vi.advanceTimersByTimeAsync(10) expect( queryClient.getMutationCache().findAll({ mutationKey }), ).toHaveLength(0) expect(count).toBe(1) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSuccessMutate).toHaveBeenCalledTimes(0) expect(onSettledMutate).toHaveBeenCalledTimes(0) }) it('should call mutate callbacks only for the last observer', async () => { const onSuccess = vi.fn() const onSuccessMutate = vi.fn() const onSettled = vi.fn() const onSettledMutate = vi.fn() let count = 0 function Page() { const mutation = useMutation({ mutationFn: async (text: string) => { count++ const result = `result-${text}` await sleep(10) return result }, onSuccess, onSettled, }) return (
    data: {mutation.data ?? 'null'}, status: {mutation.status}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: result-todo2, status: success'), ).toBeInTheDocument() expect(count).toBe(2) expect(onSuccess).toHaveBeenCalledTimes(2) expect(onSuccess).toHaveBeenNthCalledWith( 1, 'result-todo1', 'todo1', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) expect(onSuccess).toHaveBeenNthCalledWith( 2, 'result-todo2', 'todo2', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) expect(onSettled).toHaveBeenCalledTimes(2) expect(onSuccessMutate).toHaveBeenCalledTimes(1) expect(onSuccessMutate).toHaveBeenCalledWith( 'result-todo2', 'todo2', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) expect(onSettledMutate).toHaveBeenCalledTimes(1) expect(onSettledMutate).toHaveBeenCalledWith( 'result-todo2', null, 'todo2', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }, ) }) it('should go to error state if onSuccess callback errors', async () => { const error = new Error('error from onSuccess') const onError = vi.fn() function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { await sleep(10) return 'result' }, onSuccess: () => Promise.reject(error), onError, }) return (
    status: {mutation.status}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: idle')).toBeInTheDocument() rendered.getByRole('button', { name: /mutate/i }).click() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: error')).toBeInTheDocument() expect(onError).toHaveBeenCalledWith(error, 'todo', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }) }) it('should go to error state if onError callback errors', async ({ onTestFinished, }) => { const unhandledRejectionFn = vi.fn() process.on('unhandledRejection', (error) => unhandledRejectionFn(error)) onTestFinished(() => { process.off('unhandledRejection', unhandledRejectionFn) }) const error = new Error('error from onError') const mutateFnError = new Error('mutateFnError') function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { await sleep(10) throw mutateFnError }, onError: () => Promise.reject(error), }) return (
    error:{' '} {mutation.error instanceof Error ? mutation.error.message : 'null'}, status: {mutation.status}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('error: null, status: idle')).toBeInTheDocument() rendered.getByRole('button', { name: /mutate/i }).click() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('error: mutateFnError, status: error'), ).toBeInTheDocument() }) it('should go to error state if onSettled callback errors', async ({ onTestFinished, }) => { const unhandledRejectionFn = vi.fn() process.on('unhandledRejection', (error) => unhandledRejectionFn(error)) onTestFinished(() => { process.off('unhandledRejection', unhandledRejectionFn) }) const error = new Error('error from onSettled') const mutateFnError = new Error('mutateFnError') const onError = vi.fn() function Page() { const mutation = useMutation({ mutationFn: async (_text: string) => { await sleep(10) throw mutateFnError }, onSettled: () => Promise.reject(error), onError, }) return (
    error:{' '} {mutation.error instanceof Error ? mutation.error.message : 'null'}, status: {mutation.status}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('error: null, status: idle')).toBeInTheDocument() rendered.getByRole('button', { name: /mutate/i }).click() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('error: mutateFnError, status: error'), ).toBeInTheDocument() expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined, { client: queryClient, meta: undefined, mutationKey: undefined, }) }) it('should use provided custom queryClient', async () => { function Page() { const mutation = useMutation( { mutationFn: async (text: string) => { return Promise.resolve(text) }, }, queryClient, ) return (
    data: {mutation.data ?? 'null'}, status: {mutation.status}
    ) } const rendered = render() expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: custom client, status: success'), ).toBeInTheDocument() }) }) ================================================ FILE: packages/react-query/src/__tests__/useMutationState.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { useMutationState } from '../useMutationState' import type { MutationState, MutationStatus } from '@tanstack/query-core' describe('useMutationState', () => { it('should default to QueryState', () => { const result = useMutationState({ filters: { status: 'pending' }, }) expectTypeOf(result).toEqualTypeOf< Array> >() }) it('should infer with select', () => { const result = useMutationState({ filters: { status: 'pending' }, select: (mutation) => mutation.state.status, }) expectTypeOf(result).toEqualTypeOf>() }) }) ================================================ FILE: packages/react-query/src/__tests__/useMutationState.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, useIsMutating, useMutation, useMutationState } from '..' import { renderWithClient } from './utils' describe('useIsMutating', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return the number of fetching mutations', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() function IsMutating() { const isMutating = useIsMutating() isMutatingArray.push(isMutating) return null } function Mutations() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], mutationFn: () => sleep(50).then(() => 'data'), }) const { mutate: mutate2 } = useMutation({ mutationKey: ['mutation2'], mutationFn: () => sleep(10).then(() => 'data'), }) return (
    ) } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) await vi.advanceTimersByTimeAsync(10) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) // we don't really care if this yields // [ +0, 1, 2, +0 ] // or // [ +0, 1, 2, 1, +0 ] // our batching strategy might yield different results await vi.advanceTimersByTimeAsync(41) expect(isMutatingArray[0]).toEqual(0) expect(isMutatingArray[1]).toEqual(1) expect(isMutatingArray[2]).toEqual(2) expect(isMutatingArray[3]).toEqual(1) expect(isMutatingArray[4]).toEqual(0) expect(isMutatingArray).toEqual([0, 1, 2, 1, 0]) }) it('should filter correctly by mutationKey', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() function IsMutating() { const isMutating = useIsMutating({ mutationKey: ['mutation1'] }) isMutatingArray.push(isMutating) return null } function Page() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], mutationFn: () => sleep(100).then(() => 'data'), }) const { mutate: mutate2 } = useMutation({ mutationKey: ['mutation2'], mutationFn: () => sleep(100).then(() => 'data'), }) React.useEffect(() => { mutate1() mutate2() }, [mutate1, mutate2]) return } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(101) expect(isMutatingArray).toEqual([0, 1, 0]) }) it('should filter correctly by predicate', async () => { const isMutatingArray: Array = [] const queryClient = new QueryClient() function IsMutating() { const isMutating = useIsMutating({ predicate: (mutation) => mutation.options.mutationKey?.[0] === 'mutation1', }) isMutatingArray.push(isMutating) return null } function Page() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], mutationFn: () => sleep(100).then(() => 'data'), }) const { mutate: mutate2 } = useMutation({ mutationKey: ['mutation2'], mutationFn: () => sleep(100).then(() => 'data'), }) React.useEffect(() => { mutate1() mutate2() }, [mutate1, mutate2]) return } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(101) expect(isMutatingArray).toEqual([0, 1, 0]) }) it('should use provided custom queryClient', async () => { const queryClient = new QueryClient() function Page() { const isMutating = useIsMutating({}, queryClient) const { mutate } = useMutation( { mutationKey: ['mutation1'], mutationFn: () => sleep(10).then(() => 'data'), }, queryClient, ) React.useEffect(() => { mutate() }, [mutate]) return (
    mutating: {isMutating}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('mutating: 1')).toBeInTheDocument() }) }) describe('useMutationState', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return variables after calling mutate', async () => { const queryClient = new QueryClient() const variables: Array> = [] const mutationKey = ['mutation'] function Variables() { variables.push( useMutationState({ filters: { mutationKey, status: 'pending' }, select: (mutation) => mutation.state.variables, }), ) return null } function Mutate() { const { mutate, data } = useMutation({ mutationKey, mutationFn: (input: number) => sleep(150).then(() => 'data' + input), }) return (
    data: {data ?? 'null'}
    ) } function Page() { return (
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: null')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(151) expect(rendered.getByText('data: data1')).toBeInTheDocument() expect(variables).toEqual([[], [1], []]) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { usePrefetchInfiniteQuery } from '..' describe('usePrefetchInfiniteQuery', () => { it('should return nothing', () => { const result = usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(result).toEqualTypeOf() }) it('should require initialPageParam and getNextPageParam', () => { assertType( // @ts-expect-error TS2345 usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }), ) }) it('should not allow refetchInterval, enabled or throwOnError options', () => { assertType( usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2353 refetchInterval: 1000, }), ) assertType( usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2353 enabled: true, }), ) assertType( usePrefetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2353 throwOnError: true, }), ) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchInfiniteQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import React from 'react' import { act, fireEvent } from '@testing-library/react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, usePrefetchInfiniteQuery, useSuspenseInfiniteQuery, } from '..' import { renderWithClient } from './utils' import type { InfiniteData, UseSuspenseInfiniteQueryOptions } from '..' import type { Mock } from 'vitest' const createFallback = () => vi.fn().mockImplementation(() =>
    Loading...
    ) const generateInfiniteQueryOptions = ( data: Array<{ data: string; currentPage: number; totalPages: number }>, ) => { let currentPage = 0 return { queryFn: vi .fn<(...args: Array) => Promise<(typeof data)[number]>>() .mockImplementation(async () => { const currentPageData = data[currentPage] if (!currentPageData) { throw new Error('No data defined for page ' + currentPage) } await sleep(10) currentPage++ return currentPageData }), initialPageParam: 1, getNextPageParam: (lastPage: (typeof data)[number]) => lastPage.currentPage === lastPage.totalPages ? undefined : lastPage.currentPage + 1, } } describe('usePrefetchInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) function Suspended(props: { queryOpts: UseSuspenseInfiniteQueryOptions< T, Error, InfiniteData, Array, any > renderPage: (page: T) => React.JSX.Element }) { const state = useSuspenseInfiniteQuery(props.queryOpts) return (
    {state.data.pages.map((page, index) => (
    {props.renderPage(page)}
    ))}
    ) } it('should prefetch an infinite query if query state does not exist', async () => { const Fallback = createFallback() const data = [ { data: 'Do you fetch on render?', currentPage: 1, totalPages: 3 }, { data: 'Or do you render as you fetch?', currentPage: 2, totalPages: 3 }, { data: 'Either way, Tanstack Query helps you!', currentPage: 3, totalPages: 3, }, ] const queryOpts = { queryKey: queryKey(), ...generateInfiniteQueryOptions(data), } function App() { usePrefetchInfiniteQuery({ ...queryOpts, pages: data.length }) return ( }>
    data: {page.data}
    } />
    ) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(30)) rendered.getByText('data: Do you fetch on render?') fireEvent.click(rendered.getByText('Next Page')) expect( rendered.getByText('data: Or do you render as you fetch?'), ).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) expect( rendered.getByText('data: Either way, Tanstack Query helps you!'), ).toBeInTheDocument() expect(Fallback).toHaveBeenCalledTimes(1) expect(queryOpts.queryFn).toHaveBeenCalledTimes(3) }) it('should not display fallback if the query cache is already populated', async () => { const Fallback = createFallback() const queryOpts = { queryKey: queryKey(), ...generateInfiniteQueryOptions([ { data: 'Prefetch rocks!', currentPage: 1, totalPages: 3 }, { data: 'No waterfalls, boy!', currentPage: 2, totalPages: 3 }, { data: 'Tanstack Query #ftw', currentPage: 3, totalPages: 3 }, ]), } queryClient.prefetchInfiniteQuery({ ...queryOpts, pages: 3 }) await vi.advanceTimersByTimeAsync(30) ;(queryOpts.queryFn as Mock).mockClear() function App() { usePrefetchInfiniteQuery(queryOpts) return ( }>
    data: {page.data}
    } />
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: Prefetch rocks!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) expect(rendered.getByText('data: No waterfalls, boy!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) expect(rendered.getByText('data: Tanstack Query #ftw')).toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() expect(Fallback).not.toHaveBeenCalled() }) it('should not create an endless loop when using inside a suspense boundary', async () => { const queryOpts = { queryKey: queryKey(), ...generateInfiniteQueryOptions([ { data: 'Infinite Page 1', currentPage: 1, totalPages: 3 }, { data: 'Infinite Page 2', currentPage: 1, totalPages: 3 }, { data: 'Infinite Page 3', currentPage: 1, totalPages: 3 }, ]), } function Prefetch({ children }: { children: React.ReactNode }) { usePrefetchInfiniteQuery(queryOpts) return <>{children} } function App() { return (
    data: {page.data}
    } />
    ) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(10)) rendered.getByText('data: Infinite Page 1') fireEvent.click(rendered.getByText('Next Page')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: Infinite Page 2')).toBeInTheDocument() fireEvent.click(rendered.getByText('Next Page')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: Infinite Page 3')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(3) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken, usePrefetchQuery } from '..' describe('usePrefetchQuery', () => { it('should return nothing', () => { const result = usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(result).toEqualTypeOf() }) it('should not allow refetchInterval, enabled or throwOnError options', () => { assertType( usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 refetchInterval: 1000, }), ) assertType( usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 enabled: true, }), ) assertType( usePrefetchQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 throwOnError: true, }), ) }) it('should not allow skipToken in queryFn', () => { assertType( usePrefetchQuery({ queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }), ) assertType( usePrefetchQuery({ queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ) }) }) ================================================ FILE: packages/react-query/src/__tests__/usePrefetchQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import React from 'react' import { act, fireEvent } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, usePrefetchQuery, useQueryErrorResetBoundary, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' import type { UseSuspenseQueryOptions } from '..' const generateQueryFn = (data: string) => vi .fn<(...args: Array) => Promise>() .mockImplementation(async () => { await sleep(10) return data }) describe('usePrefetchQuery', () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) function Suspended(props: { queryOpts: UseSuspenseQueryOptions> children?: React.ReactNode }) { const state = useSuspenseQuery(props.queryOpts) return (
    data: {String(state.data)}
    {props.children}
    ) } it('should prefetch query if query state does not exist', async () => { const queryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('prefetchQuery'), } const componentQueryOpts = { ...queryOpts, queryFn: generateQueryFn('useSuspenseQuery'), } function App() { usePrefetchQuery(queryOpts) return ( ) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: prefetchQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) it('should not prefetch query if query state exists', async () => { const queryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('The usePrefetchQuery hook is smart!'), } function App() { usePrefetchQuery(queryOpts) return ( ) } queryClient.fetchQuery(queryOpts) await vi.advanceTimersByTimeAsync(10) queryOpts.queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument() expect( rendered.getByText('data: The usePrefetchQuery hook is smart!'), ).toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() }) it('should let errors fall through and not refetch failed queries', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryFn = generateQueryFn('Not an error') const queryOpts = { queryKey: queryKey(), queryFn, } queryFn.mockImplementationOnce(async () => { await sleep(10) throw new Error('Oops! Server error!') }) function App() { usePrefetchQuery(queryOpts) return (
    Oops!
    }>
    ) } queryClient.prefetchQuery(queryOpts) await vi.advanceTimersByTimeAsync(10) queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Oops!')).toBeInTheDocument() expect(rendered.queryByText('data: Not an error')).not.toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() consoleMock.mockRestore() }) it('should not create an endless loop when using inside a suspense boundary', async () => { const queryFn = generateQueryFn('prefetchedQuery') const queryOpts = { queryKey: queryKey(), queryFn, } function Prefetch({ children }: { children: React.ReactNode }) { usePrefetchQuery(queryOpts) return <>{children} } function App() { return ( ) } const rendered = renderWithClient(queryClient, ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: prefetchedQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) it('should be able to recover from errors and try fetching again', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryFn = generateQueryFn('This is fine :dog: :fire:') const queryOpts = { queryKey: queryKey(), queryFn, } queryFn.mockImplementationOnce(async () => { await sleep(10) throw new Error('Oops! Server error!') }) function App() { const { reset } = useQueryErrorResetBoundary() usePrefetchQuery(queryOpts) return ( (
    Oops!
    )} >
    ) } queryClient.prefetchQuery(queryOpts) await vi.advanceTimersByTimeAsync(10) queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Oops!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Try again')) await act(() => vi.advanceTimersByTimeAsync(10)) expect( rendered.getByText('data: This is fine :dog: :fire:'), ).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) consoleMock.mockRestore() }) it('should not create a suspense waterfall if prefetch is fired', async () => { const firstQueryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('Prefetch is nice!'), } const secondQueryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('Prefetch is really nice!!'), } const thirdQueryOpts = { queryKey: queryKey(), queryFn: generateQueryFn('Prefetch does not create waterfalls!!'), } const Fallback = vi.fn().mockImplementation(() =>
    Loading...
    ) function App() { usePrefetchQuery(firstQueryOpts) usePrefetchQuery(secondQueryOpts) usePrefetchQuery(thirdQueryOpts) return ( }> ) } const rendered = renderWithClient(queryClient, ) expect( queryClient.getQueryState(firstQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect( queryClient.getQueryState(secondQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect( queryClient.getQueryState(thirdQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect(rendered.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: Prefetch is nice!')).toBeInTheDocument() expect( rendered.getByText('data: Prefetch is really nice!!'), ).toBeInTheDocument() expect( rendered.getByText('data: Prefetch does not create waterfalls!!'), ).toBeInTheDocument() expect(Fallback).toHaveBeenCalledTimes(1) expect(firstQueryOpts.queryFn).toHaveBeenCalledTimes(1) expect(secondQueryOpts.queryFn).toHaveBeenCalledTimes(1) expect(thirdQueryOpts.queryFn).toHaveBeenCalledTimes(1) }) }) ================================================ FILE: packages/react-query/src/__tests__/useQueries.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { skipToken } from '..' import { useQueries } from '../useQueries' import { queryOptions } from '../queryOptions' import type { OmitKeyof } from '..' import type { UseQueryOptions, UseQueryResult } from '../types' describe('UseQueries config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = { queryKey: ['key2'], queryFn: () => 'Query Data', initialData: 'initial data', } const query3 = { queryKey: ['key2'], 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 }>() expectTypeOf(query2Data).toEqualTypeOf() expectTypeOf(query3Data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const queryResults = useQueries({ queries: [options] }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], 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() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const queryResults = useQueries({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) } const queryResults = useCustomQueries() const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf() }) }) it('TData should have correct type when conditional skipToken is passed', () => { const queryResults = useQueries({ queries: [ { queryKey: ['withSkipToken'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }) const firstResult = queryResults[0] expectTypeOf(firstResult).toEqualTypeOf>() expectTypeOf(firstResult.data).toEqualTypeOf() }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = useQueries({ queries: [...queries1List, { ...Queries2.get() }], }) expectTypeOf(result).toEqualTypeOf< [...Array>, UseQueryResult] >() }) }) ================================================ FILE: packages/react-query/src/__tests__/useQueries.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi, } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { IsRestoringProvider, QueryCache, QueryClient, queryOptions, skipToken, useQueries, } from '..' import { renderWithClient } from './utils' import type { QueryFunction, QueryKey, QueryObserverResult, UseQueryOptions, UseQueryResult, } from '..' import type { QueryFunctionContext } from '@tanstack/query-core' describe('useQueries', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should return the correct states', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array> = [] function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 1 }, }, { queryKey: key2, queryFn: async () => { await sleep(200) return 2 }, }, ], }) results.push(result) return (
    data1: {String(result[0].data ?? 'null')}, data2:{' '} {String(result[1].data ?? 'null')}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(201) 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 }]) }) it('should track results', async () => { const key1 = queryKey() const results: Array> = [] let count = 0 function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) count++ return count }, }, ], }) results.push(result) return (
    data: {String(result[0].data ?? 'null')}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(results.length).toBe(2) expect(results[0]).toMatchObject([{ data: undefined }]) expect(results[1]).toMatchObject([{ data: 1 }]) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 2')).toBeInTheDocument() // only one render for data update, no render for isFetching transition expect(results.length).toBe(3) expect(results[2]).toMatchObject([{ data: 2 }]) }) it('handles 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, boolean]] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf>() expectTypeOf(result1[1]).toEqualTypeOf>() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // 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() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf>() expectTypeOf(result2[1]).toEqualTypeOf>() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // types should be enforced useQueries<[[string, unknown, string], [string, boolean, number]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }) // field names should be enforced useQueries<[[string]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('handles 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; error: boolean }, ] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf>() expectTypeOf(result1[1]).toEqualTypeOf>() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // 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() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf>() expectTypeOf(result2[1]).toEqualTypeOf>() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // 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() return a as string }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as number }, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() // 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() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() 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', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('correctly returns types when passing through queryOptions', () => { // @ts-expect-error (Page component is not rendered) function Page() { // data and results types are correct when using queryOptions const result4 = useQueries({ queries: [ queryOptions({ queryKey: ['key1'], queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }), queryOptions({ queryKey: ['key2'], queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }), ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[0].data).toEqualTypeOf() expectTypeOf(result4[1].data).toEqualTypeOf() } }) it('handles array literal without type parameter to infer result type', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() const key5 = queryKey() type BizError = { code: number } const throwOnError = (_error: BizError) => true // @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> >() if (result1[0]) { expectTypeOf(result1[0].data).toEqualTypeOf() } // Array.map preserves TError const result1_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, throwOnError, })), }) expectTypeOf(result1_err).toEqualTypeOf< Array> >() if (result1_err[0]) { expectTypeOf(result1_err[0].data).toEqualTypeOf() expectTypeOf(result1_err[0].error).toEqualTypeOf() } // 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> >() const result2_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), throwOnError, })), }) expectTypeOf(result2_err).toEqualTypeOf< Array> >() const result3 = useQueries({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], select: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[2]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() expectTypeOf(result3[3].data).toEqualTypeOf() // select takes precedence over queryFn expectTypeOf(result3[2].data).toEqualTypeOf() // infer TError from throwOnError expectTypeOf(result3[3].error).toEqualTypeOf() // 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 and throwOnError 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), }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) // 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), }, { queryKey: key5, queryFn: () => 'string', select: (a: string) => parseInt(a), throwOnError, }, ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[2]).toEqualTypeOf>() expectTypeOf(result4[3]).toEqualTypeOf>() // handles when queryFn returns a Promise const result5 = useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.resolve('string'), }, ], }) expectTypeOf(result5[0]).toEqualTypeOf>() // Array as const does not throw error const result6 = useQueries({ queries: [ { queryKey: ['key1'], queryFn: () => 'string', }, { queryKey: ['key1'], queryFn: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], } as const) expectTypeOf(result6[0]).toEqualTypeOf>() expectTypeOf(result6[1]).toEqualTypeOf>() expectTypeOf(result6[2]).toEqualTypeOf>() // field names should be enforced - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // 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', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // supports queryFn using fetch() to return Promise - Array.map() result useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => fetch('return Promise').then((resp) => resp.json()), })), }) // supports queryFn using fetch() to return Promise - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }, ], }) } }) it('handles strongly typed queryFn factories and useQueries wrappers', () => { // QueryKey + queryFn factory type QueryKeyA = ['queryA'] const getQueryKeyA = (): QueryKeyA => ['queryA'] type GetQueryFunctionA = () => QueryFunction const getQueryFunctionA: GetQueryFunctionA = () => () => { return Promise.resolve(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 const getQueryFunctionB: GetQueryFunctionB = () => () => { return Promise.resolve('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>) { return useQueries({ queries: queries.map( // no need to type the mapped query (query) => { const { queryFn: fn, queryKey: key } = query expectTypeOf(fn).toEqualTypeOf< | typeof skipToken | QueryFunction | undefined >() return { queryKey: key, queryFn: fn && fn !== skipToken ? (ctx: QueryFunctionContext) => { // eslint-disable-next-line vitest/valid-expect expectTypeOf(ctx.queryKey) return fn.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>() expectTypeOf(result[1]).toEqualTypeOf>() 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> >() } }) it("should throw error if in one of queries' queryFn throws and throwOnError is in use", async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() function Page() { useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.reject( new Error( 'this should not throw because throwOnError is not set', ), ), }, { queryKey: key2, queryFn: () => Promise.reject(new Error('single query error')), throwOnError: true, retry: false, }, { queryKey: key3, queryFn: () => Promise.resolve(2), }, { queryKey: key4, queryFn: async () => Promise.reject( new Error('this should not throw because query#2 already did'), ), throwOnError: true, retry: false, }, ], }) return null } const rendered = renderWithClient( queryClient, (
    error boundary
    {error.message}
    )} >
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('single query error')).toBeInTheDocument() consoleMock.mockRestore() }) it("should throw error if in one of queries' queryFn throws and throwOnError function resolves to true", async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() function Page() { useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.reject( new Error( 'this should not throw because throwOnError function resolves to false', ), ), throwOnError: () => false, retry: false, }, { queryKey: key2, queryFn: () => Promise.resolve(2), }, { queryKey: key3, queryFn: () => Promise.reject(new Error('single query error')), throwOnError: () => true, retry: false, }, { queryKey: key4, queryFn: async () => Promise.reject( new Error('this should not throw because query#3 already did'), ), throwOnError: true, retry: false, }, ], }) return null } const rendered = renderWithClient( queryClient, (
    error boundary
    {error.message}
    )} >
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('single query error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = async () => { return Promise.resolve('custom client') } function Page() { const queries = useQueries( { queries: [ { queryKey: key, queryFn, }, ], }, queryClient, ) return
    data: {queries[0].data}
    } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) it('should combine queries', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const queries = useQueries( { queries: [ { queryKey: key1, queryFn: () => Promise.resolve('first result'), }, { queryKey: key2, queryFn: () => Promise.resolve('second result'), }, ], combine: (results) => { return { combined: true, res: results.map((res) => res.data).join(','), } }, }, queryClient, ) return (
    data: {String(queries.combined)} {queries.res}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: true first result,second result'), ).toBeInTheDocument() }) it('should not return new instances when called without queries', async () => { const key = queryKey() const ids: Array = [] let resultChanged = 0 function Page() { const [count, setCount] = React.useState(0) const result = useQueries({ queries: ids.map((id) => { return { queryKey: [key, id], queryFn: () => { return () => { return Promise.resolve({ id, content: { value: Math.random() }, }) } }, } }), combine: () => ({ empty: 'object' }), }) React.useEffect(() => { resultChanged++ }, [result]) return (
    count: {count}
    data: {JSON.stringify(result)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: {"empty":"object"}')).toBeInTheDocument() expect(rendered.getByText('count: 0')).toBeInTheDocument() expect(resultChanged).toBe(1) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('count: 1')).toBeInTheDocument() // there should be no further effect calls because the returned object is structurally shared expect(resultChanged).toBe(1) }) it('should not have infinite render loops with empty queries (#6645)', () => { let renderCount = 0 function Page() { const result = useQueries({ queries: [], }) React.useEffect(() => { renderCount++ }) return
    data: {JSON.stringify(result)}
    } renderWithClient(queryClient, ) expect(renderCount).toBe(1) }) it('should only call combine with query results', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(5) return Promise.resolve('query1') }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return Promise.resolve('query2') }, }, ], combine: ([query1, query2]) => { return { data: { query1: query1.data, query2: query2.data }, } }, }) return
    data: {JSON.stringify(result)}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText( 'data: {"data":{"query1":"query1","query2":"query2"}}', ), ).toBeInTheDocument() }) it('should track property access through combine function', async () => { const key1 = queryKey() const key2 = queryKey() let count = 0 const results: Array = [] function Page() { const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(5) return Promise.resolve('first result ' + count) }, }, { queryKey: key2, queryFn: async () => { await sleep(50) return Promise.resolve('second result ' + count) }, }, ], combine: (queryResults) => { return { combined: true, refetch: () => queryResults.forEach((res) => res.refetch()), res: queryResults .flatMap((res) => (res.data ? [res.data] : [])) .join(','), } }, }, queryClient, ) results.push(queries) return (
    data: {String(queries.combined)} {queries.res}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(51) expect( rendered.getByText('data: true first result 0,second result 0'), ).toBeInTheDocument() expect(results.length).toBe(3) expect(results[0]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: '', }) expect(results[1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0', }) expect(results[2]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0,second result 0', }) count++ fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(51) expect( rendered.getByText('data: true first result 1,second result 1'), ).toBeInTheDocument() const length = results.length expect([4, 5, 6]).toContain(results.length) expect(results[results.length - 1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 1,second result 1', }) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(100) // no further re-render because data didn't change expect(results.length).toBe(length) }) it('should synchronously track properties of all observer even if a property (isLoading) is only accessed on one observer (#7000)', async () => { const key = queryKey() const ids = [1, 2] function Page() { const { isLoading } = useQueries({ queries: ids.map((id) => ({ queryKey: [key, id], queryFn: () => sleep(10).then(() => { if (id === 2) throw new Error('FAILURE') return { id, title: `Post ${id}` } }), retry: false, })), combine: (results) => { // this tracks data on all observers void results.forEach((result) => result.data) return { // .some aborts early, so `isLoading` might not be accessed (and thus tracked) on all observers // leading to missing re-renders isLoading: results.some((result) => result.isLoading), } }, }) return (

    Loading Status: {isLoading ? 'Loading...' : 'Loaded'}

    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading Status: Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Loading Status: Loaded')).toBeInTheDocument() }) it('should not have stale closures with combine (#6648)', async () => { const key = queryKey() function Page() { const [count, setCount] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key, queryFn: () => Promise.resolve('result'), }, ], combine: (results) => { return { count, res: results.map((res) => res.data).join(','), } }, }, queryClient, ) return (
    data: {String(queries.count)} {queries.res}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 0 result')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 1 result')).toBeInTheDocument() }) it('should optimize combine if it is a stable reference', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() let value = 0 function Page() { const [state, setState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 'first result:' + value }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return 'second result:' + value }, }, ], combine: React.useCallback((results: Array) => { const result = { combined: true, res: results.map((res) => res.data).join(','), } spy(result) return result }, []), }, client, ) return (
    data: {String(queries.combined)} {queries.res}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: true first result:0,second result:0'), ).toBeInTheDocument() // both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) client.refetchQueries() await vi.advanceTimersByTimeAsync(21) // no increase because result hasn't changed expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) // one extra call due to recomputing the combined result on rerender expect(spy).toHaveBeenCalledTimes(4) value = 1 client.refetchQueries() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: true first result:1,second result:1'), ).toBeInTheDocument() // refetch with new values triggers: both pending -> one pending -> both resolved expect(spy).toHaveBeenCalledTimes(7) }) it('should re-run combine if the functional reference changes', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() function Page() { const [state, setState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(20) return 'second result' }, }, ], combine: React.useCallback( (results: Array) => { const result = { combined: true, state, res: results.map((res) => res.data).join(','), } spy(result) return result }, [state], ), }, client, ) return (
    data: {String(queries.state)} {queries.res}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: 0 first result,second result'), ).toBeInTheDocument() // both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) // state changed, re-run combine expect(spy).toHaveBeenCalledTimes(4) }) it('should not re-render if combine returns a stable reference', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const queryFns: Array = [] let renders = 0 function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) queryFns.push('first result') return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(20) queryFns.push('second result') return 'second result' }, }, ], combine: () => 'foo', }, client, ) renders++ return (
    data: {data}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('data: foo')).toBeInTheDocument() expect(queryFns).toEqual(['first result', 'second result']) expect(renders).toBe(1) }) it('should re-render once combine returns a different reference', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const client = new QueryClient() let renders = 0 function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(15) return 'second result' }, }, { queryKey: [key3], queryFn: async () => { await sleep(20) return 'third result' }, }, ], combine: (results) => { const isPending = results.some((res) => res.isPending) return isPending ? 'pending' : 'foo' }, }, client, ) renders++ return (
    data: {data}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('data: foo')).toBeInTheDocument() // one with pending, one with foo expect(renders).toBe(2) }) it('should track properties correctly with combine', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const client = new QueryClient() function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(15) return 'second result' }, }, { queryKey: [key3], queryFn: async () => { await sleep(20) return 'third result' }, }, ], combine: (results) => { if (results.find((r) => r.isPending)) { return 'pending' } return results.map((r) => r.data).join(', ') }, }, client, ) return (
    data: {data}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: first result, second result, third result'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /update/i })) await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText( 'data: first result updated, second result, third result', ), ).toBeInTheDocument() }) it('should not re-run stable combine on unrelated re-render', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() function Page() { const [unrelatedState, setUnrelatedState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return 'second result' }, }, ], combine: React.useCallback((results: Array) => { const result = { combined: true, res: results.map((res) => res.data).join(','), } spy(result) return result }, []), }, client, ) return (
    data: {String(queries.combined)} {queries.res}
    unrelated: {unrelatedState}
    ) } const rendered = render() await vi.advanceTimersByTimeAsync(21) expect( rendered.getByText('data: true first result,second result'), ).toBeInTheDocument() // initial renders: both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('unrelated: 1')).toBeInTheDocument() // combine should NOT re-run for unrelated re-render with stable reference expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('unrelated: 2')).toBeInTheDocument() // still no extra calls to combine expect(spy).toHaveBeenCalledTimes(3) }) it('should not cause infinite re-renders when removing last query', async () => { let renderCount = 0 function Page() { const [queries, setQueries] = React.useState([ { queryKey: ['query1'], queryFn: () => 'data1', }, { queryKey: ['query2'], queryFn: () => 'data2', }, ]) renderCount++ const result = useQueries({ queries }) return (
    renders: {renderCount}
    queries: {result.length}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) renderCount = 0 fireEvent.click(rendered.getByRole('button', { name: /remove last/i })) await vi.advanceTimersByTimeAsync(100) expect(renderCount).toBeLessThan(10) expect(rendered.getByTestId('query-count').textContent).toBe('queries: 1') renderCount = 0 fireEvent.click(rendered.getByRole('button', { name: /remove first/i })) await vi.advanceTimersByTimeAsync(100) expect(renderCount).toBeLessThan(10) expect(rendered.getByTestId('query-count').textContent).toBe('queries: 1') }) it('should return correct results when queries count changes with stable combine reference', async () => { const combine = (results: Array) => results const results: Array<{ n: number; length: number }> = [] function Page() { const [n, setN] = React.useState(0) const queries = useQueries( { queries: [...Array(n).keys()].map((i) => ({ queryKey: ['dynamic', i], queryFn: () => i, })), combine, }, queryClient, ) results.push({ n, length: queries.length }) return (
    {n} {queries.length}
    ) } const rendered = render() expect(rendered.getByTestId('n').textContent).toBe('0') expect(rendered.getByTestId('length').textContent).toBe('0') fireEvent.click(rendered.getByRole('button', { name: /increase/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByTestId('n').textContent).toBe('1') expect(rendered.getByTestId('length').textContent).toBe('1') fireEvent.click(rendered.getByRole('button', { name: /increase/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByTestId('n').textContent).toBe('2') expect(rendered.getByTestId('length').textContent).toBe('2') results.forEach((result) => { expect(result.length).toBe(result.n) }) }) 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 (
    {results[0]?.status}
    {results[1]?.status}
    {results[0]?.fetchStatus}
    {results[1]?.fetchStatus}
    {results[0]?.data ?? 'undefined'}
    {results[1]?.data ?? 'undefined'}
    ) } const rendered = renderWithClient( queryClient, , ) 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(11) 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) }) it('should not fetch queries with different durations 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(20).then(() => 'data2')) function Page() { const results = useQueries({ queries: [ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ], }) return (
    {results[0]?.status}
    {results[1]?.status}
    {results[0]?.fetchStatus}
    {results[1]?.fetchStatus}
    {results[0]?.data ?? 'undefined'}
    {results[1]?.data ?? 'undefined'}
    ) } const rendered = renderWithClient( queryClient, , ) 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(11) 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) }) }) ================================================ FILE: packages/react-query/src/__tests__/useQuery.promise.test.tsx ================================================ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { createRenderStream, useTrackRenders, } from '@testing-library/react-render-stream' import { queryKey } from '@tanstack/query-test-utils' import { waitFor } from '@testing-library/react' import { QueryClient, QueryClientProvider, QueryErrorResetBoundary, keepPreviousData, useInfiniteQuery, useQuery, } from '..' import { QueryCache } from '../index' describe('useQuery().promise', { timeout: 10_000 }, () => { const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache, }) beforeAll(() => { vi.useFakeTimers({ shouldAdvanceTime: true, toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'], }) queryClient.setDefaultOptions({ queries: { experimental_prefetchInRender: true }, }) }) afterAll(() => { vi.useRealTimers() queryClient.setDefaultOptions({ queries: { experimental_prefetchInRender: false }, }) }) it('should work with a basic test', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) useTrackRenders() return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }, }) return ( }>
    status:{query.status}
    ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('colocate suspense and promise', async () => { const key = queryKey() let callCount = 0 const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { callCount++ await vi.advanceTimersByTimeAsync(1) return 'test' }, staleTime: 1000, }) const data = React.use(query.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([MyComponent]) } expect(callCount).toBe(1) }) it('parallel queries', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) let callCount = 0 function MyComponent() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { callCount++ await vi.advanceTimersByTimeAsync(1) return 'test' }, staleTime: 1000, }) const data = React.use(query.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() return ( <> }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('testtesttesttesttest') expect(renderedComponents).toEqual([ MyComponent, MyComponent, MyComponent, MyComponent, MyComponent, ]) } expect(callCount).toBe(1) }) it('should work with initial data', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }, initialData: 'initial', }) return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') expect(renderedComponents).toEqual([Page, MyComponent]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should not fetch with initial data and staleTime', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const query = useQuery({ queryKey: key, queryFn, initialData: 'initial', staleTime: 1000, }) return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') expect(renderedComponents).toEqual([Page, MyComponent]) } // should not call queryFn because of staleTime + initialData combo expect(queryFn).toHaveBeenCalledTimes(0) }) it('should work with static placeholderData', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }, placeholderData: 'placeholder', }) useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('placeholder') expect(renderedComponents).toEqual([Page, MyComponent]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should work with placeholderData: keepPreviousData', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [...key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test-' + count }, placeholderData: keepPreviousData, }) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test-0') expect(renderedComponents).toEqual([Page, MyComponent]) } rendered.getByRole('button', { name: 'increment' }).click() // re-render because of the increment { const { renderedComponents } = await renderStream.takeRender() expect(renderedComponents).toEqual([Page, MyComponent]) } // re-render with new data, no loading between { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test-1') // no more suspense boundary rendering expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should be possible to select a part of the data with select', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return { name: 'test' } }, select: (data) => data.name, }) useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should throw error if the promise fails', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } let queryCount = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) if (++queryCount > 1) { // second time this query mounts, it should not throw return 'data' } throw new Error('Error test') }, retry: false, }) return ( }> ) } const rendered = await renderStream.render( {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('error boundary') } consoleMock.mockRestore() rendered.getByText('resetErrorBoundary').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('data') } expect(queryCount).toBe(2) }) it('should throw error if the promise fails (colocate suspense and promise)', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function MyComponent() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) throw new Error('Error test') }, retry: false, }) const data = React.use(query.promise) return <>{data} } function Page() { return ( ) } await renderStream.render(
    error boundary
    }>
    , ) { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('loading..')).toBeInTheDocument() } { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('error boundary')).toBeInTheDocument() } consoleMock.mockRestore() }) it('should recreate promise with data changes', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return 'test1' }, }) useTrackRenders() return ( }> ) } await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test1') expect(renderedComponents).toEqual([Page, MyComponent]) } queryClient.setQueryData(key, 'test2') { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test2') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should dedupe when re-fetched with queryClient.fetchQuery while suspending', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('fetch').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('test') } expect(queryFn).toHaveBeenCalledOnce() }) it('should dedupe when re-fetched with refetchQueries while suspending', async () => { const key = queryKey() let count = 0 const renderStream = createRenderStream({ snapshotDOM: true }) const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count++ }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('refetch').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') } expect(queryFn).toHaveBeenCalledOnce() }) it.skip('should stay pending when canceled with cancelQueries while suspending until refetched', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const key = queryKey() let count = 0 const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count++ }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
    }>
    ) } const rendered = await renderStream.render( <>error boundary}> , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('cancel').click() await vi.waitFor(() => { const state = queryClient.getQueryState(key) expect(state).toMatchObject({ status: 'pending', fetchStatus: 'idle', }) }) expect(queryFn).toHaveBeenCalledOnce() rendered.getByText('fetch').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('hello') } }) it('should resolve to previous data when canceled with cancelQueries while suspending', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const key = queryKey() const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(10) return 'test' }) const options = { queryKey: key, queryFn, } function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery(options) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } queryClient.setQueryData(key, 'initial') rendered.getByText('cancel').click() await vi.waitFor(() => { const state = queryClient.getQueryState(key) expect(state?.data).toBe('initial') }) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should suspend when not enabled', async () => { const renderStream = createRenderStream({ snapshotDOM: true }) const key = queryKey() const options = (count: number) => ({ queryKey: [...key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count }, }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const [count, setCount] = React.useState(0) const query = useQuery({ ...options(count), enabled: count > 0 }) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('loading..')).toBeInTheDocument() } rendered.getByText('enable').click() // loading re-render with enabled await renderStream.takeRender() { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('test1')).toBeInTheDocument() } }) it('should show correct data when read from cache only (staleTime)', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) queryClient.setQueryData(key, 'initial') const queryFn = vi.fn().mockImplementation(async () => { await vi.advanceTimersByTimeAsync(1) return 'test' }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) return <>{data} } function Loading() { return <>loading.. } function Page() { const query = useQuery({ queryKey: key, queryFn, staleTime: Infinity, }) return ( }> ) } await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('initial') } expect(queryFn).toHaveBeenCalledTimes(0) }) it('should show correct data when switching between cache entries without re-fetches', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent(props: { promise: Promise }) { useTrackRenders() const data = React.use(props.promise) return <>{data} } function Loading() { useTrackRenders() return <>loading.. } function Page() { useTrackRenders() const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count }, staleTime: Infinity, }) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(renderedComponents).toEqual([Page, MyComponent]) } rendered.getByText('inc').click() { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(renderedComponents).toEqual([Page, Loading]) } { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test1') expect(renderedComponents).toEqual([Page, MyComponent]) } rendered.getByText('dec').click() { const { renderedComponents, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(renderedComponents).toEqual([Page, MyComponent]) } }) it('should not resolve with intermediate data when keys are switched', async () => { const key = queryKey() const renderStream = createRenderStream<{ data: string }>({ snapshotDOM: true, }) function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) renderStream.replaceSnapshot({ data }) return <>{data} } function Loading() { return <>loading.. } function Page() { const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count }, staleTime: Infinity, }) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } rendered.getByText('inc').click() await renderStream.takeRender() rendered.getByText('inc').click() await renderStream.takeRender() { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test3') expect(snapshot).toMatchObject({ data: 'test3' }) } }) it('should not resolve with intermediate data when keys are switched (with background updates)', async () => { const key = queryKey() const renderStream = createRenderStream<{ data: string }>({ snapshotDOM: true, }) let modifier = '' function MyComponent(props: { promise: Promise }) { const data = React.use(props.promise) renderStream.replaceSnapshot({ data }) return <>{data} } function Loading() { return <>loading.. } function Page() { const [count, setCount] = React.useState(0) const query = useQuery({ queryKey: [key, count], queryFn: async () => { await vi.advanceTimersByTimeAsync(10) return 'test' + count + modifier }, }) return (
    }>
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test0') expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test0' }) } rendered.getByText('inc').click() { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') expect(snapshot).toMatchObject({ data: 'test0' }) } { const { snapshot, withinDOM } = await renderStream.takeRender() withinDOM().getByText('test3') expect(snapshot).toMatchObject({ data: 'test3' }) } modifier = 'new' rendered.getByText('dec').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test3' }) } rendered.getByText('dec').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test3' }) } rendered.getByText('dec').click() { const { snapshot } = await renderStream.takeRender() expect(snapshot).toMatchObject({ data: 'test0' }) } await waitFor(() => rendered.getByText('test0new')) }) it('should not suspend indefinitely with multiple, nested observers)', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function MyComponent({ input }: { input: string }) { const query = useTheQuery(input) const data = React.use(query.promise) return <>{data} } function useTheQuery(input: string) { return useQuery({ staleTime: Infinity, queryKey: [key, input], queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return input + ' response' }, }) } function Page() { const [input, setInput] = React.useState('defaultInput') useTheQuery(input) return (
    ) } const rendered = await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('defaultInput response') } expect( queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })! .observers.length, ).toBe(2) rendered.getByText('setInput').click() { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('loading..') } { const { withinDOM } = await renderStream.takeRender() withinDOM().getByText('someInput response') } expect( queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })! .observers.length, ).toBe(0) expect( queryClient.getQueryCache().find({ queryKey: [key, 'someInput'] })! .observers.length, ).toBe(2) }) it('should implicitly observe data when promise is used', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function Page() { useTrackRenders() const query = useInfiniteQuery({ queryKey: key, queryFn: async () => { await vi.advanceTimersByTimeAsync(1) return { nextCursor: 1, data: 'test' } }, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, }) React.use(query.promise) const hasNextPage = query.hasNextPage return (
    hasNextPage: {String(hasNextPage)}
    ) } await renderStream.render( , ) { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('loading..')).toBeInTheDocument() } { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('hasNextPage: true')).toBeInTheDocument() } }) it('should not throw to error boundary for refetch errors in infinite queries', async () => { const key = queryKey() const renderStream = createRenderStream({ snapshotDOM: true }) function Page() { const query = useInfiniteQuery({ queryKey: key, queryFn: async ({ pageParam = 0 }) => { await vi.advanceTimersByTimeAsync(1) if (pageParam === 0) { return { nextCursor: 1, data: 'page-1' } } throw new Error('page error') }, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, retry: false, }) const data = React.use(query.promise) return (
    pages:{data.pages.length}
    isError:{String(query.isError)}
    isFetchNextPageError:{String(query.isFetchNextPageError)}
    ) } const rendered = await renderStream.render(
    error boundary
    }>
    , ) { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('loading..')).toBeInTheDocument() } { const { withinDOM } = await renderStream.takeRender() expect(withinDOM().getByText('pages:1')).toBeInTheDocument() expect(withinDOM().getByText('isError:false')).toBeInTheDocument() expect( withinDOM().getByText('isFetchNextPageError:false'), ).toBeInTheDocument() } rendered.getByText('fetchNext').click() await vi.advanceTimersByTimeAsync(1) await waitFor(() => { expect( rendered.getByText('isFetchNextPageError:true'), ).toBeInTheDocument() }) expect(rendered.queryByText('error boundary')).toBeNull() }) }) ================================================ FILE: packages/react-query/src/__tests__/useQuery.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { queryOptions } from '../queryOptions' 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() expectTypeOf(noQueryFn.error).toEqualTypeOf() // it should infer the result type from the query function const fromQueryFn = useQuery({ queryKey: key, queryFn: () => 'test' }) expectTypeOf(fromQueryFn.data).toEqualTypeOf() expectTypeOf(fromQueryFn.error).toEqualTypeOf() expectTypeOf(fromQueryFn.promise).toEqualTypeOf>() // it should be possible to specify the result type const withResult = useQuery({ queryKey: key, queryFn: () => 'test', }) expectTypeOf(withResult.data).toEqualTypeOf() expectTypeOf(withResult.error).toEqualTypeOf() // it should be possible to specify the error type const withError = useQuery({ queryKey: key, queryFn: () => 'test', }) expectTypeOf(withError.data).toEqualTypeOf() expectTypeOf(withError.error).toEqualTypeOf() // it should provide the result type in the configuration useQuery({ queryKey: [key], queryFn: () => Promise.resolve(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({ queryKey: key, queryFn: () => 'test' }) // it should infer the result type from a generic query function function queryFn(): Promise { return Promise.resolve({} as T) } const fromGenericQueryFn = useQuery({ queryKey: key, queryFn: () => queryFn(), }) expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf() expectTypeOf(fromGenericQueryFn.error).toEqualTypeOf() const fromGenericOptionsQueryFn = useQuery({ queryKey: key, queryFn: () => queryFn(), }) expectTypeOf(fromGenericOptionsQueryFn.data).toEqualTypeOf< string | undefined >() expectTypeOf(fromGenericOptionsQueryFn.error).toEqualTypeOf() type MyData = number type MyQueryKey = readonly ['my-data', number] const getMyDataArrayKey: QueryFunction = ({ queryKey: [, n], }) => { return Promise.resolve(n + 42) } useQuery({ queryKey: ['my-data', 100], queryFn: getMyDataArrayKey, }) const getMyDataStringKey: QueryFunction = (context) => { expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() return Promise.resolve(Number(context.queryKey[0]) + 42) } useQuery({ queryKey: ['1'], queryFn: getMyDataStringKey, }) // it should handle query-functions that return Promise useQuery({ queryKey: key, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }) // handles wrapped queries with custom fetcher passed as inline queryFn const useWrappedQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: ( obj: TQueryKey[1], token: string, // return type must be wrapped with TQueryFnReturn ) => Promise, options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn' | 'initialData' >, ) => useQuery({ queryKey: qk, queryFn: () => fetcher(qk[1], 'token'), ...options, }) const testQuery = useWrappedQuery([''], () => Promise.resolve('1')) expectTypeOf(testQuery.data).toEqualTypeOf() // handles wrapped queries with custom fetcher passed directly to useQuery const useWrappedFuncStyleQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: () => Promise, options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn' | 'initialData' >, ) => useQuery({ queryKey: qk, queryFn: fetcher, ...options }) const testFuncStyle = useWrappedFuncStyleQuery([''], () => Promise.resolve(true), ) expectTypeOf(testFuncStyle.data).toEqualTypeOf() it('should return the correct states for a successful query', () => { const state = useQuery({ queryKey: key, queryFn: () => Promise.resolve('test'), }) if (state.isPending) { expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() return pending } if (state.isLoadingError) { expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() return {state.error.message} } expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() return {state.data} }) describe('initialData', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const { data } = useQuery({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, }) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { data } = useQuery(options) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) const query = useQuery({ ...options, select: (data) => data > 1, }) expectTypeOf(query.data).toEqualTypeOf() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = useQuery({ queryKey: ['key'], queryFn: () => { return { 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: ['key'], queryFn: () => { return { 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: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const { data, isSuccess } = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }) if (isSuccess) { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() } }) // eslint-disable-next-line vitest/expect-expect it('TData should depend from only arguments, not the result', () => { // @ts-expect-error const result: UseQueryResult<{ wow: string }> = useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }) void result }) it('data should not have undefined when initialData is provided', () => { const { data } = useQuery({ queryKey: ['query-key'], initialData: 42, }) expectTypeOf(data).toEqualTypeOf() }) }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQuery = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useQuery({ ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }) } const { data } = useCustomQuery() expectTypeOf(data).toEqualTypeOf() }) }) describe('structuralSharing', () => { it('should be able to use structuralSharing with unknown types', () => { // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 useQuery({ queryKey: ['key'], queryFn: () => 5, structuralSharing: (oldData, newData) => { expectTypeOf(oldData).toBeUnknown() expectTypeOf(newData).toBeUnknown() return newData }, }) }) }) }) }) ================================================ FILE: packages/react-query/src/__tests__/useQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { act, fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { mockVisibilityState, queryKey, sleep, } from '@tanstack/query-test-utils' import { IsRestoringProvider, QueryCache, QueryClient, dehydrate, hydrate, keepPreviousData, skipToken, useQuery, } from '..' import { Blink, mockOnlineManagerIsOnline, renderWithClient, setActTimeout, } from './utils' import type { DefinedUseQueryResult, QueryFunction, UseQueryResult } from '..' import type { Mock } from 'vitest' describe('useQuery', () => { let queryCache: QueryCache let queryClient: QueryClient beforeEach(() => { queryCache = new QueryCache() queryClient = new QueryClient({ queryCache, }) vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) // See https://github.com/tannerlinsley/react-query/issues/105 it('should allow to set default data value', async () => { const key = queryKey() function Page() { const { data = 'default' } = useQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'test'), }) return (

    {data}

    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('default')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('test')).toBeInTheDocument() }) it('should return the correct states for a successful query', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'test' }, }) states.push(state) if (state.isPending) { return pending } if (state.isLoadingError) { return {state.error.message} } return {state.data} } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('test') expect(states.length).toEqual(2) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: 'test', dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, isEnabled: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }) expect(states[0]!.promise).toEqual(states[1]!.promise) }) it('should return the correct states for an unsuccessful query', async () => { const key = queryKey() const states: Array = [] let index = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error(`rejected #${++index}`)), retry: 1, retryDelay: 1, }) states.push(state) return (

    Status: {state.status}

    Failure Count: {state.failureCount}
    Failure Reason: {state.failureReason?.message}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(2) rendered.getByText('Status: error') expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 1, failureReason: new Error('rejected #1'), errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[2]).toEqual({ data: undefined, dataUpdatedAt: 0, error: new Error('rejected #2'), errorUpdatedAt: expect.any(Number), failureCount: 2, failureReason: new Error('rejected #2'), errorUpdateCount: 1, isError: true, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: true, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', promise: expect.any(Promise), }) expect(states[0]!.promise).toEqual(states[1]!.promise) expect(states[1]!.promise).toEqual(states[2]!.promise) }) it('should set isFetchedAfterMount to true after a query has been fetched', async () => { const key = queryKey() await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) function Page() { const result = useQuery({ queryKey: key, queryFn: () => 'new data' }) return ( <>
    data: {result.data}
    isFetched: {result.isFetched ? 'true' : 'false'}
    isFetchedAfterMount: {result.isFetchedAfterMount ? 'true' : 'false'}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('data: prefetched')).toBeInTheDocument() expect(rendered.getByText('isFetched: true')).toBeInTheDocument() expect(rendered.getByText('isFetchedAfterMount: false')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: new data')).toBeInTheDocument() expect(rendered.getByText('isFetched: true')).toBeInTheDocument() expect(rendered.getByText('isFetchedAfterMount: true')).toBeInTheDocument() }) it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery({ queryKey: key, queryFn: async () => { fetchCount++ await sleep(10) return 'data' }, enabled: false, initialData: 'initialData', }) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch({ cancelRefetch: false }) }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) // first refetch only, second refetch is ignored expect(fetchCount).toBe(1) }) it('should cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we have data already', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery({ queryKey: key, queryFn: async () => { fetchCount++ await sleep(10) return 'data' }, enabled: false, initialData: 'initialData', }) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch() }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) // first refetch (gets cancelled) and second refetch expect(fetchCount).toBe(2) }) it('should not cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we do not have data yet', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery({ queryKey: key, queryFn: async () => { fetchCount++ await sleep(10) return 'data' }, enabled: false, }) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch() }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(15) // first refetch will not get cancelled, second one gets skipped expect(fetchCount).toBe(1) }) it('should be able to watch a query without providing a query function', async () => { const key = queryKey() const states: Array> = [] queryClient.setQueryDefaults(key, { queryFn: () => 'data' }) function Page() { const state = useQuery({ queryKey: key }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'data' }) }) it('should pick up a query when re-mounting with gcTime 0', async () => { const key = queryKey() const states: Array> = [] function Page() { const [toggle, setToggle] = React.useState(false) return (
    {toggle ? ( ) : ( )}
    ) } function Component({ value }: { value: string }) { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data: ' + value }, gcTime: 0, notifyOnChangeProps: 'all', }) states.push(state) return (
    {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /toggle/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 2') expect(states.length).toBe(4) // First load expect(states[0]).toMatchObject({ isPending: true, isSuccess: false, isFetching: true, }) // First success expect(states[1]).toMatchObject({ isPending: false, isSuccess: true, isFetching: false, }) // Switch, goes to fetching expect(states[2]).toMatchObject({ isPending: false, isSuccess: true, isFetching: true, }) // Second success expect(states[3]).toMatchObject({ isPending: false, isSuccess: true, isFetching: false, }) }) it('should not get into an infinite loop when removing a query with gcTime 0 and rerendering', async () => { const key = queryKey() const states: Array> = [] function Page() { const [, rerender] = React.useState({}) const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) return 'data' }, gcTime: 0, notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], }) states.push(state) return ( <>
    {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('data') fireEvent.click(rendered.getByRole('button', { name: 'remove' })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('data') expect(states.length).toBe(4) // First load expect(states[0]).toMatchObject({ isPending: true, isSuccess: false, data: undefined, }) // First success expect(states[1]).toMatchObject({ isPending: false, isSuccess: true, data: 'data', }) // Remove expect(states[2]).toMatchObject({ isPending: true, isSuccess: false, data: undefined, }) // Second success expect(states[3]).toMatchObject({ isPending: false, isSuccess: true, data: 'data', }) }) it('should fetch when refetchOnMount is false and nothing has been fetched yet', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test', refetchOnMount: false, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should not fetch when refetchOnMount is false and data has been fetched already', () => { const key = queryKey() const states: Array> = [] queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test', refetchOnMount: false, }) states.push(state) return null } renderWithClient(queryClient, ) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: 'prefetched' }) }) it('should be able to select a part of the data with select', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, }) states.push(state) return
    {state.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should be able to select a part of the data with select in object syntax', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, }) states.push(state) return
    {state.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should throw an error when a selector throws', async () => { const key = queryKey() const states: Array> = [] const error = new Error('Select Error') function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: () => { throw error }, }) states.push(state) return
    {state.status}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('error') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) expect(states[1]).toMatchObject({ status: 'error', error }) }) it('should not re-run a stable select when it re-renders if selector throws an error', async () => { const key = queryKey() const error = new Error('Select Error') let runs = 0 function Page() { const [, rerender] = React.useReducer(() => ({}), {}) const state = useQuery({ queryKey: key, queryFn: () => (runs === 0 ? 'test' : 'test2'), select: React.useCallback(() => { runs++ throw error }, []), }) return (
    error: {state.error?.message}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('error: Select Error') expect(runs).toEqual(1) fireEvent.click(rendered.getByRole('button', { name: 'rerender' })) await vi.advanceTimersByTimeAsync(0) expect(runs).toEqual(1) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(0) expect(runs).toEqual(2) }) it('should track properties and only re-render when a tracked property changes', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return 'test' + count }, }) states.push(state) return (

    {state.data ?? null}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('test1') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('test2') expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test1' }) expect(states[2]).toMatchObject({ data: 'test2' }) }) it('should always re-render if we are tracking props but not using any', async () => { const key = queryKey() let renderCount = 0 const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test' }) states.push(state) React.useEffect(() => { renderCount++ }, [state]) return (

    hello

    ) } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(renderCount).toBe(2) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should be able to remove a query', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const [, rerender] = React.useState({}) const state = useQuery({ queryKey: key, queryFn: () => ++count, notifyOnChangeProps: 'all', }) states.push(state) return (
    data: {state.data ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /remove/i })) await vi.advanceTimersByTimeAsync(0) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: 2') expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) // Fetched expect(states[1]).toMatchObject({ status: 'success', data: 1 }) // Remove + Hook state update, batched expect(states[2]).toMatchObject({ status: 'pending', data: undefined }) // Fetched expect(states[3]).toMatchObject({ status: 'success', data: 2 }) }) it('should create a new query when refetching a removed query', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return ++count }, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state return (
    data: {state.data ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /remove/i })) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 2') expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[1]).toMatchObject({ data: 1 }) // Switch expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[3]).toMatchObject({ data: 2 }) }) it('should share equal data structures between query results', async () => { const key = queryKey() const result1 = [ { id: '1', done: false }, { id: '2', done: false }, ] const result2 = [ { id: '1', done: false }, { id: '2', done: true }, ] const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count === 1 ? result1 : result2 }, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state return (
    data: {String(state.data?.[1]?.done)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: false') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: true') expect(states.length).toBe(4) const todos = states[2]?.data const todo1 = todos?.[0] const todo2 = todos?.[1] const newTodos = states[3]?.data const newTodo1 = newTodos?.[0] const newTodo2 = newTodos?.[1] expect(todos).toEqual(result1) expect(newTodos).toEqual(result2) expect(newTodos).not.toBe(todos) expect(newTodo1).toBe(todo1) expect(newTodo2).not.toBe(todo2) return null }) it('should use query function from hook when the existing query does not have a query function', async () => { const key = queryKey() queryClient.setQueryData(key, 'set') function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'fetched' }, initialData: 'initial', staleTime: Infinity, }) return (
    isFetching: {result.isFetching}
    data: {result.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: set')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: fetched')).toBeInTheDocument() }) it('should update query stale state and refetch when invalidated with invalidateQueries', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, staleTime: Infinity, }) return (
    data: {state.data}, isStale: {String(state.isStale)}, isFetching:{' '} {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 1, isStale: false, isFetching: false'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('data: 1, isStale: true, isFetching: true'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('data: 2, isStale: false, isFetching: false'), ).toBeInTheDocument() }) it('should not update disabled query when refetching with refetchQueries', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, enabled: false, }) states.push(state) React.useEffect(() => { setActTimeout(() => { queryClient.refetchQueries({ queryKey: key }) }, 20) }, []) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(31) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: false, }) }) it('should not refetch disabled query when invalidated with invalidateQueries', () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, enabled: false, }) states.push(state) React.useEffect(() => { setActTimeout(() => { queryClient.invalidateQueries({ queryKey: key }) }, 10) }, []) return null } renderWithClient(queryClient, ) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: false, }) }) it('should not fetch when switching to a disabled query', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(5) return count }, enabled: count === 0, }) states.push(state) return (
    data: {state.data ?? 'undefined'}
    count: {count}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('count: 1') rendered.getByText('data: undefined') expect(states.length).toBe(3) // Fetch query expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, }) // Fetched query expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, }) // Switch to disabled query expect(states[2]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, }) }) it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, placeholderData: keepPreviousData, }) states.push(state) return (
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous data when placeholderData is set and select fn transform is used', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return { count, } }, select(data) { return data.count }, placeholderData: keepPreviousData, }) states.push(state) return (
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous queryKey (from prevQuery) between multiple pending queries when placeholderData is set and select fn transform is used', async () => { const keys: Array | null> = [] const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return { count, } }, select(data) { return data.count }, placeholderData: (prevData, prevQuery) => { if (prevQuery) { keys.push(prevQuery.queryKey) } return prevData }, }) states.push(state) return (
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 3') const allPreviousKeysAreTheFirstQueryKey = keys.every( (k) => JSON.stringify(k) === JSON.stringify([key, 0]), ) expect(allPreviousKeysAreTheFirstQueryKey).toBe(true) }) it('should show placeholderData between multiple pending queries when select fn transform is used', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return { count, } }, select(data) { return data.count }, placeholderData: keepPreviousData, }) states.push(state) return (
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 3') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state -> count = 1 expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Set state -> count = 2 expect(states[3]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Set state -> count = 3 expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[5]).toMatchObject({ data: 3, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should transition to error state when placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page({ count }: { count: number }) { const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) if (count === 2) { throw new Error('Error test') } return Promise.resolve(count) }, retry: false, placeholderData: keepPreviousData, }) states.push(state) return (

    data: {state.data}

    error: {state.error?.message}

    placeholder data: {state.isPlaceholderData}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') rendered.rerender() await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') rendered.rerender() await vi.advanceTimersByTimeAsync(11) rendered.getByText('error: Error test') expect(states.length).toBe(6) // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, status: 'pending', error: null, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, status: 'success', error: null, isPlaceholderData: false, }) // rerender Page 1 expect(states[2]).toMatchObject({ data: 0, isFetching: true, status: 'success', error: null, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, status: 'success', error: null, isPlaceholderData: false, }) // rerender Page 2 expect(states[4]).toMatchObject({ data: 1, isFetching: true, status: 'success', error: null, isPlaceholderData: true, }) // Error expect(states[5]).toMatchObject({ data: undefined, isFetching: false, status: 'error', isPlaceholderData: false, }) expect(states[5]!.error).toHaveProperty('message', 'Error test') }) it('should not show initial data from next query if placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, initialData: 99, placeholderData: keepPreviousData, }) states.push(state) return (

    data: {state.data}, count: {count}, isFetching:{' '} {String(state.isFetching)}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0, count: 0, isFetching: false') fireEvent.click(rendered.getByRole('button', { name: 'inc' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1, count: 1, isFetching: false') expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPlaceholderData: false, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous data on disabled query when placeholderData is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, enabled: false, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) states.push(state) return (
    data: {state.data ?? 'undefined'}
    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('data: undefined') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') expect(states.length).toBe(6) // Disabled query expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isPlaceholderData: false, }) // Fetching query expect(states[1]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched query expect(states[2]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[3]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // Fetching new query expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Fetched new query expect(states[5]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', async () => { const key = queryKey() const states: Array> = [] queryClient.setQueryData([key, 10], 10) await vi.advanceTimersByTimeAsync(10) function Page() { const [count, setCount] = React.useState(10) const state = useQuery({ queryKey: [key, count], queryFn: async () => { await sleep(10) return count }, enabled: false, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state React.useEffect(() => { setActTimeout(() => { setCount(11) }, 20) setActTimeout(() => { setCount(12) }, 30) setActTimeout(() => { refetch() }, 40) }, [refetch]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) expect(states.length).toBe(5) // Disabled query expect(states[0]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[1]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // State update expect(states[2]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // Refetch expect(states[3]).toMatchObject({ data: 10, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Refetch done expect(states[4]).toMatchObject({ data: 12, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) it('should use the correct query function when components use different configurations', async () => { const key = queryKey() const states: Array> = [] function FirstComponent() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 1 }, notifyOnChangeProps: 'all', }) const refetch = state.refetch states.push(state) return (
    data: {state.data}
    ) } function SecondComponent() { useQuery({ queryKey: key, queryFn: () => 2, notifyOnChangeProps: 'all' }) return null } function Page() { return ( <> ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) expect(states[0]).toMatchObject({ data: undefined, }) expect(states[1]).toMatchObject({ data: 1, }) expect(states[2]).toMatchObject({ data: 1, }) // This state should be 1 instead of 2 expect(states[3]).toMatchObject({ data: 1, }) }) it('should be able to set different stale times for a query', async () => { const key = queryKey() const states1: Array> = [] const states2: Array> = [] queryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'prefetch' }, }) await vi.advanceTimersByTimeAsync(20) function FirstComponent() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'one' }, staleTime: 100, }) states1.push(state) return null } function SecondComponent() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'two' }, staleTime: 10, }) states2.push(state) return null } function Page() { return ( <> ) } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(200) expect(states1.length).toBe(4) expect(states2.length).toBe(3) expect(states1).toMatchObject([ // First render { data: 'prefetch', isStale: false, }, // Second useQuery started fetching { data: 'prefetch', isStale: false, }, // Second useQuery data came in { data: 'two', isStale: false, }, // Data became stale after 100ms { data: 'two', isStale: true, }, ]) expect(states2).toMatchObject([ // First render, data is stale and starts fetching { data: 'prefetch', isStale: true, }, // Second useQuery data came in { data: 'two', isStale: false, }, // Data became stale after 5ms { data: 'two', isStale: true, }, ]) }) it('should re-render when a query becomes stale', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'test', staleTime: 50, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(100) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ isStale: true }) expect(states[1]).toMatchObject({ isStale: false }) expect(states[2]).toMatchObject({ isStale: true }) }) it('should re-render disabled observers when other observers trigger a query (#8741)', async () => { const key = queryKey() const useUserInfoQuery = ({ id, enabled, }: { id: number | null enabled: boolean }) => { return useQuery({ queryKey: [key, id], queryFn: async () => { await sleep(10) return { id, name: 'John' } }, enabled: !!id && enabled, }) } const Page = () => { const [id, setId] = React.useState(null) const searchQuery = useUserInfoQuery({ id, enabled: false }) return ( <>
    User fetching status is {searchQuery.fetchStatus}
    ) } function UserInfo({ id }: { id: number | null }) { const searchQuery = useUserInfoQuery({ id, enabled: true }) return
    UserInfo data is {JSON.stringify(searchQuery.data)}
    } const rendered = renderWithClient(queryClient, ) rendered.getByText('User fetching status is idle') fireEvent.click(rendered.getByRole('button', { name: /set id/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('User fetching status is fetching'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('UserInfo data is {"id":42,"name":"John"}'), ).toBeInTheDocument() expect( rendered.getByText('User fetching status is idle'), ).toBeInTheDocument() }) describe('notifyOnChangeProps', () => { it('should not re-render when it should only re-render only data change and the selected data did not change', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, notifyOnChangeProps: ['data'], }) states.push(state) return (
    {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('test') expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) // make sure no additional renders happen await vi.advanceTimersByTimeAsync(50) expect(states.length).toBe(2) }) it('should not re-render when it should only re-render on data changes and the data did not change', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) return 'test' }, notifyOnChangeProps: ['data'], }) states.push(state) return ( <>
    {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('test') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) // sleep is required to make sure no additional renders happen after click await vi.advanceTimersByTimeAsync(20) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, status: 'pending', isFetching: true, }) expect(states[1]).toMatchObject({ data: 'test', status: 'success', isFetching: false, }) }) // See https://github.com/TanStack/query/discussions/5588 describe('function', () => { it('should not re-render when it should only re-render on data changes and the data did not change', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) return 'test' }, notifyOnChangeProps: () => ['data'], }) states.push(state) return ( <>
    {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('test') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(20) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, status: 'pending', isFetching: true, }) expect(states[1]).toMatchObject({ data: 'test', status: 'success', isFetching: false, }) }) it('should not re-render when change props are not actively being tracked', async () => { const key = queryKey() const states: Array> = [] function Page() { const fetchCounterRef = React.useRef(0) const trackChangesRef = React.useRef(true) const notifyOnChangeProps = React.useCallback(() => { return trackChangesRef.current ? 'all' : [] }, []) const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(5) fetchCounterRef.current++ return `fetch counter: ${fetchCounterRef.current}` }, notifyOnChangeProps, }) states.push(state) return ( <>
    {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('fetch counter: 1') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true, status: 'pending', }) expect(states[1]).toMatchObject({ data: 'fetch counter: 1', status: 'success', isFetching: false, }) // disable tracking and refetch to check for re-renders fireEvent.click( rendered.getByRole('button', { name: 'disableTracking' }), ) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(20) // still expect to only have two re-renders from the initial fetch expect(states.length).toBe(2) // enable tracking and refetch to check for re-renders fireEvent.click( rendered.getByRole('button', { name: 'enableTracking' }), ) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('fetch counter: 3') await vi.advanceTimersByTimeAsync(20) expect(states.length).toBe(4) expect(states[2]).toMatchObject({ data: 'fetch counter: 2', status: 'success', isFetching: true, }) expect(states[3]).toMatchObject({ data: 'fetch counter: 3', status: 'success', isFetching: false, }) }) }) }) // See https://github.com/tannerlinsley/react-query/issues/137 it('should not override initial data in dependent queries', () => { const key1 = queryKey() const key2 = queryKey() function Page() { const first = useQuery({ queryKey: key1, queryFn: () => 'data', enabled: false, initialData: 'init', }) const second = useQuery({ queryKey: key2, queryFn: () => 'data', enabled: false, initialData: 'init', }) return (

    First Data: {first.data}

    Second Data: {second.data}

    First Status: {first.status}
    Second Status: {second.status}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('First Data: init')).toBeInTheDocument() expect(rendered.getByText('Second Data: init')).toBeInTheDocument() expect(rendered.getByText('First Status: success')).toBeInTheDocument() expect(rendered.getByText('Second Status: success')).toBeInTheDocument() }) it('should update query options', () => { const key = queryKey() const queryFn = async () => { await sleep(10) return 'data1' } function Page() { useQuery({ queryKey: key, queryFn, retryDelay: 10 }) useQuery({ queryKey: key, queryFn, retryDelay: 20 }) return null } renderWithClient(queryClient, ) expect(queryCache.find({ queryKey: key })!.options.retryDelay).toBe(20) }) it('should batch re-renders', async () => { const key = queryKey() let renders = 0 const queryFn = async () => { await sleep(15) return 'data' } function Page() { const query1 = useQuery({ queryKey: key, queryFn }) const query2 = useQuery({ queryKey: key, queryFn }) renders++ return (
    {query1.data} {query2.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(16) rendered.getByText('data data') // Should be 2 instead of 3 expect(renders).toBe(2) }) it('should render latest data even if react has discarded certain renders', async () => { const key = queryKey() function Page() { const [, setNewState] = React.useState('state') const state = useQuery({ queryKey: key, queryFn: () => 'data' }) React.useEffect(() => { setActTimeout(() => { queryClient.setQueryData(key, 'new') // Update with same state to make react discard the next render setNewState('state') }, 10) }, []) return
    {state.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('new')).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/170 it('should start with status pending, fetchStatus idle if enabled is false', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const first = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return 'data' }, enabled: false, }) const second = useQuery({ queryKey: key2, queryFn: async () => { await sleep(10) return 'data' }, }) return (
    First Status: {first.status}, {first.fetchStatus}
    Second Status: {second.status}, {second.fetchStatus}
    ) } const rendered = renderWithClient(queryClient, ) // use "act" to wait for state update and prevent console warning expect( rendered.getByText('First Status: pending, idle'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('Second Status: pending, fetching'), ).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('Second Status: success, idle'), ).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/144 it('should be in "pending" state by default', () => { const key = queryKey() function Page() { const { status } = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'test' }, }) return
    status: {status}
    } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: pending')).toBeInTheDocument() }) it('should not refetch query on focus when `enabled` is set to `false`', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') function Page() { const { data = 'default' } = useQuery({ queryKey: key, queryFn, enabled: false, }) return (

    {data}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('default') act(() => { window.dispatchEvent(new Event('visibilitychange')) }) expect(queryFn).not.toHaveBeenCalled() }) it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => count++, staleTime: 0, refetchOnWindowFocus: false, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => count++, staleTime: 0, refetchOnWindowFocus: () => false, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: () => count++, staleTime: Infinity, refetchOnWindowFocus: true, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should refetch fresh query on focus when `refetchOnWindowFocus` is set to `always`', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return count++ }, staleTime: Infinity, refetchOnWindowFocus: 'always', }) states.push(state) return (
    data: {state.data}, isFetching: {String(state.isFetching)}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 0, isFetching: false')).toBeInTheDocument() act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1, isFetching: false')).toBeInTheDocument() }) it('should calculate focus behavior for `refetchOnWindowFocus` depending on function', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return count++ }, staleTime: 0, retry: 0, refetchOnWindowFocus: (query) => (query.state.data || 0) < 1, }) states.push(state) return
    data: {String(state.data)}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') // refetch should happen expect(states.length).toBe(4) expect(states[2]).toMatchObject({ data: 0, isFetching: true }) expect(states[3]).toMatchObject({ data: 1, isFetching: false }) act(() => { window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(20) // no more refetch now expect(states.length).toBe(4) }) it('should refetch fresh query when refetchOnMount is set to always', async () => { const key = queryKey() const states: Array> = [] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', refetchOnMount: 'always', staleTime: Infinity, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) }) it('should refetch stale query when refetchOnMount is set to true', async () => { const key = queryKey() const states: Array> = [] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) await vi.advanceTimersByTimeAsync(0) function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', refetchOnMount: true, staleTime: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should set status to error if queryFn throws', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => { return Promise.reject(new Error('Error test')) }, retry: false, }) return (

    {status}

    {error?.message}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error')).toBeInTheDocument() expect(rendered.getByText('Error test')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw error if queryFn throws and throwOnError is in use', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error('Error test')), retry: false, throwOnError: true, }) return (

    {status}

    {error}

    ) } const rendered = renderWithClient( queryClient,
    error boundary
    }>
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should update with data if we observe no properties and throwOnError', async () => { const key = queryKey() let result: UseQueryResult | undefined function Page() { const query = useQuery({ queryKey: key, queryFn: () => Promise.resolve('data'), throwOnError: true, }) React.useEffect(() => { result = query }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(queryClient.isFetching()).toBe(0) expect(result?.data).toBe('data') }) it('should set status to error instead of throwing when error should not be thrown', async () => { const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error('Local Error')), retry: false, throwOnError: (err) => err.message !== 'Local Error', }) return (

    {status}

    {error?.message}

    ) } const rendered = renderWithClient( queryClient,
    error boundary
    }>
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error')).toBeInTheDocument() expect(rendered.getByText('Local Error')).toBeInTheDocument() }) it('should throw error instead of setting status when error should be thrown', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const { status, error } = useQuery({ queryKey: key, queryFn: () => Promise.reject(new Error('Remote Error')), retry: false, throwOnError: (err) => err.message !== 'Local Error', }) return (

    {status}

    {error?.message ?? ''}

    ) } const rendered = renderWithClient( queryClient, (
    error boundary
    {error?.message}
    )} >
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('Remote Error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should continue retries when observers unmount and remount while waiting for a retry (#3031)', async () => { const key = queryKey() let count = 0 function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.reject(new Error('some error')) }, retry: 2, retryDelay: 100, }) return (
    error: {result.error?.message ?? 'null'}
    failureCount: {result.failureCount}
    failureReason: {result.failureReason?.message}
    ) } function App() { const [show, toggle] = React.useReducer((x) => !x, true) return (
    {show && }
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount: 1')).toBeInTheDocument() expect(rendered.getByText('failureReason: some error')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(90) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByRole('button', { name: /show/i })).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /show/i })) await vi.advanceTimersByTimeAsync(11) await vi.advanceTimersByTimeAsync(110) await vi.advanceTimersByTimeAsync(110) expect(rendered.getByText('error: some error')).toBeInTheDocument() expect(count).toBe(4) }) it('should restart when observers unmount and remount while waiting for a retry when query was cancelled in between (#3031)', async () => { const key = queryKey() let count = 0 function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.reject(new Error('some error')) }, retry: 2, retryDelay: 100, }) return (
    error: {result.error?.message ?? 'null'}
    failureCount: {result.failureCount}
    failureReason: {result.failureReason?.message}
    ) } function App() { const [show, toggle] = React.useReducer((x) => !x, true) return (
    {show && }
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount: 1')).toBeInTheDocument() expect(rendered.getByText('failureReason: some error')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /hide/i })) fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) expect(rendered.getByRole('button', { name: /show/i })).toBeInTheDocument() await vi.advanceTimersByTimeAsync(1) fireEvent.click(rendered.getByRole('button', { name: /show/i })) await vi.advanceTimersByTimeAsync(11) await vi.advanceTimersByTimeAsync(110) await vi.advanceTimersByTimeAsync(110) expect(rendered.getByText('error: some error')).toBeInTheDocument() // initial fetch (1), which will be cancelled, followed by new mount(2) + 2 retries = 4 expect(count).toBe(4) }) it('should always fetch if refetchOnMount is set to always', async () => { const key = queryKey() const states: Array> = [] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', refetchOnMount: 'always', staleTime: 50, }) states.push(state) return (
    data: {state.data ?? 'null'}
    isFetching: {state.isFetching}
    isStale: {state.isStale}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: data') await vi.advanceTimersByTimeAsync(52) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) expect(states[2]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should fetch if initial data is set', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', initialData: 'initial', }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should not fetch if initial data is set with a stale time', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', staleTime: 50, initialData: 'initial', }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(52) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: false, isFetching: false, }) expect(states[1]).toMatchObject({ data: 'initial', isStale: true, isFetching: false, }) }) it('should fetch if initial data updated at is older than stale time', async () => { const key = queryKey() const states: Array> = [] const oneSecondAgo = Date.now() - 1000 function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', staleTime: 50, initialData: 'initial', initialDataUpdatedAt: oneSecondAgo, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(52) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) expect(states[2]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should fetch if "initial data updated at" is exactly 0', async () => { const key = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data', staleTime: 10 * 1000, // 10 seconds initialData: 'initial', initialDataUpdatedAt: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) }) it('should keep initial data when the query key changes', async () => { const key = queryKey() const states: Array> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: [key, count], queryFn: () => ({ count: 10 }), staleTime: Infinity, initialData: () => ({ count }), }) states.push(state) React.useEffect(() => { setActTimeout(() => { setCount(1) }, 10) }, []) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) // Initial expect(states[0]).toMatchObject({ data: { count: 0 } }) // Set state expect(states[1]).toMatchObject({ data: { count: 1 } }) }) it('should retry specified number of times', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(() => { return Promise.reject(new Error('Error test Barrett')) }) function Page() { const { status, failureCount, failureReason } = useQuery({ queryKey: key, queryFn, retry: 1, retryDelay: 1, }) return (

    {status}

    Failed {failureCount} times

    Failed because {failureReason?.message}

    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('pending') await vi.advanceTimersByTimeAsync(2) rendered.getByText('error') // query should fail `retry + 1` times, since first time isn't a "retry" rendered.getByText('Failed 2 times') rendered.getByText('Failed because Error test Barrett') expect(queryFn).toHaveBeenCalledTimes(2) }) it('should not retry if retry function `false`', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementationOnce(() => { return Promise.reject(new Error('Error test Tanner')) }) queryFn.mockImplementation(() => { return Promise.reject(new Error('NoRetry')) }) function Page() { const { status, failureCount, failureReason, error } = useQuery({ queryKey: key, queryFn, retryDelay: 1, retry: (_failureCount, err) => err.message !== 'NoRetry', }) return (

    {status}

    Failed {failureCount} times

    Failed because {failureReason?.message}

    {error?.message}

    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('pending') await vi.advanceTimersByTimeAsync(2) rendered.getByText('error') rendered.getByText('Failed 2 times') rendered.getByText('Failed because NoRetry') rendered.getByText('NoRetry') expect(queryFn).toHaveBeenCalledTimes(2) }) it('should extract retryDelay from error', async () => { const key = queryKey() type DelayError = { delay: number } const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(() => { return Promise.reject({ delay: 50 }) }) function Page() { const { status, failureCount, failureReason } = useQuery({ queryKey: key, queryFn, retry: 1, retryDelay: (_, error: DelayError) => error.delay, }) return (

    {status}

    Failed {failureCount} times

    Failed because DelayError: {failureReason?.delay}ms

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(1) rendered.getByText('Failed because DelayError: 50ms') await vi.advanceTimersByTimeAsync(51) rendered.getByText('Failed 2 times') expect(queryFn).toHaveBeenCalledTimes(2) }) // See https://github.com/tannerlinsley/react-query/issues/160 it('should continue retry after focus regain', async () => { const key = queryKey() // make page unfocused const visibilityMock = mockVisibilityState('hidden') let count = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: () => { count++ return Promise.reject(`fetching error ${count}`) }, retry: 3, retryDelay: 1, }) return (
    error {String(query.error)}
    status {query.status}
    failureCount {query.failureCount}
    failureReason {query.failureReason}
    ) } const rendered = renderWithClient(queryClient, ) // The query should display the first error result await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('failureCount 1')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 1'), ).toBeInTheDocument() expect(rendered.getByText('status pending')).toBeInTheDocument() expect(rendered.getByText('error null')).toBeInTheDocument() // Check if the query really paused await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('failureCount 1')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 1'), ).toBeInTheDocument() act(() => { // reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new Event('visibilitychange')) }) // Wait for the final result await vi.advanceTimersByTimeAsync(4) expect(rendered.getByText('failureCount 4')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 4'), ).toBeInTheDocument() expect(rendered.getByText('status error')).toBeInTheDocument() expect(rendered.getByText('error fetching error 4')).toBeInTheDocument() // Check if the query really stopped await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('failureCount 4')).toBeInTheDocument() expect( rendered.getByText('failureReason fetching error 4'), ).toBeInTheDocument() }) it('should fetch on mount when a query was already created with setQueryData', async () => { const key = queryKey() const states: Array> = [] queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data' }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states.length).toBe(2) expect(states).toMatchObject([ { data: 'prefetched', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, ]) }) it('should refetch after focus regain', async () => { const key = queryKey() const states: Array> = [] // make page unfocused const visibilityMock = mockVisibilityState('hidden') // set data in cache to check if the hook query fn is actually called queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, }) states.push(state) return (
    {state.data}, {state.isStale}, {state.isFetching}
    ) } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) act(() => { // reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new Event('visibilitychange')) }) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) expect(states).toMatchObject([ { data: 'prefetched', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, { data: 'data', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, ]) }) // See https://github.com/tannerlinsley/react-query/issues/195 it('should refetch if stale after a prefetch', async () => { const key = queryKey() const states: Array> = [] const queryFn = vi.fn<(...args: Array) => string>() queryFn.mockImplementation(() => 'data') const prefetchQueryFn = vi.fn<(...args: Array) => string>() prefetchQueryFn.mockImplementation(() => 'not yet...') await queryClient.prefetchQuery({ queryKey: key, queryFn: prefetchQueryFn, staleTime: 10, }) await vi.advanceTimersByTimeAsync(10) function Page() { const state = useQuery({ queryKey: key, queryFn }) states.push(state) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(2) expect(prefetchQueryFn).toHaveBeenCalledTimes(1) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should not refetch if not stale after a prefetch', async () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => string>() queryFn.mockImplementation(() => 'data') const prefetchQueryFn = vi.fn<(...args: Array) => Promise>() prefetchQueryFn.mockImplementation(async () => { await sleep(10) return 'not yet...' }) queryClient.prefetchQuery({ queryKey: key, queryFn: prefetchQueryFn, staleTime: 1000, }) await vi.advanceTimersByTimeAsync(0) function Page() { useQuery({ queryKey: key, queryFn, staleTime: 1000 }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(prefetchQueryFn).toHaveBeenCalledTimes(1) expect(queryFn).toHaveBeenCalledTimes(0) }) // See https://github.com/tannerlinsley/react-query/issues/190 it('should reset failureCount on successful fetch', async () => { const key = queryKey() let counter = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: () => { if (counter < 2) { counter++ return Promise.reject(new Error('error')) } else { return Promise.resolve('data') } }, retryDelay: 10, }) return (
    failureCount {query.failureCount}
    failureReason {query.failureReason?.message ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount 2')).toBeInTheDocument() expect(rendered.getByText('failureReason error')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('failureCount 0')).toBeInTheDocument() expect(rendered.getByText('failureReason null')).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/199 it('should use prefetched data for dependent query', async () => { const key = queryKey() let count = 0 function Page() { const [enabled, setEnabled] = React.useState(false) const [isPrefetched, setPrefetched] = React.useState(false) const query = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return count }, enabled, }) React.useEffect(() => { async function prefetch() { await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('prefetched data'), }) act(() => setPrefetched(true)) } prefetch() }, []) return (
    {isPrefetched &&
    isPrefetched
    }
    data: {query.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isPrefetched')).toBeInTheDocument() fireEvent.click(rendered.getByText('setKey')) expect(rendered.getByText('data: prefetched data')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(count).toBe(1) }) it('should support dependent queries via the enable config option', async () => { const key = queryKey() function Page() { const [shouldFetch, setShouldFetch] = React.useState(false) const query = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, enabled: shouldFetch, }) return (
    FetchStatus: {query.fetchStatus}

    Data: {query.data || 'no data'}

    {shouldFetch ? null : ( )}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('FetchStatus: idle')).toBeInTheDocument() expect(rendered.getByText('Data: no data')).toBeInTheDocument() fireEvent.click(rendered.getByText('fetch')) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('FetchStatus: fetching')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('FetchStatus: idle')).toBeInTheDocument() expect(rendered.getByText('Data: data')).toBeInTheDocument() }) it('should mark query as fetching, when using initialData', async () => { const key = queryKey() const results: Array> = [] function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'serverData' }, initialData: 'initialData', }) results.push(result) return
    data: {result.data}
    } const rendered = renderWithClient(queryClient, ) rendered.getByText('data: initialData') await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: serverData') expect(results.length).toBe(2) expect(results[0]).toMatchObject({ data: 'initialData', isFetching: true }) expect(results[1]).toMatchObject({ data: 'serverData', isFetching: false }) }) it('should initialize state properly, when initialData is falsy', async () => { const key = queryKey() const results: Array> = [] function Page() { const result = useQuery({ queryKey: key, queryFn: () => 1, initialData: 0, }) results.push(result) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(results.length).toBe(2) expect(results[0]).toMatchObject({ data: 0, isFetching: true }) expect(results[1]).toMatchObject({ data: 1, isFetching: false }) }) it('should show the correct data when switching keys with initialData, placeholderData & staleTime', async () => { const key = queryKey() const ALL_TODOS = [ { name: 'todo A', priority: 'high' }, { name: 'todo B', priority: 'medium' }, ] const initialTodos = ALL_TODOS function Page() { const [filter, setFilter] = React.useState('') const { data: todos } = useQuery({ queryKey: [...key, filter], queryFn: () => { return Promise.resolve( ALL_TODOS.filter((todo) => filter ? todo.priority === filter : true, ), ) }, initialData() { return filter === '' ? initialTodos : undefined }, placeholderData: keepPreviousData, staleTime: 5000, }) return (
    Current Todos, filter: {filter || 'all'}
      {(todos ?? []).map((todo) => (
    • {todo.name} - {todo.priority}
    • ))}
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Current Todos, filter: all')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /high/i })) await vi.advanceTimersByTimeAsync(0) expect( rendered.getByText('Current Todos, filter: high'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /all/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('todo B - medium')).toBeInTheDocument() }) // // See https://github.com/tannerlinsley/react-query/issues/214 it('data should persist when enabled is changed to false', async () => { const key = queryKey() const results: Array> = [] function Page() { const [shouldFetch, setShouldFetch] = React.useState(true) const result = useQuery({ queryKey: key, queryFn: () => 'fetched data', enabled: shouldFetch, initialData: shouldFetch ? 'initial' : 'initial falsy', }) results.push(result) return (
    {result.data}
    {shouldFetch ? 'enabled' : 'disabled'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('fetched data')).toBeInTheDocument() expect(rendered.getByText('enabled')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /enable/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('fetched data')).toBeInTheDocument() expect(rendered.getByText('disabled')).toBeInTheDocument() expect(results.length).toBe(3) expect(results[0]).toMatchObject({ data: 'initial', isStale: true }) expect(results[1]).toMatchObject({ data: 'fetched data', isStale: true }) // disabled observers are not stale expect(results[2]).toMatchObject({ data: 'fetched data', isStale: false }) }) it('should support enabled:false in query object syntax', () => { const key = queryKey() const queryFn = vi.fn<(...args: Array) => string>() queryFn.mockImplementation(() => 'data') function Page() { const { fetchStatus } = useQuery({ queryKey: key, queryFn, enabled: false, }) return
    fetchStatus: {fetchStatus}
    } const rendered = renderWithClient(queryClient, ) expect(queryFn).not.toHaveBeenCalled() expect(queryCache.find({ queryKey: key })).not.toBeUndefined() expect(rendered.getByText('fetchStatus: idle')).toBeInTheDocument() }) // See https://github.com/tannerlinsley/react-query/issues/360 test('should init to status:pending, fetchStatus:idle when enabled is false', async () => { const key = queryKey() function Page() { const query = useQuery({ queryKey: key, queryFn: () => 'data', enabled: false, }) return (
    status: {query.status}, {query.fetchStatus}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('status: pending, idle')).toBeInTheDocument() }) test('should not schedule garbage collection, if gcTimeout is set to `Infinity`', async () => { const key = queryKey() function Page() { const query = useQuery({ queryKey: key, queryFn: () => 'fetched data', gcTime: Infinity, }) return
    {query.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('fetched data') const setTimeoutSpy = vi.spyOn(globalThis.window, 'setTimeout') rendered.unmount() expect(setTimeoutSpy).not.toHaveBeenCalled() }) test('should schedule garbage collection, if gcTimeout is not set to infinity', async () => { const key = queryKey() function Page() { const query = useQuery({ queryKey: key, queryFn: () => 'fetched data', gcTime: 1000 * 60 * 10, // 10 Minutes }) return
    {query.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('fetched data') const setTimeoutSpy = vi.spyOn(globalThis.window, 'setTimeout') rendered.unmount() expect(setTimeoutSpy).toHaveBeenLastCalledWith( expect.any(Function), 1000 * 60 * 10, ) }) it('should not cause memo churn when data does not change', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') const memoFn = vi.fn() function Page() { const result = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return ( queryFn() || { data: { nested: true, }, } ) }, }) React.useMemo(() => { memoFn() return result.data }, [result.data]) return (
    status {result.status}
    isFetching {result.isFetching ? 'true' : 'false'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status pending') await vi.advanceTimersByTimeAsync(11) rendered.getByText('status success') fireEvent.click(rendered.getByText('refetch')) await vi.advanceTimersByTimeAsync(0) rendered.getByText('isFetching true') await vi.advanceTimersByTimeAsync(11) rendered.getByText('isFetching false') expect(queryFn).toHaveBeenCalledTimes(2) expect(memoFn).toHaveBeenCalledTimes(2) }) it('should update data upon interval changes', async () => { const key = queryKey() let count = 0 function Page() { const [int, setInt] = React.useState(200) const { data } = useQuery({ queryKey: key, queryFn: () => count++, refetchInterval: int, }) React.useEffect(() => { if (data === 2) { setInt(0) } }, [data]) return
    count: {data}
    } const rendered = renderWithClient(queryClient, ) // mount await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('count: 0')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(201) expect(rendered.getByText('count: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(201) expect(rendered.getByText('count: 2')).toBeInTheDocument() }) it('should refetch in an interval depending on function result', async () => { const key = queryKey() let count = 0 const states: Array> = [] function Page() { const queryInfo = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return count++ }, refetchInterval: ({ state: { data = 0 } }) => (data < 2 ? 10 : false), }) states.push(queryInfo) return (

    count: {queryInfo.data}

    status: {queryInfo.status}

    data: {queryInfo.data}

    refetch: {queryInfo.isRefetching}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) rendered.getByText('count: 2') expect(states.length).toEqual(6) expect(states).toMatchObject([ { status: 'pending', isFetching: true, data: undefined, }, { status: 'success', isFetching: false, data: 0, }, { status: 'success', isFetching: true, data: 0, }, { status: 'success', isFetching: false, data: 1, }, { status: 'success', isFetching: true, data: 1, }, { status: 'success', isFetching: false, data: 2, }, ]) }) it('should not interval fetch with a refetchInterval of 0', async () => { const key = queryKey() const queryFn = vi.fn(() => 1) function Page() { const queryInfo = useQuery({ queryKey: key, queryFn, refetchInterval: 0, }) return
    count: {queryInfo.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('count: 1') await vi.advanceTimersByTimeAsync(10) // extra sleep to make sure we're not re-fetching expect(queryFn).toHaveBeenCalledTimes(1) }) it('should accept an empty string as query key', async () => { function Page() { const result = useQuery({ queryKey: [''], queryFn: (ctx) => ctx.queryKey, }) return <>{JSON.stringify(result.data)} } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('')).toBeInTheDocument() }) it('should accept an object as query key', async () => { function Page() { const result = useQuery({ queryKey: [{ a: 'a' }], queryFn: (ctx) => ctx.queryKey, }) return <>{JSON.stringify(result.data)} } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('[{"a":"a"}]')).toBeInTheDocument() }) it('should refetch if any query instance becomes enabled', async () => { const key = queryKey() const queryFn = vi .fn<(...args: Array) => string>() .mockReturnValue('data') function Disabled() { useQuery({ queryKey: key, queryFn, enabled: false }) return null } function Page() { const [enabled, setEnabled] = React.useState(false) const result = useQuery({ queryKey: key, queryFn, enabled }) return ( <>
    {result.data}
    ) } const rendered = renderWithClient(queryClient, ) expect(queryFn).toHaveBeenCalledTimes(0) fireEvent.click(rendered.getByText('enable')) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data') expect(queryFn).toHaveBeenCalledTimes(1) }) it('should use placeholder data while the query loads', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key1, queryFn: () => 'data', placeholderData: 'placeholder', }) states.push(state) return (

    Data: {state.data}

    Status: {state.status}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: data') expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, { isSuccess: true, isPlaceholderData: false, data: 'data', }, ]) }) it('should use placeholder data even for disabled queries', async () => { const key1 = queryKey() const states: Array<{ state: UseQueryResult; count: number }> = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ queryKey: key1, queryFn: () => 'data', placeholderData: 'placeholder', enabled: count === 0, }) states.push({ state, count }) React.useEffect(() => { setCount(1) }, []) return (

    Data: {state.data}

    Status: {state.status}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: data') expect(states).toMatchObject([ { state: { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, count: 0, }, { state: { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, count: 1, }, { state: { isSuccess: true, isPlaceholderData: false, data: 'data', }, count: 1, }, ]) }) it('placeholder data should run through select', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const state = useQuery({ queryKey: key1, queryFn: () => 1, placeholderData: 23, select: (data) => String(data * 2), }) states.push(state) return (

    Data: {state.data}

    Status: {state.status}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: 2') expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: '46', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, ]) }) it('placeholder data function result should run through select', async () => { const key1 = queryKey() const states: Array> = [] let placeholderFunctionRunCount = 0 function Page() { const state = useQuery({ queryKey: key1, queryFn: () => 1, placeholderData: () => { placeholderFunctionRunCount++ return 23 }, select: (data) => String(data * 2), }) states.push(state) return (

    Data: {state.data}

    Status: {state.status}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('Data: 2') rendered.rerender() expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: '46', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, ]) expect(placeholderFunctionRunCount).toEqual(1) }) it('select should only run when dependencies change if memoized', async () => { const key1 = queryKey() let selectRun = 0 function Page() { const [count, inc] = React.useReducer((prev) => prev + 1, 2) const state = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return 0 }, select: React.useCallback( (data: number) => { selectRun++ return `selected ${data + count}` }, [count], ), placeholderData: 99, }) return (

    Data: {state.data}

    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('Data: selected 101') // 99 + 2 expect(selectRun).toBe(1) await vi.advanceTimersByTimeAsync(11) rendered.getByText('Data: selected 2') // 0 + 2 expect(selectRun).toBe(2) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('Data: selected 3') // 0 + 3 expect(selectRun).toBe(3) }) it('select should always return the correct state', async () => { const key1 = queryKey() function Page() { const [count, inc] = React.useReducer((prev) => prev + 1, 2) const [forceValue, forceUpdate] = React.useReducer((prev) => prev + 1, 1) const state = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return 0 }, select: React.useCallback( (data: number) => { return `selected ${data + count}` }, [count], ), placeholderData: 99, }) return (

    Data: {state.data}

    forceValue: {forceValue}

    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Data: selected 101')).toBeInTheDocument() // 99 + 2 await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: selected 2')).toBeInTheDocument() // 0 + 2 fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: selected 3')).toBeInTheDocument() // 0 + 3 fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) expect(rendered.getByText('forceValue: 2')).toBeInTheDocument() // data should still be 3 after an independent re-render await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: selected 3')).toBeInTheDocument() }) it('select should structurally share data', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const [forceValue, forceUpdate] = React.useReducer((prev) => prev + 1, 1) const state = useQuery({ queryKey: key1, queryFn: async () => { await sleep(10) return [1, 2] }, select: (res) => res.map((x) => x + 1), }) React.useEffect(() => { if (state.data) { states.push(state.data) } }, [state.data]) return (

    Data: {JSON.stringify(state.data)}

    forceValue: {forceValue}

    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('Data: [2,3]') expect(states).toHaveLength(1) fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('forceValue: 2') rendered.getByText('Data: [2,3]') // effect should not be triggered again due to structural sharing expect(states).toHaveLength(1) }) it('should cancel the query function when there are no more subscriptions', async () => { const key = queryKey() let cancelFn: Mock = vi.fn() const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = vi.fn(() => reject('Cancelled')) signal?.addEventListener('abort', cancelFn) sleep(20).then(() => resolve('OK')) }) return promise } function Page() { const state = useQuery({ queryKey: key, queryFn }) return (

    Status: {state.status}

    ) } const rendered = renderWithClient( queryClient, , ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('off') expect(cancelFn).toHaveBeenCalled() }) it('should cancel the query if the signal was consumed and there are no more subscriptions', async () => { const key = queryKey() const states: Array> = [] const queryFn: QueryFunction = async ( ctx, ) => { const [, limit] = ctx.queryKey const value = limit % 2 && ctx.signal ? 'abort' : `data ${limit}` await sleep(25) return value } function Page(props: { limit: number }) { const state = useQuery({ queryKey: [key, props.limit], queryFn }) // eslint-disable-next-line react-hooks/immutability states[props.limit] = state return (

    Status: {state.status}

    data: {state.data}

    ) } const rendered = renderWithClient( queryClient, , ) await vi.advanceTimersByTimeAsync(6) rendered.getByText('off') await vi.advanceTimersByTimeAsync(20) expect(states).toHaveLength(4) expect(queryCache.find({ queryKey: [key, 0] })?.state).toMatchObject({ data: 'data 0', status: 'success', dataUpdateCount: 1, }) expect(queryCache.find({ queryKey: [key, 1] })?.state).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'idle', }) expect(queryCache.find({ queryKey: [key, 2] })?.state).toMatchObject({ data: 'data 2', status: 'success', dataUpdateCount: 1, }) expect(queryCache.find({ queryKey: [key, 3] })?.state).toMatchObject({ data: undefined, status: 'pending', fetchStatus: 'idle', }) }) it('should refetch when quickly switching to a failed query', async () => { const key = queryKey() const states: Array> = [] const queryFn = async () => { await sleep(50) return 'OK' } function Page() { const [id, setId] = React.useState(1) const [hasChanged, setHasChanged] = React.useState(false) const state = useQuery({ queryKey: [key, id], queryFn }) states.push(state) React.useEffect(() => { setId((prevId) => (prevId === 1 ? 2 : 1)) setHasChanged(true) }, [hasChanged]) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(51) expect(states.length).toBe(4) // Load query 1 expect(states[0]).toMatchObject({ status: 'pending', error: null, }) // Load query 2 expect(states[1]).toMatchObject({ status: 'pending', error: null, }) // Load query 1 expect(states[2]).toMatchObject({ status: 'pending', error: null, }) // Loaded query 1 expect(states[3]).toMatchObject({ status: 'success', error: null, }) }) it('should update query state and refetch when reset with resetQueries', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, staleTime: Infinity, }) states.push(state) return (
    data: {state.data ?? 'null'}
    isFetching: {state.isFetching}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(11) expect(states.length).toBe(4) rendered.getByText('data: 2') expect(count).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isPending: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[1]).toMatchObject({ data: 1, isPending: false, isFetching: false, isSuccess: true, isStale: false, }) expect(states[2]).toMatchObject({ data: undefined, isPending: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[3]).toMatchObject({ data: 2, isPending: false, isFetching: false, isSuccess: true, isStale: false, }) }) it('should update query state and not refetch when resetting a disabled query with resetQueries', async () => { const key = queryKey() const states: Array> = [] let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) count++ return count }, staleTime: Infinity, enabled: false, notifyOnChangeProps: 'all', }) states.push(state) const { refetch } = state return (
    data: {state.data ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('data: null') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: null') expect(states.length).toBe(4) expect(count).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isPending: true, isFetching: false, isSuccess: false, isStale: false, }) expect(states[1]).toMatchObject({ data: undefined, isPending: true, isFetching: true, isSuccess: false, isStale: false, }) expect(states[2]).toMatchObject({ data: 1, isPending: false, isFetching: false, isSuccess: true, isStale: false, }) expect(states[3]).toMatchObject({ data: undefined, isPending: true, isFetching: false, isSuccess: false, isStale: false, }) }) it('should only call the query hash function once each render', async () => { const key = queryKey() let hashes = 0 let renders = 0 function queryKeyHashFn(x: any) { hashes++ return JSON.stringify(x) } function Page() { React.useEffect(() => { renders++ }) useQuery({ queryKey: key, queryFn: () => 'test', queryKeyHashFn }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(renders).toBe(hashes) }) it('should hash query keys that contain bigints given a supported query hash function', async () => { const key = [queryKey(), 1n] function queryKeyHashFn(x: any) { return JSON.stringify(x, (_, value) => { if (typeof value === 'bigint') return value.toString() return value }) } function Page() { useQuery({ queryKey: key, queryFn: () => 'test', queryKeyHashFn }) return null } renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) const query = queryClient.getQueryCache().get(queryKeyHashFn(key)) expect(query?.state.data).toBe('test') }) it('should refetch when changed enabled to true in error state', async () => { const queryFn = vi.fn<(...args: Array) => unknown>() queryFn.mockImplementation(async () => { await sleep(10) return Promise.reject(new Error('Suspense Error Bingo')) }) function Page({ enabled }: { enabled: boolean }) { const { error, isPending } = useQuery({ queryKey: ['key'], queryFn, enabled, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isPending) { return
    status: pending
    } if (error instanceof Error) { return
    error
    } return
    rendered
    } function App() { const [enabled, toggle] = React.useReducer((x) => !x, true) return (
    ) } const rendered = renderWithClient(queryClient, ) // initial state check rendered.getByText('status: pending') // // render error state component await vi.advanceTimersByTimeAsync(11) rendered.getByText('error') expect(queryFn).toBeCalledTimes(1) // change to enabled to false fireEvent.click(rendered.getByLabelText('retry')) await vi.advanceTimersByTimeAsync(11) rendered.getByText('error') expect(queryFn).toBeCalledTimes(1) // // change to enabled to true fireEvent.click(rendered.getByLabelText('retry')) expect(queryFn).toBeCalledTimes(2) }) it('should refetch when query key changed when previous status is error', async () => { function Page({ id }: { id: number }) { const { error, isPending } = useQuery({ queryKey: [id], queryFn: async () => { await sleep(10) if (id % 2 === 1) { return Promise.reject(new Error('Error')) } else { return 'data' } }, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isPending) { return
    status: pending
    } if (error instanceof Error) { return
    error
    } return
    rendered
    } function App() { const [id, changeId] = React.useReducer((x) => x + 1, 1) return (
    ) } const rendered = renderWithClient(queryClient, ) // initial state check expect(rendered.getByText('status: pending')).toBeInTheDocument() // render error state component await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() // change to unmount query fireEvent.click(rendered.getByLabelText('change')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('rendered')).toBeInTheDocument() // change to mount new query fireEvent.click(rendered.getByLabelText('change')) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() }) it('should refetch when query key changed when switching between erroneous queries', async () => { function Page({ id }: { id: boolean }) { const { error, isFetching } = useQuery({ queryKey: [id], queryFn: async () => { await sleep(10) return Promise.reject(new Error('Error')) }, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isFetching) { return
    status: fetching
    } if (error instanceof Error) { return
    error
    } return
    rendered
    } function App() { const [value, toggle] = React.useReducer((x) => !x, true) return (
    ) } const rendered = renderWithClient(queryClient, ) // initial state check expect(rendered.getByText('status: fetching')).toBeInTheDocument() // render error state component await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() // change to mount second query fireEvent.click(rendered.getByLabelText('change')) expect(rendered.getByText('status: fetching')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() // change to mount first query again fireEvent.click(rendered.getByLabelText('change')) expect(rendered.getByText('status: fetching')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('error')).toBeInTheDocument() }) it('should have no error in pending state when refetching after error occurred', async () => { const key = queryKey() const states: Array> = [] const error = new Error('oops') let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) if (count === 0) { count++ throw error } return 5 }, retry: false, }) states.push(state) if (state.isPending) { return
    status: pending
    } if (state.error instanceof Error) { return (
    error
    ) } return
    data: {state.data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('error') fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 5') expect(states.length).toBe(4) expect(states[0]).toMatchObject({ status: 'pending', data: undefined, error: null, }) expect(states[1]).toMatchObject({ status: 'error', data: undefined, error, }) expect(states[2]).toMatchObject({ status: 'pending', data: undefined, error: null, }) expect(states[3]).toMatchObject({ status: 'success', data: 5, error: null, }) }) describe('networkMode online', () => { it('online queries should not start fetching if you are offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() const states: Array = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, }) React.useEffect(() => { states.push(state.fetchStatus) }) return (
    status: {state.status}, isPaused: {String(state.isPaused)}
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, isPaused: true') onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, isPaused: false') expect(rendered.getByText('data: data')).toBeInTheDocument() expect(states).toEqual(['paused', 'fetching', 'idle']) onlineMock.mockRestore() }) it('online queries should not refetch if you are offline', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
    failureReason: {state.failureReason ?? 'null'}
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: data1') const onlineMock = mockOnlineManagerIsOnline(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText( 'status: success, fetchStatus: paused, failureCount: 0', ) rendered.getByText('failureReason: null') onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(0) rendered.getByText( 'status: success, fetchStatus: fetching, failureCount: 0', ) rendered.getByText('failureReason: null') await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: idle, failureCount: 0') rendered.getByText('failureReason: null') expect(rendered.getByText('data: data2')).toBeInTheDocument() onlineMock.mockRestore() }) it('online queries should not refetch if you are offline and refocus', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: data1') const onlineMock = mockOnlineManagerIsOnline(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status: success, fetchStatus: paused') window.dispatchEvent(new Event('visibilitychange')) await vi.advanceTimersByTimeAsync(11) expect(rendered.queryByText('data: data2')).not.toBeInTheDocument() expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should not refetch while already paused', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status: pending, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) // invalidation should not trigger a refetch rendered.getByText('status: pending, fetchStatus: paused') expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not refetch while already paused if data is in the cache', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, initialData: 'initial', }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: success, fetchStatus: paused') expect(rendered.getByText('data: initial')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) // invalidation should not trigger a refetch rendered.getByText('status: success, fetchStatus: paused') expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not get stuck in fetching state when pausing multiple times', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, initialData: 'initial', }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: success, fetchStatus: paused') expect(rendered.getByText('data: initial')).toBeInTheDocument() // triggers one pause fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: paused') // triggers a second pause act(() => { window.dispatchEvent(new Event('visibilitychange')) }) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: idle') expect(rendered.getByText('data: data1')).toBeInTheDocument() expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should pause retries if you are offline', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) }, retry: 2, retryDelay: 10, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
    failureReason: {state.failureReason?.message ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText(/status: pending, fetchStatus: fetching/i) const onlineMock = mockOnlineManagerIsOnline(false) await vi.advanceTimersByTimeAsync(31) rendered.getByText( 'status: pending, fetchStatus: paused, failureCount: 1', ) rendered.getByText('failureReason: failed1') expect(count).toBe(1) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(31) rendered.getByText('status: error, fetchStatus: idle, failureCount: 3') rendered.getByText('failureReason: failed3') expect(count).toBe(3) onlineMock.mockRestore() }) it('online queries should not fetch if paused initial load and we go online after unmount', async () => { const key = queryKey() let count = 0 function Component() { const state = useQuery({ queryKey: key, queryFn: async ({ signal: _signal }) => { count++ await sleep(10) return `signal${count}` }, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } function Page() { const [show, setShow] = React.useState(true) return (
    {show && }
    ) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'pending', }) expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should re-fetch if paused and we go online even if already unmounted (because not cancelled)', async () => { const key = queryKey() let count = 0 queryClient.setQueryData(key, 'initial') function Component() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } function Page() { const [show, setShow] = React.useState(true) return (
    {show && }
    ) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: success, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'success', }) // give it a bit more time to make sure queryFn is not called again expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should not fetch if paused and we go online when cancelled and no refetchOnReconnect', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, refetchOnReconnect: false, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } const onlineMock = mockOnlineManagerIsOnline(false) const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: pending, fetchStatus: idle') expect(count).toBe(0) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: pending, fetchStatus: idle') expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should fetch if paused and we go online even if already unmounted when refetch was not cancelled', async () => { const key = queryKey() let count = 0 function Component() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ return `data${count}` }, }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}
    data: {state.data}
    ) } function Page() { const [show, setShow] = React.useState(true) return (
    {show && }
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, fetchStatus: idle') const onlineMock = mockOnlineManagerIsOnline(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('status: success, fetchStatus: paused') fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(11) expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'success', }) expect(count).toBe(2) onlineMock.mockRestore() }) }) describe('networkMode always', () => { it('always queries should start fetching even if you are offline', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data ' + count }, networkMode: 'always', }) return (
    status: {state.status}, isPaused: {String(state.isPaused)}
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: success, isPaused: false') expect(rendered.getByText('data: data 1')).toBeInTheDocument() onlineMock.mockRestore() }) it('always queries should not pause retries', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('error ' + count) }, networkMode: 'always', retry: 1, retryDelay: 5, }) return (
    status: {state.status}, isPaused: {String(state.isPaused)}
    error: {state.error instanceof Error && state.error.message}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(26) rendered.getByText('status: error, isPaused: false') expect(rendered.getByText('error: error 2')).toBeInTheDocument() expect(count).toBe(2) onlineMock.mockRestore() }) }) describe('networkMode offlineFirst', () => { it('offlineFirst queries should start fetching if you are offline, but pause retries', async () => { const onlineMock = mockOnlineManagerIsOnline(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) }, retry: 2, retryDelay: 1, networkMode: 'offlineFirst', }) return (
    status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
    failureReason: {state.failureReason?.message ?? 'null'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(12) rendered.getByText( 'status: pending, fetchStatus: paused, failureCount: 1', ) rendered.getByText('failureReason: failed1') expect(count).toBe(1) onlineMock.mockReturnValue(true) queryClient.getQueryCache().onOnline() await vi.advanceTimersByTimeAsync(22) rendered.getByText('status: error, fetchStatus: idle, failureCount: 3') rendered.getByText('failureReason: failed3') expect(count).toBe(3) onlineMock.mockRestore() }) }) describe('subscribed', () => { it('should be able to toggle subscribed', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) function Page() { const [subscribed, setSubscribed] = React.useState(true) const { data } = useQuery({ queryKey: key, queryFn, subscribed, }) return (
    data: {data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: data') expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(1) fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(0) expect(queryFn).toHaveBeenCalledTimes(1) fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) // background refetch when we re-subscribe await vi.advanceTimersByTimeAsync(0) expect(queryFn).toHaveBeenCalledTimes(2) expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(1) }) it('should not be attached to the query when subscribed is false', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) function Page() { const { data } = useQuery({ queryKey: key, queryFn, subscribed: false, }) return (
    data: {data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data:') expect( queryClient.getQueryCache().find({ queryKey: key })!.observers.length, ).toBe(0) expect(queryFn).toHaveBeenCalledTimes(0) }) it('should not re-render when data is added to the cache when subscribed is false', async () => { const key = queryKey() let renders = 0 function Page() { const { data } = useQuery({ queryKey: key, queryFn: () => Promise.resolve('data'), subscribed: false, }) renders++ return (
    {data ? 'has data' + data : 'no data'}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('no data') fireEvent.click(rendered.getByRole('button', { name: 'set data' })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('no data') expect(renders).toBe(1) }) }) it('should have status=error on mount when a query has failed', async () => { const key = queryKey() const states: Array> = [] const error = new Error('oops') const queryFn = (): Promise => { return Promise.reject(error) } function Page() { const state = useQuery({ queryKey: key, queryFn, retry: false, retryOnMount: false, }) states.push(state) return <> } await queryClient.prefetchQuery({ queryKey: key, queryFn }) renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(states).toHaveLength(1) expect(states[0]).toMatchObject({ status: 'error', error, }) }) it('setQueryData - should respect updatedAt', async () => { const key = queryKey() function Page() { const state = useQuery({ queryKey: key, queryFn: () => 'data' }) return (
    data: {state.data}
    dataUpdatedAt: {state.dataUpdatedAt}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: data') fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) await vi.advanceTimersByTimeAsync(0) rendered.getByText('data: newData') expect(rendered.getByText('dataUpdatedAt: 100')).toBeInTheDocument() }) it('errorUpdateCount should increased on each fetch failure', async () => { const key = queryKey() const error = new Error('oops') function Page() { const { refetch, errorUpdateCount } = useQuery({ queryKey: key, queryFn: (): Promise => { return Promise.reject(error) }, retry: false, }) return (
    data: {errorUpdateCount}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) const fetchBtn = rendered.getByRole('button', { name: 'refetch' }) expect(rendered.getByText('data: 1')).toBeInTheDocument() fireEvent.click(fetchBtn) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 2')).toBeInTheDocument() fireEvent.click(fetchBtn) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: 3')).toBeInTheDocument() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = async () => { return Promise.resolve('custom client') } function Page() { const { data } = useQuery( { queryKey: key, queryFn, }, queryClient, ) return
    data: {data}
    } const rendered = render() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) it('should be notified of updates between create and subscribe', async () => { const key = queryKey() function Page() { const { data, status } = useQuery({ enabled: false, queryKey: key, queryFn: async () => { await sleep(10) return 5 }, }) const mounted = React.useRef(null) // this simulates a synchronous update between the time the query is created // and the time it is subscribed to that could be missed otherwise if (mounted.current === null) { mounted.current = true queryClient.setQueryData(key, 1) } return (
    status: {status} data: {data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: success')).toBeInTheDocument() expect(rendered.getByText('data: 1')).toBeInTheDocument() }) it('should reuse same data object reference when queryKey changes back to some cached data', async () => { const key = queryKey() const spy = vi.fn() async function fetchNumber(id: number) { await sleep(5) return { numbers: { current: { id } } } } function Test() { const [id, setId] = React.useState(1) const { data } = useQuery({ select: selector, queryKey: [key, 'user', id], queryFn: () => fetchNumber(id), }) React.useEffect(() => { spy(data) }, [data]) return (
    Rendered Id: {data?.id}
    ) } function selector(data: any) { return data.numbers.current } const rendered = renderWithClient(queryClient, ) expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(2) // called with undefined because id changed spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /1/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(1) }) it('should reuse same data object reference when queryKey changes and placeholderData is present', async () => { const key = queryKey() const spy = vi.fn() async function fetchNumber(id: number) { await sleep(5) return { numbers: { current: { id } } } } function Test() { const [id, setId] = React.useState(1) const { data } = useQuery({ select: selector, queryKey: [key, 'user', id], queryFn: () => fetchNumber(id), placeholderData: { numbers: { current: { id: 99 } } }, }) React.useEffect(() => { spy(data) }, [data]) return (
    Rendered Id: {data?.id}
    ) } function selector(data: any) { return data.numbers.current } const rendered = renderWithClient(queryClient, ) expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() rendered.getByText('Rendered Id: 99') await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) rendered.getByText('Rendered Id: 99') await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(2) // called with undefined because id changed spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /1/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 1') expect(spy).toHaveBeenCalledTimes(1) spy.mockClear() fireEvent.click(rendered.getByRole('button', { name: /2/ })) await vi.advanceTimersByTimeAsync(6) rendered.getByText('Rendered Id: 2') expect(spy).toHaveBeenCalledTimes(1) }) it('should not cause an infinite render loop when using unstable callback ref', async () => { const key = queryKey() function Test() { const [_, setRef] = React.useState() const { data } = useQuery({ queryKey: [key], queryFn: async () => { await sleep(5) return 'Works' }, }) return
    setRef(value)}>{data}
    } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(6) expect(rendered.getByText('Works')).toBeInTheDocument() }) it('should keep the previous data when placeholderData is set and cache is used', async () => { const key = queryKey() const states: Array> = [] const steps = [0, 1, 0, 2] function Page() { const [count, setCount] = React.useState(0) const state = useQuery({ staleTime: Infinity, queryKey: [key, steps[count]], queryFn: async () => { await sleep(10) return steps[count] }, placeholderData: keepPreviousData, }) states.push(state) return (
    data: {state.data}
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 0') fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: 2') // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state with existing data expect(states[4]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state where the placeholder value should come from cache request expect(states[5]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[6]).toMatchObject({ data: 2, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }) // For Project without TS, when migrating from v4 to v5, make sure invalid calls due to bad parameters are tracked. it('should throw in case of bad arguments to enhance DevX', () => { // Mock console error to avoid noise when test is run const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() const queryFn = () => 'data' function Page() { // Invalid call on purpose // @ts-expect-error useQuery(key, { queryFn }) return
    Does not matter
    } expect(() => render()).toThrow('Bad argument type') consoleMock.mockRestore() }) it('should respect skipToken and refetch when skipToken is taken away', async () => { const key = queryKey() function Page({ enabled }: { enabled: boolean }) { const { data, status } = useQuery({ queryKey: [key], queryFn: enabled ? async () => { await sleep(10) return Promise.resolve('data') } : skipToken, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) return (
    status: {status}
    data: {String(data)}
    ) } function App() { const [enabled, toggle] = React.useReducer((x) => !x, false) return (
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('status: pending')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: 'enable' })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('status: success')).toBeInTheDocument() expect(rendered.getByText('data: data')).toBeInTheDocument() }) it('should allow enabled: true and queryFn: skipToken', () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function App() { const query = useQuery({ queryKey: key, queryFn: skipToken, enabled: true, }) return (
    status: {query.status}, fetchStatus: {query.fetchStatus}
    ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('status: pending, fetchStatus: idle') // no warnings expected about skipToken / missing queryFn expect(consoleMock).toHaveBeenCalledTimes(0) consoleMock.mockRestore() }) it('should return correct optimistic result when fetching after error', async () => { const key = queryKey() const error = new Error('oh no') const results: Array> = [] function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return Promise.reject(error) }, retry: false, notifyOnChangeProps: 'all', }) results.push(query) return (
    status: {query.status}, {query.fetchStatus}
    error: {query.error?.message}
    ) } function App() { const [enabled, setEnabled] = React.useState(true) return (
    {enabled && }
    ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: error, idle') fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('status: error, idle') expect(results).toHaveLength(4) // initial fetch expect(results[0]).toMatchObject({ status: 'pending', fetchStatus: 'fetching', error: null, errorUpdatedAt: 0, errorUpdateCount: 0, isLoading: true, failureCount: 0, failureReason: null, }) // error state expect(results[1]).toMatchObject({ status: 'error', fetchStatus: 'idle', error, errorUpdateCount: 1, isLoading: false, failureCount: 1, failureReason: error, }) expect(results[1]?.errorUpdatedAt).toBeGreaterThan(0) // refetch, optimistic state, no errors anymore expect(results[2]).toMatchObject({ status: 'pending', fetchStatus: 'fetching', error: null, errorUpdateCount: 1, isLoading: true, failureCount: 0, failureReason: null, }) expect(results[2]?.errorUpdatedAt).toBeGreaterThan(0) // final state expect(results[3]).toMatchObject({ status: 'error', fetchStatus: 'idle', error: error, errorUpdateCount: 2, isLoading: false, failureCount: 1, failureReason: error, }) expect(results[3]?.errorUpdatedAt).toBeGreaterThan(0) }) it('should pick up an initialPromise', async () => { const key = queryKey() const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } }, }) void serverQueryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return Promise.resolve('server') }, }) const dehydrated = dehydrate(serverQueryClient) let count = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.resolve('client') }, }) return (
    data: {query.data}
    ) } const clientQueryClient = new QueryClient() hydrate(clientQueryClient, dehydrated) const rendered = renderWithClient(clientQueryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: server') expect(count).toBe(0) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await vi.advanceTimersByTimeAsync(11) rendered.getByText('data: client') expect(count).toBe(1) }) it('should retry failed initialPromise on the client', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() const serverQueryClient = new QueryClient({ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true }, }, }) void serverQueryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return Promise.reject(new Error('server error')) }, }) const dehydrated = dehydrate(serverQueryClient) let count = 0 function Page() { const query = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return Promise.resolve('client') }, }) return (
    failure: {query.failureReason?.message}
    data: {query.data}
    ) } const clientQueryClient = new QueryClient({ defaultOptions: { hydrate: { queries: { retry: 1, retryDelay: 10 } } }, }) hydrate(clientQueryClient, dehydrated) const rendered = renderWithClient(clientQueryClient, ) await vi.advanceTimersByTimeAsync(11) rendered.getByText('failure: redacted') await vi.advanceTimersByTimeAsync(21) rendered.getByText('data: client') expect(count).toBe(1) const query = clientQueryClient.getQueryCache().find({ queryKey: key }) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenCalledWith( `A query that was dehydrated as pending ended up rejecting. [${query?.queryHash}]: Error: server error; The error will be redacted in production builds`, ) consoleMock.mockRestore() }) it('should console.error when there is no queryFn', () => { const consoleErrorMock = vi.spyOn(console, 'error') const key = queryKey() function Example() { useQuery({ queryKey: key }) return <> } renderWithClient(queryClient, ) expect(consoleErrorMock).toHaveBeenCalledTimes(1) expect(consoleErrorMock).toHaveBeenCalledWith( `[${queryClient.getQueryCache().find({ queryKey: key })?.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`, ) consoleErrorMock.mockRestore() }) it('should retry on mount when throwOnError returns false', async () => { const key = queryKey() let fetchCount = 0 const queryFn = vi.fn().mockImplementation(() => { fetchCount++ console.log(`Fetching... (attempt ${fetchCount})`) return Promise.reject(new Error('Simulated 500 error')) }) function Component() { const { status, error } = useQuery({ queryKey: key, queryFn, throwOnError: () => false, retryOnMount: true, staleTime: Infinity, retry: false, }) return (
    {status}
    {error &&
    {error.message}
    }
    ) } const rendered1 = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered1.getByTestId('status')).toHaveTextContent('error') expect(rendered1.getByTestId('error')).toHaveTextContent( 'Simulated 500 error', ) expect(fetchCount).toBe(1) rendered1.unmount() const initialFetchCount = fetchCount const rendered2 = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered2.getByTestId('status')).toHaveTextContent('error') expect(fetchCount).toBe(initialFetchCount + 1) expect(queryFn).toHaveBeenCalledTimes(2) }) it('should not retry on mount when throwOnError function returns true', async () => { const key = queryKey() let fetchCount = 0 const queryFn = vi.fn().mockImplementation(() => { fetchCount++ console.log(`Fetching... (attempt ${fetchCount})`) return Promise.reject(new Error('Simulated 500 error')) }) function Component() { const { status, error } = useQuery({ queryKey: key, queryFn, throwOnError: () => true, retryOnMount: true, staleTime: Infinity, retry: false, }) return (
    {status}
    {error &&
    {error.message}
    }
    ) } const rendered1 = renderWithClient( queryClient, (
    error
    {error?.message}
    )} >
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered1.getByTestId('status')).toHaveTextContent('error') expect(rendered1.getByTestId('error')).toHaveTextContent( 'Simulated 500 error', ) expect(fetchCount).toBe(1) rendered1.unmount() const initialFetchCount = fetchCount const rendered2 = renderWithClient( queryClient, (
    error
    {error?.message}
    )} >
    , ) await vi.advanceTimersByTimeAsync(0) expect(rendered2.getByTestId('status')).toHaveTextContent('error') // Should not retry because throwOnError returns true expect(fetchCount).toBe(initialFetchCount) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should handle throwOnError function based on actual error state', async () => { const key = queryKey() let fetchCount = 0 const queryFn = vi.fn().mockImplementation(() => { fetchCount++ console.log(`Fetching... (attempt ${fetchCount})`) return Promise.reject(new Error('Simulated 500 error')) }) function Component() { const { status, error } = useQuery({ queryKey: key, queryFn, throwOnError: (error) => error.message.includes('404'), retryOnMount: true, staleTime: Infinity, retry: false, }) return (
    {status}
    {error &&
    {error.message}
    }
    ) } const rendered1 = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered1.getByTestId('status')).toHaveTextContent('error') expect(rendered1.getByTestId('error')).toHaveTextContent( 'Simulated 500 error', ) expect(fetchCount).toBe(1) rendered1.unmount() const initialFetchCount = fetchCount const rendered2 = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(rendered2.getByTestId('status')).toHaveTextContent('error') // Should retry because throwOnError returns false (500 error doesn't include '404') expect(fetchCount).toBe(initialFetchCount + 1) expect(queryFn).toHaveBeenCalledTimes(2) }) it('should not fetch for the duration of the restoring period when isRestoring is true', async () => { const key = queryKey() const queryFn = vi .fn() .mockImplementation(() => sleep(10).then(() => 'data')) function Page() { const result = useQuery({ queryKey: key, queryFn, }) return (
    {result.status}
    {result.fetchStatus}
    {result.data ?? 'undefined'}
    ) } const rendered = renderWithClient( queryClient, , ) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByTestId('status')).toHaveTextContent('pending') expect(rendered.getByTestId('fetchStatus')).toHaveTextContent('idle') expect(rendered.getByTestId('data')).toHaveTextContent('undefined') expect(queryFn).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByTestId('status')).toHaveTextContent('pending') expect(rendered.getByTestId('fetchStatus')).toHaveTextContent('idle') expect(rendered.getByTestId('data')).toHaveTextContent('undefined') expect(queryFn).toHaveBeenCalledTimes(0) }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken } from '@tanstack/query-core' import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' describe('useSuspenseInfiniteQuery', () => { it('should always have data defined', () => { const { data } = useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(data).toEqualTypeOf>() }) it('should not allow skipToken in queryFn', () => { assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }), ) assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ) }) it('should not have pending status', () => { const { status } = useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(status).toEqualTypeOf<'error' | 'success'>() }) it('should not allow placeholderData, enabled or throwOnError props', () => { assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2345 placeholderData: 5, enabled: true, }), ) assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2345 enabled: true, }), ) assertType( useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error TS2345 throwOnError: true, }), ) }) it('should not return isPlaceholderData', () => { const query = useSuspenseInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), initialPageParam: 1, getNextPageParam: () => 1, }) expectTypeOf(query).not.toHaveProperty('isPlaceholderData') }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent } from '@testing-library/react' import * as React from 'react' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, skipToken, useSuspenseInfiniteQuery, } from '..' import { renderWithClient } from './utils' import type { InfiniteData, UseSuspenseInfiniteQueryResult } from '..' describe('useSuspenseInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should return the correct states for a successful infinite query', async () => { const key = queryKey() const states: Array>> = [] function Page() { const [multiplier, setMultiplier] = React.useState(1) const state = useSuspenseInfiniteQuery({ queryKey: [`${key}_${multiplier}`], queryFn: ({ pageParam }) => sleep(10).then(() => pageParam * multiplier), initialPageParam: 1, getNextPageParam: (lastPage) => lastPage + 1, }) states.push(state) return (
    data: {state.data?.pages.join(',')}
    ) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: { pages: [1], pageParams: [1] }, status: 'success', }) fireEvent.click(rendered.getByText('next')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() expect(states.length).toBe(2) expect(states[1]).toMatchObject({ data: { pages: [2], pageParams: [1] }, status: 'success', }) }) it('should log an error when skipToken is passed as queryFn', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const key = queryKey() function Page() { useSuspenseInfiniteQuery({ queryKey: key, initialPageParam: 1, getNextPageParam: () => 1, // @ts-expect-error // eslint-disable-next-line react-hooks/purity queryFn: Math.random() >= 0 ? skipToken : () => Promise.resolve(5), }) return null } function App() { return ( ) } renderWithClient(queryClient, ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseInfiniteQuery', ) consoleErrorSpy.mockRestore() }) it('should log an error when skipToken is used in development environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseInfiniteQuery({ queryKey: key, queryFn: skipToken as any, initialPageParam: 1, getNextPageParam: () => 1, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseInfiniteQuery', ) consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should not log an error when skipToken is used in production environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'production' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseInfiniteQuery({ queryKey: key, queryFn: skipToken as any, initialPageParam: 1, getNextPageParam: () => 1, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQueries.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken, useSuspenseQueries } from '..' import { queryOptions } from '../queryOptions' import type { OmitKeyof } from '..' import type { UseQueryOptions, UseSuspenseQueryResult } from '../types' describe('UseSuspenseQueries config object overload', () => { it('TData should always be defined', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = { queryKey: ['key2'], queryFn: () => 'Query Data', } const queryResults = useSuspenseQueries({ queries: [query1, query2] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, }) const queryResults = useSuspenseQueries({ queries: [options] }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data: number) => data > 1, } const queryResults = useSuspenseQueries({ queries: [query1, query2] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data expectTypeOf(query1Data).toEqualTypeOf() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const queryResults = useSuspenseQueries({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should not allow skipToken in queryFn', () => { assertType( useSuspenseQueries({ queries: [ { queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }, ], }), ) assertType( useSuspenseQueries({ queries: [ { queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }), ) }) it('TData should have correct type when conditional skipToken is passed', () => { const queryResults = useSuspenseQueries({ queries: [ { queryKey: ['withSkipToken'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }) const firstResult = queryResults[0] expectTypeOf(firstResult).toEqualTypeOf< UseSuspenseQueryResult >() expectTypeOf(firstResult.data).toEqualTypeOf() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useSuspenseQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) } const queryResults = useCustomQueries() const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf() }) }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = useSuspenseQueries({ queries: [ ...queries1List, { ...Queries2.get(), select(data: boolean) { return data }, }, ], }) expectTypeOf(result).toEqualTypeOf< [ ...Array>, UseSuspenseQueryResult, ] >() }) it('queryOptions with initialData works on useSuspenseQueries', () => { const query1 = queryOptions({ queryKey: ['key1'], queryFn: () => 'Query Data', initialData: 'initial data', }) const queryResults = useSuspenseQueries({ queries: [query1] }) const query1Data = queryResults[0].data expectTypeOf(query1Data).toEqualTypeOf() }) it('queryOptions with skipToken in queryFn should not work on useSuspenseQueries', () => { assertType( useSuspenseQueries({ queries: [ // @ts-expect-error queryOptions({ queryKey: ['key1'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ], }), ) assertType( useSuspenseQueries({ queries: [ // @ts-expect-error queryOptions({ queryKey: ['key1'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), initialData: 5, }), ], }), ) }) it('should not show type error when using rest queryOptions', () => { assertType( useSuspenseQueries({ queries: [ { ...queryOptions({ queryKey: ['key1'], queryFn: () => 'Query Data', }), select(data: string) { return data }, }, ], }), ) }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQueries.test.tsx ================================================ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from 'vitest' import { act, fireEvent, render } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, skipToken, useSuspenseQueries, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' import type { UseSuspenseQueryOptions } from '..' type NumberQueryOptions = UseSuspenseQueryOptions const QUERY_DURATION = 1000 const createQuery: (id: number) => NumberQueryOptions = (id) => ({ queryKey: [id], queryFn: () => sleep(QUERY_DURATION).then(() => id), }) const resolveQueries = () => vi.advanceTimersByTimeAsync(QUERY_DURATION) const queryClient = new QueryClient() describe('useSuspenseQueries', () => { const onSuspend = vi.fn() const onQueriesResolution = vi.fn() beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) afterEach(() => { queryClient.clear() onSuspend.mockClear() onQueriesResolution.mockClear() }) function SuspenseFallback() { React.useEffect(() => { onSuspend() }, []) return
    loading
    } const withSuspenseWrapper = (Component: React.FC) => { function SuspendedComponent(props: T) { return ( }> ) } return SuspendedComponent } function QueriesContainer({ queries, }: { queries: Array }) { const queriesResults = useSuspenseQueries( { queries, combine: (results) => results.map((r) => r.data) }, queryClient, ) React.useEffect(() => { onQueriesResolution(queriesResults) }, [queriesResults]) return null } const TestComponent = withSuspenseWrapper(QueriesContainer) it('should suspend on mount', () => { render() expect(onSuspend).toHaveBeenCalledOnce() }) it('should resolve queries', async () => { render() await act(resolveQueries) expect(onQueriesResolution).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenLastCalledWith([1, 2]) }) it('should not suspend on mount if query has been already fetched', () => { const query = createQuery(1) queryClient.setQueryData(query.queryKey, query.queryFn) render() expect(onSuspend).not.toHaveBeenCalled() }) it('should not break suspense when queries change without resolving', async () => { const initQueries = [1, 2].map(createQuery) const nextQueries = [3, 4, 5, 6].map(createQuery) const { rerender } = render() rerender() await act(resolveQueries) expect(onSuspend).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6]) }) it('should suspend only once per queries change', async () => { const initQueries = [1, 2].map(createQuery) const nextQueries = [3, 4, 5, 6].map(createQuery) const { rerender } = render() await act(resolveQueries) rerender() await act(resolveQueries) expect(onSuspend).toHaveBeenCalledTimes(2) expect(onQueriesResolution).toHaveBeenCalledTimes(2) expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6]) }) it('should only call combine after resolving', async () => { const spy = vi.fn() const key = queryKey() function Page() { const data = useSuspenseQueries({ queries: [1, 2, 3].map((value) => ({ queryKey: [...key, { value }], queryFn: () => sleep(value * 10).then(() => ({ value: value * 10 })), })), combine: (result) => { spy(result) return 'data' }, }) return

    {data}

    } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() expect(spy).not.toHaveBeenCalled() await act(() => vi.advanceTimersByTimeAsync(30)) expect(rendered.getByText('data')).toBeInTheDocument() expect(spy).toHaveBeenCalled() }) it('should handle duplicate query keys without infinite loops', async () => { const key = queryKey() const localDuration = 10 let renderCount = 0 function getUserData() { return { queryKey: key, queryFn: async () => { await sleep(localDuration) return { name: 'John Doe', age: 50 } }, } } function getName() { return { ...getUserData(), select: (data: any) => data.name, } } function getAge() { return { ...getUserData(), select: (data: any) => data.age, } } function App() { renderCount++ const [{ data }, { data: data2 }] = useSuspenseQueries({ queries: [getName(), getAge()], }) React.useEffect(() => { onQueriesResolution({ data, data2 }) }, [data, data2]) return (

    Data

    {JSON.stringify({ data }, null, 2)} {JSON.stringify({ data2 }, null, 2)}
    ) } renderWithClient( queryClient, }> , ) await act(() => vi.advanceTimersByTimeAsync(localDuration)) expect(onSuspend).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(100) expect(onQueriesResolution).toHaveBeenCalledTimes(1) expect(onQueriesResolution).toHaveBeenLastCalledWith({ data: 'John Doe', data2: 50, }) // With the infinite loop bug, renderCount would be very high (e.g. > 100) // Without bug, it should be small (initial suspend + resolution = 2-3) expect(renderCount).toBeLessThan(10) }) }) describe('useSuspenseQueries 2', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should suspend all queries in parallel', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] function Fallback() { results.push('loading') return
    loading
    } function Page() { const result = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(10).then(() => { results.push('1') return '1' }), }, { queryKey: key2, queryFn: () => sleep(20).then(() => { results.push('2') return '2' }), }, ], }) return (

    data: {result.map((item) => item.data ?? 'null').join(',')}

    ) } const rendered = renderWithClient( queryClient, }> , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(20)) expect(rendered.getByText('data: 1,2')).toBeInTheDocument() expect(results).toEqual(['loading', '1', '2']) }) it("shouldn't unmount before all promises fetched", async () => { const key1 = queryKey() const key2 = queryKey() const results: Array = [] const refs: Array = [] function Fallback() { results.push('loading') return
    loading
    } function Page() { // eslint-disable-next-line react-hooks/purity const ref = React.useRef(Math.random()) const result = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(10).then(() => { refs.push(ref.current) results.push('1') return '1' }), }, { queryKey: key2, queryFn: () => sleep(20).then(() => { refs.push(ref.current) results.push('2') return '2' }), }, ], }) return (

    data: {result.map((item) => item.data ?? 'null').join(',')}

    ) } const rendered = renderWithClient( queryClient, }> , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(20)) expect(rendered.getByText('data: 1,2')).toBeInTheDocument() expect(refs.length).toBe(2) expect(refs[0]).toBe(refs[1]) }) // this addresses the following issue: // https://github.com/TanStack/query/issues/6344 it('should suspend on offline when query changes, and data should not be undefined', async () => { function Page() { const [id, setId] = React.useState(0) const { data } = useSuspenseQuery({ queryKey: [id], queryFn: () => sleep(10).then(() => `Data ${id}`), }) // defensive guard here if (data === undefined) { throw new Error('data cannot be undefined') } return ( <>
    {data}
    ) } const rendered = renderWithClient( queryClient, loading}> , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('Data 0')).toBeInTheDocument() // go offline document.dispatchEvent(new CustomEvent('offline')) fireEvent.click(rendered.getByText('fetch')) expect(rendered.getByText('Data 0')).toBeInTheDocument() // go back online document.dispatchEvent(new CustomEvent('online')) fireEvent.click(rendered.getByText('fetch')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) // query should resume expect(rendered.getByText('Data 1')).toBeInTheDocument() }) it('should throw error when queryKey changes and new query fails', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { const [fail, setFail] = React.useState(false) const { data } = useSuspenseQuery({ queryKey: [key, fail], queryFn: () => sleep(10).then(() => { if (fail) throw new Error('Suspense Error Bingo') return 'data' }), retry: 0, }) return (
    rendered: {data}
    ) } const rendered = renderWithClient( queryClient,
    error boundary
    }>
    , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered: data')).toBeInTheDocument() fireEvent.click(rendered.getByText('trigger fail')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should keep previous data when wrapped in a transition', async () => { const key = queryKey() function Page() { const [count, setCount] = React.useState(0) const [isPending, startTransition] = React.useTransition() const { data } = useSuspenseQuery({ queryKey: [key, count], queryFn: () => sleep(10).then(() => 'data' + count), }) return (
    {isPending ? 'pending' : data}
    ) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data0')).toBeInTheDocument() fireEvent.click(rendered.getByText('inc')) expect(rendered.getByText('pending')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data1')).toBeInTheDocument() }) it('should not request old data inside transitions (issue #6486)', async () => { const key = queryKey() let queryFnCount = 0 function App() { const [count, setCount] = React.useState(0) return (
    ) } function Page({ count }: { count: number }) { const { data } = useSuspenseQuery({ queryKey: [key, count], queryFn: () => sleep(10).then(() => { queryFnCount++ return 'data' + count }), }) return (
    {String(data)}
    ) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data0')).toBeInTheDocument() fireEvent.click(rendered.getByText('inc')) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data1')).toBeInTheDocument() expect(queryFnCount).toBe(2) }) it('should still suspense if queryClient has placeholderData config', async () => { const key = queryKey() const queryClientWithPlaceholder = new QueryClient({ defaultOptions: { queries: { placeholderData: (previousData: any) => previousData, }, }, }) function Page() { const [count, setCount] = React.useState(0) const [isPending, startTransition] = React.useTransition() const { data } = useSuspenseQuery({ queryKey: [key, count], queryFn: () => sleep(10).then(() => 'data' + count), }) return (
    {isPending ? 'pending' : data}
    ) } const rendered = renderWithClient( queryClientWithPlaceholder, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data0')).toBeInTheDocument() fireEvent.click(rendered.getByText('inc')) expect(rendered.getByText('pending')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data1')).toBeInTheDocument() }) it('should show error boundary even with gcTime:0 (#7853)', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let count = 0 function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { count++ throw new Error('Query failed') }), gcTime: 0, retry: false, }) return null } function App() { return ( { return
    There was an error!
    }} >
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('There was an error!')).toBeInTheDocument() expect(count).toBe(1) consoleMock.mockRestore() }) describe('gc (with fake timers)', () => { beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) it('should gc when unmounted while fetching with low gcTime (#8159)', async () => { const key = queryKey() function Page() { return ( ) } function Component() { const { data } = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(3000).then(() => 'data'), gcTime: 1000, }) return
    {data}
    } function Page2() { return
    page2
    } function App() { const [show, setShow] = React.useState(true) return (
    {show ? : }
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() fireEvent.click(rendered.getByText('hide')) expect(rendered.getByText('page2')).toBeInTheDocument() // wait for query to be resolved await vi.advanceTimersByTimeAsync(3000) expect(queryClient.getQueryData(key)).toBe('data') // wait for gc await vi.advanceTimersByTimeAsync(1000) expect(queryClient.getQueryData(key)).toBe(undefined) }) }) it('should log an error when skipToken is passed as queryFn', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const key = queryKey() function Page() { useSuspenseQueries({ queries: [ { queryKey: key, // @ts-expect-error // eslint-disable-next-line react-hooks/purity queryFn: Math.random() >= 0 ? skipToken : () => Promise.resolve(5), }, ], }) return null } function App() { return ( ) } renderWithClient(queryClient, ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQueries', ) consoleErrorSpy.mockRestore() }) it('should log an error when skipToken is used in development environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQueries({ queries: [ { queryKey: key, queryFn: skipToken as any, }, ], }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQueries', ) consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should not log an error when skipToken is used in production environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'production' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQueries({ queries: [ { queryKey: key, queryFn: skipToken as any, }, ], }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should only suspend queries that are pending when the slower query already has data', async () => { const key1 = queryKey() const key2 = queryKey() queryClient.setQueryData(key1, 'cached') function Page() { const [result1, result2] = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(2000).then(() => 'data1'), }, { queryKey: key2, queryFn: () => sleep(1000).then(() => 'data2'), }, ], }) return (
    data1: {result1.data}
    data2: {result2.data}
    ) } const rendered = renderWithClient( queryClient, loading}> , ) expect(rendered.getByText('loading')).toBeInTheDocument() // key2 resolves: suspend lifts, key1 shows cached data, key2 shows fresh data await act(() => vi.advanceTimersByTimeAsync(1000)) expect(rendered.getByText('data1: cached')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() // key1 stale timer fires, triggering background refetch await vi.advanceTimersByTimeAsync(1000) // key1 background refetch completes: key1 updates to fresh data await vi.advanceTimersByTimeAsync(2000) expect(rendered.getByText('data1: data1')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() }) it('should only suspend queries that are pending when the faster query already has data', async () => { const key1 = queryKey() const key2 = queryKey() queryClient.setQueryData(key2, 'cached') function Page() { const [result1, result2] = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(2000).then(() => 'data1'), }, { queryKey: key2, queryFn: () => sleep(1000).then(() => 'data2'), }, ], }) return (
    data1: {result1.data}
    data2: {result2.data}
    ) } const rendered = renderWithClient( queryClient, loading}> , ) expect(rendered.getByText('loading')).toBeInTheDocument() // key1 resolves: suspend lifts, key1 shows fresh data, key2 shows cached data await act(() => vi.advanceTimersByTimeAsync(2000)) expect(rendered.getByText('data1: data1')).toBeInTheDocument() expect(rendered.getByText('data2: cached')).toBeInTheDocument() // key2 stale timer fires, triggering background refetch await vi.advanceTimersByTimeAsync(1000) // key2 background refetch completes: key2 updates to fresh data await vi.advanceTimersByTimeAsync(1000) expect(rendered.getByText('data1: data1')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() }) it('should not suspend and not refetch when all queries have fresh cached data', () => { const key1 = queryKey() const key2 = queryKey() queryClient.setQueryData(key1, 'cached1') queryClient.setQueryData(key2, 'cached2') const queryFn1 = vi.fn(() => sleep(20).then(() => 'data1')) const queryFn2 = vi.fn(() => sleep(10).then(() => 'data2')) function Page() { const [result1, result2] = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: queryFn1, }, { queryKey: key2, queryFn: queryFn2, }, ], }) return (
    data1: {result1.data}
    data2: {result2.data}
    ) } const rendered = renderWithClient( queryClient, loading}> , ) // No suspend, fresh cached data shown immediately expect(rendered.getByText('data1: cached1')).toBeInTheDocument() expect(rendered.getByText('data2: cached2')).toBeInTheDocument() // No background refetch because data is still fresh (within staleTime) expect(queryFn1).toHaveBeenCalledTimes(0) expect(queryFn2).toHaveBeenCalledTimes(0) }) it('should not suspend but refetch when all queries have stale cached data', async () => { const key1 = queryKey() const key2 = queryKey() queryClient.setQueryData(key1, 'cached1') queryClient.setQueryData(key2, 'cached2') // Advance past staleTime (min 1000ms in suspense) so data becomes stale before mount vi.advanceTimersByTime(1000) function Page() { const [result1, result2] = useSuspenseQueries({ queries: [ { queryKey: key1, queryFn: () => sleep(20).then(() => 'data1'), }, { queryKey: key2, queryFn: () => sleep(10).then(() => 'data2'), }, ], }) return (
    data1: {result1.data}
    data2: {result2.data}
    ) } const rendered = renderWithClient( queryClient, loading}> , ) // No suspend, stale cached data shown immediately with background refetch started expect(rendered.getByText('data1: cached1')).toBeInTheDocument() expect(rendered.getByText('data2: cached2')).toBeInTheDocument() // key2 background refetch completes await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data1: cached1')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() // key1 background refetch completes await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data1: data1')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQuery.test-d.tsx ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken } from '@tanstack/query-core' import { useSuspenseQuery } from '../useSuspenseQuery' describe('useSuspenseQuery', () => { it('should always have data defined', () => { const { data } = useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(data).toEqualTypeOf() }) it('should not have pending status', () => { const { status } = useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(status).toEqualTypeOf<'error' | 'success'>() }) it('should not allow skipToken in queryFn', () => { assertType( useSuspenseQuery({ queryKey: ['key'], // @ts-expect-error queryFn: skipToken, }), ) assertType( useSuspenseQuery({ queryKey: ['key'], // @ts-expect-error queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ) }) it('should not allow placeholderData, enabled or throwOnError props', () => { assertType( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 placeholderData: 5, enabled: true, }), ) assertType( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 enabled: true, }), ) assertType( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error TS2345 throwOnError: true, }), ) }) it('should not return isPlaceholderData', () => { expectTypeOf( useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }), ).not.toHaveProperty('isPlaceholderData') }) it('should type-narrow the error field', () => { const query = useSuspenseQuery({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) if (query.status === 'error') { expectTypeOf(query.error).toEqualTypeOf() } }) }) ================================================ FILE: packages/react-query/src/__tests__/useSuspenseQuery.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, QueryErrorResetBoundary, skipToken, useQueryErrorResetBoundary, useSuspenseInfiniteQuery, useSuspenseQuery, } from '..' import { renderWithClient } from './utils' import type { InfiniteData, UseSuspenseInfiniteQueryResult, UseSuspenseQueryResult, } from '..' describe('useSuspenseQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) it('should render the correct amount of times in Suspense mode', async () => { const key = queryKey() const states: Array> = [] let count = 0 let renders = 0 function Page() { renders++ const [stateKey, setStateKey] = React.useState(key) const state = useSuspenseQuery({ queryKey: stateKey, queryFn: () => sleep(10).then(() => ++count), }) states.push(state) return (
    ) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() expect(renders).toBe(6) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 1, status: 'success' }) expect(states[1]).toMatchObject({ data: 2, status: 'success' }) }) it('should return the correct states for a successful infinite query', async () => { const key = queryKey() const states: Array>> = [] function Page() { const [multiplier, setMultiplier] = React.useState(1) const state = useSuspenseInfiniteQuery({ queryKey: [`${key}_${multiplier}`], queryFn: ({ pageParam }) => sleep(10).then(() => pageParam * multiplier), initialPageParam: 1, getNextPageParam: (lastPage) => lastPage + 1, }) states.push(state) return (
    data: {state.data?.pages.join(',')}
    ) } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: { pages: [1], pageParams: [1] }, status: 'success', }) fireEvent.click(rendered.getByText('next')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() expect(states.length).toBe(2) expect(states[1]).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() { useSuspenseQuery({ queryKey: [key], queryFn }) return <>rendered } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() expect(queryFn).toHaveBeenCalledTimes(1) }) it('should remove query instance when component unmounted', async () => { const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => 'data'), }) return <>rendered } function App() { const [show, setShow] = React.useState(false) return ( <> {show && } )} > )} , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(70)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(rendered.getByText('retry')).toBeInTheDocument() fireEvent.click(rendered.getByText('retry')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) return
    rendered
    } const rendered = renderWithClient( queryClient, {({ reset }) => ( (
    error boundary
    )} >
    )}
    , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => 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 act(() => 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 act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() consoleMock.mockRestore() }) it('should set staleTime when having passed a function', async () => { const key = queryKey() let count = 0 function Component() { const result = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ++count), staleTime: () => 60 * 1000, }) return (
    data: {result.data}
    ) } function Page() { return ( ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() expect( typeof queryClient.getQueryCache().find({ queryKey: key })?.observers[0] ?.options.staleTime, ).toBe('function') }) it('should suspend when switching to a new query', async () => { const key1 = queryKey() const key2 = queryKey() function Component(props: { queryKey: Array }) { const result = useSuspenseQuery({ queryKey: props.queryKey, queryFn: () => sleep(10).then(() => props.queryKey), retry: false, }) return
    data: {result.data}
    } function Page() { const [key, setKey] = React.useState(key1) return (
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText(`data: ${key1}`)).toBeInTheDocument() fireEvent.click(rendered.getByText('switch')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText(`data: ${key2}`)).toBeInTheDocument() }) it('should retry fetch if the reset error boundary has been reset with global hook', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = false function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) return
    rendered
    } function App() { const { reset } = useQueryErrorResetBoundary() return ( (
    error boundary
    )} >
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => 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 act(() => 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 act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw errors to the error boundary by default', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => Promise.reject(new Error('Suspense Error a1x'))), retry: false, }) return
    rendered
    } function App() { return ( (
    error boundary
    )} >
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should throw select errors to the error boundary by default', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ({ a: { b: 'c' } })), select: () => { throw new Error('foo') }, }) return
    rendered
    } function App() { return ( (
    error boundary
    )} >
    ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() consoleMock.mockRestore() }) it('should error caught in error boundary without infinite loop', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() let succeed = true function Page() { const [nonce] = React.useState(0) const queryKeys = [`${key}-${succeed}`] const result = useSuspenseQuery({ queryKey: queryKeys, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return nonce }), retry: false, }) return (
    rendered {result.data}
    ) } function App() { const { reset } = useQueryErrorResetBoundary() return (
    error boundary
    } >
    ) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (loading) expect(rendered.getByText('loading')).toBeInTheDocument() // resolve promise -> render Page (rendered) await act(() => 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 act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) consoleMock.mockRestore() }) it('should error caught in error boundary without infinite loop when query keys changed', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let succeed = true function Page() { const [key, rerender] = React.useReducer((x) => x + 1, 0) const queryKeys = [key, succeed] const result = useSuspenseQuery({ queryKey: queryKeys, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) if (result.error) { throw result.error } return (
    rendered {result.data}
    ) } function App() { const { reset } = useQueryErrorResetBoundary() return (
    error boundary
    } >
    ) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (loading) expect(rendered.getByText('loading')).toBeInTheDocument() // resolve promise -> render Page (rendered) await act(() => 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 act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('error boundary')).toBeInTheDocument() expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual( new Error('Suspense Error Bingo'), ) 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: UseSuspenseQueryResult | null = null let count = 0 let renders = 0 function Page() { renders++ state = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ++count), gcTime: 0, }) return (
    rendered
    ) } const rendered = renderWithClient( queryClient, , ) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered')).toBeInTheDocument() expect(state).toMatchObject({ data: 1, status: 'success', }) expect(renders).toBe(3) }) it('should not throw background errors to the error boundary', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) let succeed = true const key = queryKey() function Page() { const result = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => { if (!succeed) throw new Error('Suspense Error Bingo') return 'data' }), retry: false, }) return (
    rendered {result.data} {result.status}
    ) } function App() { const { reset } = useQueryErrorResetBoundary() return (
    error boundary
    } >
    ) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (loading) expect(rendered.getByText('loading')).toBeInTheDocument() // resolve promise -> render Page (rendered) await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('rendered data success')).toBeInTheDocument() // change promise result to error succeed = false // refetch fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) // we are now in error state but still have data to show await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('rendered data error')).toBeInTheDocument() consoleMock.mockRestore() }) it('should still suspense if queryClient has placeholderData config', async () => { const key = queryKey() const queryClientWithPlaceholder = new QueryClient({ defaultOptions: { queries: { placeholderData: (previousData: any) => previousData, }, }, }) const states: Array> = [] let count = 0 function Page() { const [stateKey, setStateKey] = React.useState(key) const state = useSuspenseQuery({ queryKey: stateKey, queryFn: async () => sleep(10).then(() => ++count), }) states.push(state) return (
    ) } const rendered = renderWithClient( queryClientWithPlaceholder, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 1')).toBeInTheDocument() fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('data: 2')).toBeInTheDocument() }) it('should log an error when skipToken is passed as queryFn', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, // @ts-expect-error // eslint-disable-next-line react-hooks/purity queryFn: Math.random() >= 0 ? skipToken : () => Promise.resolve(5), }) return null } function App() { return ( ) } renderWithClient(queryClient, ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQuery', ) consoleErrorSpy.mockRestore() }) it('should properly refresh data when refetchInterval is set', async () => { const key = queryKey() let count = 0 function Page() { const state = useSuspenseQuery({ queryKey: key, queryFn: () => sleep(10).then(() => ++count), refetchInterval: 10, }) return
    count: {state.data}
    } const rendered = renderWithClient( queryClient, , ) expect(rendered.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10)) expect(rendered.getByText('count: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('count: 2')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('count: 3')).toBeInTheDocument() expect(count).toBeGreaterThanOrEqual(3) }) it('should log an error when skipToken is used in development environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: skipToken as any, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).toHaveBeenCalledWith( 'skipToken is not allowed for useSuspenseQuery', ) consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) it('should not log an error when skipToken is used in production environment', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'production' const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key = queryKey() function Page() { useSuspenseQuery({ queryKey: key, queryFn: skipToken as any, }) return null } renderWithClient( queryClient, , ) expect(consoleErrorSpy).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() process.env.NODE_ENV = envCopy }) }) ================================================ FILE: packages/react-query/src/__tests__/utils.tsx ================================================ import { vi } from 'vitest' import * as React from 'react' import { act, render } from '@testing-library/react' import { environmentManager, isServer } from '@tanstack/query-core' import { QueryClientProvider, onlineManager } from '..' import type { QueryClient } from '..' import type { MockInstance } from 'vitest' export function renderWithClient( client: QueryClient, ui: React.ReactElement, ): ReturnType { const { rerender, ...result } = render( {ui}, ) return { ...result, rerender: (rerenderUi: React.ReactElement) => rerender( {rerenderUi}, ), } as any } export function Blink({ duration, children, }: { duration: number children: React.ReactNode }) { const [shouldShow, setShouldShow] = React.useState(true) React.useEffect(() => { setShouldShow(true) const timeout = setActTimeout(() => setShouldShow(false), duration) return () => { clearTimeout(timeout) } }, [duration, children]) return shouldShow ? <>{children} : <>off } export function mockOnlineManagerIsOnline( value: boolean, ): MockInstance<() => boolean> { return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value) } export function setActTimeout(fn: () => void, ms?: number) { return setTimeout(() => { act(() => { fn() }) }, ms) } export function setIsServer(value: boolean) { environmentManager.setIsServer(() => value) return () => { environmentManager.setIsServer(() => isServer) } } ================================================ FILE: packages/react-query/src/errorBoundaryUtils.ts ================================================ 'use client' import * as React from 'react' import { shouldThrowError } from '@tanstack/query-core' import type { DefaultedQueryObserverOptions, Query, QueryKey, QueryObserverResult, ThrowOnError, } from '@tanstack/query-core' import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary' export const ensurePreventErrorBoundaryRetry = < TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( options: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, errorResetBoundary: QueryErrorResetBoundaryValue, query: Query | undefined, ) => { const throwOnError = query?.state.error && typeof options.throwOnError === 'function' ? shouldThrowError(options.throwOnError, [query.state.error, query]) : options.throwOnError if ( options.suspense || options.experimental_prefetchInRender || throwOnError ) { // Prevent retrying failed query if the error boundary has not been reset yet if (!errorResetBoundary.isReset()) { options.retryOnMount = false } } } export const useClearResetErrorBoundary = ( errorResetBoundary: QueryErrorResetBoundaryValue, ) => { React.useEffect(() => { errorResetBoundary.clearReset() }, [errorResetBoundary]) } export const getHasError = < TData, TError, TQueryFnData, TQueryData, TQueryKey extends QueryKey, >({ result, errorResetBoundary, throwOnError, query, suspense, }: { result: QueryObserverResult errorResetBoundary: QueryErrorResetBoundaryValue throwOnError: ThrowOnError query: Query | undefined suspense: boolean | undefined }) => { return ( result.isError && !errorResetBoundary.isReset() && !result.isFetching && query && ((suspense && result.data === undefined) || shouldThrowError(throwOnError, [result.error, query])) ) } ================================================ FILE: packages/react-query/src/index.ts ================================================ /* istanbul ignore file */ // Re-export core export * from '@tanstack/query-core' // React Query export * from './types' export { useQueries } from './useQueries' export type { QueriesResults, QueriesOptions } from './useQueries' export { useQuery } from './useQuery' export { useSuspenseQuery } from './useSuspenseQuery' export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery' export { useSuspenseQueries } from './useSuspenseQueries' export type { SuspenseQueriesResults, SuspenseQueriesOptions, } from './useSuspenseQueries' export { usePrefetchQuery } from './usePrefetchQuery' export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery' export { queryOptions } from './queryOptions' export type { DefinedInitialDataOptions, UndefinedInitialDataOptions, UnusedSkipTokenOptions, } from './queryOptions' export { infiniteQueryOptions } from './infiniteQueryOptions' export type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, UnusedSkipTokenInfiniteOptions, } from './infiniteQueryOptions' export { QueryClientContext, QueryClientProvider, useQueryClient, } from './QueryClientProvider' export type { QueryClientProviderProps } from './QueryClientProvider' export type { QueryErrorResetBoundaryProps } from './QueryErrorResetBoundary' export { HydrationBoundary } from './HydrationBoundary' export type { HydrationBoundaryProps } from './HydrationBoundary' export type { QueryErrorClearResetFunction, QueryErrorIsResetFunction, QueryErrorResetBoundaryFunction, QueryErrorResetFunction, } from './QueryErrorResetBoundary' export { QueryErrorResetBoundary, useQueryErrorResetBoundary, } from './QueryErrorResetBoundary' export { useIsFetching } from './useIsFetching' export { useIsMutating, useMutationState } from './useMutationState' export { useMutation } from './useMutation' export { mutationOptions } from './mutationOptions' export { useInfiniteQuery } from './useInfiniteQuery' export { useIsRestoring, IsRestoringProvider } from './IsRestoringProvider' ================================================ FILE: packages/react-query/src/infiniteQueryOptions.ts ================================================ import type { DataTag, DefaultError, InfiniteData, InitialDataFunction, NonUndefinedGuard, OmitKeyof, QueryKey, SkipToken, } from '@tanstack/query-core' import type { UseInfiniteQueryOptions } from './types' export type UndefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData?: | undefined | NonUndefinedGuard> | InitialDataFunction< NonUndefinedGuard> > } export type UnusedSkipTokenInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = OmitKeyof< UseInfiniteQueryOptions, 'queryFn' > & { queryFn?: Exclude< UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >['queryFn'], SkipToken | undefined > } export type DefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData: | NonUndefinedGuard> | (() => NonUndefinedGuard>) | undefined } export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { queryKey: DataTag, TError> } export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UnusedSkipTokenInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): UnusedSkipTokenInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { queryKey: DataTag, TError> } export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { queryKey: DataTag, TError> } export function infiniteQueryOptions(options: unknown) { return options } ================================================ FILE: packages/react-query/src/mutationOptions.ts ================================================ import type { DefaultError, WithRequired } from '@tanstack/query-core' import type { UseMutationOptions } from './types' export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: WithRequired< UseMutationOptions, 'mutationKey' >, ): WithRequired< UseMutationOptions, 'mutationKey' > export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: Omit< UseMutationOptions, 'mutationKey' >, ): Omit< UseMutationOptions, 'mutationKey' > export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: UseMutationOptions, ): UseMutationOptions { return options } ================================================ FILE: packages/react-query/src/queryOptions.ts ================================================ import type { DataTag, DefaultError, InitialDataFunction, NonUndefinedGuard, OmitKeyof, QueryFunction, QueryKey, SkipToken, } from '@tanstack/query-core' import type { UseQueryOptions } from './types' export type UndefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions & { initialData?: | undefined | InitialDataFunction> | NonUndefinedGuard } export type UnusedSkipTokenOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< UseQueryOptions, 'queryFn' > & { queryFn?: Exclude< UseQueryOptions['queryFn'], SkipToken | undefined > } export type DefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = Omit, 'queryFn'> & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) queryFn?: QueryFunction } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions, ): DefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UnusedSkipTokenOptions, ): UnusedSkipTokenOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, ): UndefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions(options: unknown) { return options } ================================================ FILE: packages/react-query/src/suspense.ts ================================================ import type { DefaultError, DefaultedQueryObserverOptions, Query, QueryKey, QueryObserver, QueryObserverResult, } from '@tanstack/query-core' import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary' export const defaultThrowOnError = < TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( _error: TError, query: Query, ) => query.state.data === undefined export const ensureSuspenseTimers = ( defaultedOptions: DefaultedQueryObserverOptions, ) => { if (defaultedOptions.suspense) { // Handle staleTime to ensure minimum 1000ms in Suspense mode // This prevents unnecessary refetching when components remount after suspending const MIN_SUSPENSE_TIME_MS = 1000 const clamp = (value: number | 'static' | undefined) => value === 'static' ? value : Math.max(value ?? MIN_SUSPENSE_TIME_MS, MIN_SUSPENSE_TIME_MS) const originalStaleTime = defaultedOptions.staleTime defaultedOptions.staleTime = typeof originalStaleTime === 'function' ? (...args) => clamp(originalStaleTime(...args)) : clamp(originalStaleTime) if (typeof defaultedOptions.gcTime === 'number') { defaultedOptions.gcTime = Math.max( defaultedOptions.gcTime, MIN_SUSPENSE_TIME_MS, ) } } } export const willFetch = ( result: QueryObserverResult, isRestoring: boolean, ) => result.isLoading && result.isFetching && !isRestoring export const shouldSuspend = ( defaultedOptions: | DefaultedQueryObserverOptions | undefined, result: QueryObserverResult, ) => defaultedOptions?.suspense && result.isPending export const fetchOptimistic = < TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( defaultedOptions: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, observer: QueryObserver, errorResetBoundary: QueryErrorResetBoundaryValue, ) => observer.fetchOptimistic(defaultedOptions).catch(() => { errorResetBoundary.clearReset() }) ================================================ FILE: packages/react-query/src/types.ts ================================================ /* istanbul ignore file */ import type { DefaultError, DefinedInfiniteQueryObserverResult, DefinedQueryObserverResult, DistributiveOmit, FetchQueryOptions, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutateFunction, MutationObserverOptions, MutationObserverResult, OmitKeyof, Override, QueryKey, QueryObserverOptions, QueryObserverResult, SkipToken, } from '@tanstack/query-core' export type AnyUseBaseQueryOptions = UseBaseQueryOptions< any, any, any, any, any > export interface UseBaseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > { /** * Set this to `false` to unsubscribe this observer from updates to the query cache. * Defaults to `true`. */ subscribed?: boolean } export interface UsePrefetchQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends OmitKeyof< FetchQueryOptions, 'queryFn' > { queryFn?: Exclude< FetchQueryOptions['queryFn'], SkipToken > } export type AnyUseQueryOptions = UseQueryOptions export interface UseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends OmitKeyof< UseBaseQueryOptions, 'suspense' > {} export type AnyUseSuspenseQueryOptions = UseSuspenseQueryOptions< any, any, any, any > export interface UseSuspenseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > extends OmitKeyof< UseQueryOptions, 'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData' > { queryFn?: Exclude< UseQueryOptions['queryFn'], SkipToken > } export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions< any, any, any, any, any > export interface UseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > extends OmitKeyof< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, 'suspense' > { /** * Set this to `false` to unsubscribe this observer from updates to the query cache. * Defaults to `true`. */ subscribed?: boolean } export type AnyUseSuspenseInfiniteQueryOptions = UseSuspenseInfiniteQueryOptions export interface UseSuspenseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > extends OmitKeyof< UseInfiniteQueryOptions, 'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData' > { queryFn?: Exclude< UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >['queryFn'], SkipToken > } export type UseBaseQueryResult< TData = unknown, TError = DefaultError, > = QueryObserverResult export type UseQueryResult< TData = unknown, TError = DefaultError, > = UseBaseQueryResult export type UseSuspenseQueryResult< TData = unknown, TError = DefaultError, > = DistributiveOmit< DefinedQueryObserverResult, 'isPlaceholderData' | 'promise' > export type DefinedUseQueryResult< TData = unknown, TError = DefaultError, > = DefinedQueryObserverResult export type UseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = InfiniteQueryObserverResult export type DefinedUseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = DefinedInfiniteQueryObserverResult export type UseSuspenseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = OmitKeyof< DefinedInfiniteQueryObserverResult, 'isPlaceholderData' | 'promise' > export type AnyUseMutationOptions = UseMutationOptions export interface UseMutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > extends OmitKeyof< MutationObserverOptions, '_defaulted' > {} export type UseMutateFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( ...args: Parameters< MutateFunction > ) => void export type UseMutateAsyncFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = MutateFunction export type UseBaseMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = Override< MutationObserverResult, { mutate: UseMutateFunction } > & { mutateAsync: UseMutateAsyncFunction< TData, TError, TVariables, TOnMutateResult > } export type UseMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = UseBaseMutationResult ================================================ FILE: packages/react-query/src/useBaseQuery.ts ================================================ 'use client' import * as React from 'react' import { environmentManager, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary' import { ensurePreventErrorBoundaryRetry, getHasError, useClearResetErrorBoundary, } from './errorBoundaryUtils' import { useIsRestoring } from './IsRestoringProvider' import { ensureSuspenseTimers, fetchOptimistic, shouldSuspend, willFetch, } from './suspense' import type { QueryClient, QueryKey, QueryObserver, QueryObserverResult, } from '@tanstack/query-core' import type { UseBaseQueryOptions } from './types' export function useBaseQuery< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( options: UseBaseQueryOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, Observer: typeof QueryObserver, queryClient?: QueryClient, ): QueryObserverResult { if (process.env.NODE_ENV !== 'production') { if (typeof options !== 'object' || Array.isArray(options)) { throw new Error( 'Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. Please use the error stack to find the culprit call. More info here: https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#supports-a-single-signature-one-object', ) } } const isRestoring = useIsRestoring() const errorResetBoundary = useQueryErrorResetBoundary() const client = useQueryClient(queryClient) const defaultedOptions = client.defaultQueryOptions(options) ;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.( defaultedOptions, ) const query = client .getQueryCache() .get< TQueryFnData, TError, TQueryData, TQueryKey >(defaultedOptions.queryHash) if (process.env.NODE_ENV !== 'production') { if (!defaultedOptions.queryFn) { console.error( `[${defaultedOptions.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`, ) } } // Make sure results are optimistically set in fetching state before subscribing or updating options defaultedOptions._optimisticResults = isRestoring ? 'isRestoring' : 'optimistic' ensureSuspenseTimers(defaultedOptions) ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query) useClearResetErrorBoundary(errorResetBoundary) // this needs to be invoked before creating the Observer because that can create a cache entry const isNewCacheEntry = !client .getQueryCache() .get(defaultedOptions.queryHash) const [observer] = React.useState( () => new Observer( client, defaultedOptions, ), ) // note: this must be called before useSyncExternalStore const result = observer.getOptimisticResult(defaultedOptions) const shouldSubscribe = !isRestoring && options.subscribed !== false React.useSyncExternalStore( React.useCallback( (onStoreChange) => { const unsubscribe = shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop // Update result to make sure we did not miss any query updates // between creating the observer and subscribing to it. observer.updateResult() return unsubscribe }, [observer, shouldSubscribe], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), ) React.useEffect(() => { observer.setOptions(defaultedOptions) }, [defaultedOptions, observer]) // Handle suspense if (shouldSuspend(defaultedOptions, result)) { throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary) } // Handle error boundary if ( getHasError({ result, errorResetBoundary, throwOnError: defaultedOptions.throwOnError, query, suspense: defaultedOptions.suspense, }) ) { throw result.error } ;(client.getDefaultOptions().queries as any)?._experimental_afterQuery?.( defaultedOptions, result, ) if ( defaultedOptions.experimental_prefetchInRender && !environmentManager.isServer() && willFetch(result, isRestoring) ) { const promise = isNewCacheEntry ? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted fetchOptimistic(defaultedOptions, observer, errorResetBoundary) : // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in query?.promise promise?.catch(noop).finally(() => { // `.updateResult()` will trigger `.#currentThenable` to finalize observer.updateResult() }) } // Handle result property usage tracking return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result) : result } ================================================ FILE: packages/react-query/src/useInfiniteQuery.ts ================================================ 'use client' import { InfiniteQueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefaultError, InfiniteData, QueryClient, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { DefinedUseInfiniteQueryResult, UseInfiniteQueryOptions, UseInfiniteQueryResult, } from './types' import type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): DefinedUseInfiniteQueryResult export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryResult export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseInfiniteQueryResult export function useInfiniteQuery( options: UseInfiniteQueryOptions, queryClient?: QueryClient, ) { return useBaseQuery( options, InfiniteQueryObserver as typeof QueryObserver, queryClient, ) } ================================================ FILE: packages/react-query/src/useIsFetching.ts ================================================ 'use client' import * as React from 'react' import { notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { QueryClient, QueryFilters } from '@tanstack/query-core' export function useIsFetching( filters?: QueryFilters, queryClient?: QueryClient, ): number { const client = useQueryClient(queryClient) const queryCache = client.getQueryCache() return React.useSyncExternalStore( React.useCallback( (onStoreChange) => queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), [queryCache], ), () => client.isFetching(filters), () => client.isFetching(filters), ) } ================================================ FILE: packages/react-query/src/useMutation.ts ================================================ 'use client' import * as React from 'react' import { MutationObserver, noop, notifyManager, shouldThrowError, } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { UseMutateFunction, UseMutationOptions, UseMutationResult, } from './types' import type { DefaultError, QueryClient } from '@tanstack/query-core' // HOOK export function useMutation< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: UseMutationOptions, queryClient?: QueryClient, ): UseMutationResult { const client = useQueryClient(queryClient) const [observer] = React.useState( () => new MutationObserver( client, options, ), ) React.useEffect(() => { observer.setOptions(options) }, [observer, options]) const result = React.useSyncExternalStore( React.useCallback( (onStoreChange) => observer.subscribe(notifyManager.batchCalls(onStoreChange)), [observer], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), ) const mutate = React.useCallback< UseMutateFunction >( (variables, mutateOptions) => { observer.mutate(variables, mutateOptions).catch(noop) }, [observer], ) if ( result.error && shouldThrowError(observer.options.throwOnError, [result.error]) ) { throw result.error } return { ...result, mutate, mutateAsync: result.mutate } } ================================================ FILE: packages/react-query/src/useMutationState.ts ================================================ 'use client' import * as React from 'react' import { notifyManager, replaceEqualDeep } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { Mutation, MutationCache, MutationFilters, MutationState, QueryClient, } from '@tanstack/query-core' export function useIsMutating( filters?: MutationFilters, queryClient?: QueryClient, ): number { const client = useQueryClient(queryClient) return useMutationState( { filters: { ...filters, status: 'pending' } }, client, ).length } type MutationStateOptions = { filters?: MutationFilters select?: (mutation: Mutation) => TResult } function getResult( mutationCache: MutationCache, options: MutationStateOptions, ): Array { return mutationCache .findAll(options.filters) .map( (mutation): TResult => (options.select ? options.select(mutation) : mutation.state) as TResult, ) } export function useMutationState( options: MutationStateOptions = {}, queryClient?: QueryClient, ): Array { const mutationCache = useQueryClient(queryClient).getMutationCache() const optionsRef = React.useRef(options) const result = React.useRef>(null) if (result.current === null) { result.current = getResult(mutationCache, options) } React.useEffect(() => { optionsRef.current = options }) return React.useSyncExternalStore( React.useCallback( (onStoreChange) => mutationCache.subscribe(() => { const nextResult = replaceEqualDeep( result.current, getResult(mutationCache, optionsRef.current), ) if (result.current !== nextResult) { result.current = nextResult notifyManager.schedule(onStoreChange) } }), [mutationCache], ), () => result.current, () => result.current, )! } ================================================ FILE: packages/react-query/src/usePrefetchInfiniteQuery.tsx ================================================ import { useQueryClient } from './QueryClientProvider' import type { DefaultError, FetchInfiniteQueryOptions, QueryClient, QueryKey, } from '@tanstack/query-core' export function usePrefetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { client.prefetchInfiniteQuery(options) } } ================================================ FILE: packages/react-query/src/usePrefetchQuery.tsx ================================================ import { useQueryClient } from './QueryClientProvider' import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' import type { UsePrefetchQueryOptions } from './types' export function usePrefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UsePrefetchQueryOptions, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { client.prefetchQuery(options) } } ================================================ FILE: packages/react-query/src/useQueries.ts ================================================ 'use client' import * as React from 'react' import { QueriesObserver, QueryObserver, noop, notifyManager, } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import { useIsRestoring } from './IsRestoringProvider' import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary' import { ensurePreventErrorBoundaryRetry, getHasError, useClearResetErrorBoundary, } from './errorBoundaryUtils' import { ensureSuspenseTimers, fetchOptimistic, shouldSuspend, } from './suspense' import type { DefinedUseQueryResult, UseQueryOptions, UseQueryResult, } from './types' import type { DefaultError, OmitKeyof, QueriesObserverOptions, QueriesPlaceholderDataFunction, QueryClient, QueryFunction, QueryKey, QueryObserverOptions, ThrowOnError, } from '@tanstack/query-core' // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function always gets undefined passed type UseQueryOptionsForUseQueries< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< UseQueryOptions, 'placeholderData' | 'subscribed' > & { placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction } // 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 GetUseQueryOptionsForUseQueries = // 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 : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseQueryOptionsForUseQueries : T extends { data: infer TData; error?: infer TError } ? UseQueryOptionsForUseQueries : // 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 : T extends [infer TQueryFnData, infer TError] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData] ? UseQueryOptionsForUseQueries : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseQueryOptionsForUseQueries< TQueryFnData, unknown extends TError ? DefaultError : TError, unknown extends TData ? TQueryFnData : TData, TQueryKey > : // Fallback UseQueryOptionsForUseQueries // A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult type GetDefinedOrUndefinedQueryResult = T extends { initialData?: infer TInitialData } ? unknown extends TInitialData ? UseQueryResult : TInitialData extends TData ? DefinedUseQueryResult : TInitialData extends () => infer TInitialDataResult ? unknown extends TInitialDataResult ? UseQueryResult : TInitialDataResult extends TData ? DefinedUseQueryResult : UseQueryResult : UseQueryResult : UseQueryResult type GetUseQueryResult = // Part 1: responsible for mapping explicit type parameter to function result, if object T extends { queryFnData: any; error?: infer TError; data: infer TData } ? GetDefinedOrUndefinedQueryResult : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? GetDefinedOrUndefinedQueryResult : T extends { data: infer TData; error?: infer TError } ? GetDefinedOrUndefinedQueryResult : // Part 2: responsible for mapping explicit type parameter to function result, if tuple T extends [any, infer TError, infer TData] ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData, infer TError] ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData] ? GetDefinedOrUndefinedQueryResult : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : // Fallback UseQueryResult /** * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type QueriesOptions< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueries] : T extends [infer Head, ...infer Tails] ? QueriesOptions< [...Tails], [...TResults, GetUseQueryOptionsForUseQueries], [...TDepth, 1] > : ReadonlyArray extends T ? T : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous 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 /** * QueriesResults reducer recursively maps type param to results */ export type QueriesResults< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryResult] : T extends [infer Head, ...infer Tails] ? QueriesResults< [...Tails], [...TResults, GetUseQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetUseQueryResult } export function useQueries< T extends Array, TCombinedResult = QueriesResults, >( { queries, ...options }: { queries: | readonly [...QueriesOptions] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }] combine?: (result: QueriesResults) => TCombinedResult subscribed?: boolean }, queryClient?: QueryClient, ): TCombinedResult { const client = useQueryClient(queryClient) const isRestoring = useIsRestoring() const errorResetBoundary = useQueryErrorResetBoundary() const defaultedQueries = React.useMemo( () => queries.map((opts) => { const defaultedOptions = client.defaultQueryOptions( opts as QueryObserverOptions, ) // Make sure the results are already in fetching state before subscribing or updating options defaultedOptions._optimisticResults = isRestoring ? 'isRestoring' : 'optimistic' return defaultedOptions }), [queries, client, isRestoring], ) defaultedQueries.forEach((queryOptions) => { ensureSuspenseTimers(queryOptions) const query = client.getQueryCache().get(queryOptions.queryHash) ensurePreventErrorBoundaryRetry(queryOptions, errorResetBoundary, query) }) useClearResetErrorBoundary(errorResetBoundary) const [observer] = React.useState( () => new QueriesObserver( client, defaultedQueries, options as QueriesObserverOptions, ), ) // note: this must be called before useSyncExternalStore const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, (options as QueriesObserverOptions).combine, ) const shouldSubscribe = !isRestoring && options.subscribed !== false React.useSyncExternalStore( React.useCallback( (onStoreChange) => shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop, [observer, shouldSubscribe], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), ) React.useEffect(() => { observer.setQueries( defaultedQueries, options as QueriesObserverOptions, ) }, [defaultedQueries, options, observer]) const shouldAtLeastOneSuspend = optimisticResult.some((result, index) => shouldSuspend(defaultedQueries[index], result), ) const suspensePromises = shouldAtLeastOneSuspend ? optimisticResult.flatMap((result, index) => { const opts = defaultedQueries[index] if (opts && shouldSuspend(opts, result)) { const queryObserver = new QueryObserver(client, opts) return fetchOptimistic(opts, queryObserver, errorResetBoundary) } return [] }) : [] if (suspensePromises.length > 0) { throw Promise.all(suspensePromises) } const firstSingleResultWhichShouldThrow = optimisticResult.find( (result, index) => { const query = defaultedQueries[index] return ( query && getHasError({ result, errorResetBoundary, throwOnError: query.throwOnError, query: client.getQueryCache().get(query.queryHash), suspense: query.suspense, }) ) }, ) if (firstSingleResultWhichShouldThrow?.error) { throw firstSingleResultWhichShouldThrow.error } return getCombinedResult(trackResult()) } ================================================ FILE: packages/react-query/src/useQuery.ts ================================================ 'use client' import { QueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefaultError, NoInfer, QueryClient, QueryKey, } from '@tanstack/query-core' import type { DefinedUseQueryResult, UseQueryOptions, UseQueryResult, } from './types' import type { DefinedInitialDataOptions, UndefinedInitialDataOptions, } from './queryOptions' export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions, queryClient?: QueryClient, ): DefinedUseQueryResult, TError> export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, queryClient?: QueryClient, ): UseQueryResult, TError> export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UseQueryOptions, queryClient?: QueryClient, ): UseQueryResult, TError> export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) { return useBaseQuery(options, QueryObserver, queryClient) } ================================================ FILE: packages/react-query/src/useSuspenseInfiniteQuery.ts ================================================ 'use client' import { InfiniteQueryObserver, skipToken } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import { defaultThrowOnError } from './suspense' import type { DefaultError, InfiniteData, InfiniteQueryObserverSuccessResult, QueryClient, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { UseSuspenseInfiniteQueryOptions, UseSuspenseInfiniteQueryResult, } from './types' export function useSuspenseInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UseSuspenseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, queryClient?: QueryClient, ): UseSuspenseInfiniteQueryResult { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseInfiniteQuery') } } return useBaseQuery( { ...options, enabled: true, suspense: true, throwOnError: defaultThrowOnError, }, InfiniteQueryObserver as typeof QueryObserver, queryClient, ) as InfiniteQueryObserverSuccessResult } ================================================ FILE: packages/react-query/src/useSuspenseQueries.ts ================================================ 'use client' import { skipToken } from '@tanstack/query-core' import { useQueries } from './useQueries' import { defaultThrowOnError } from './suspense' import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' import type { DefaultError, QueryClient, QueryFunction, ThrowOnError, } from '@tanstack/query-core' // 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 GetUseSuspenseQueryOptions = // 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 } ? UseSuspenseQueryOptions : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseSuspenseQueryOptions : T extends { data: infer TData; error?: infer TError } ? UseSuspenseQueryOptions : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] ? UseSuspenseQueryOptions : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryOptions : T extends [infer TQueryFnData] ? UseSuspenseQueryOptions : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseSuspenseQueryOptions< TQueryFnData, TError, TData, TQueryKey > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? UseSuspenseQueryOptions< TQueryFnData, TError, TQueryFnData, TQueryKey > : // Fallback UseSuspenseQueryOptions type GetUseSuspenseQueryResult = // Part 1: responsible for mapping explicit type parameter to function result, if object T extends { queryFnData: any; error?: infer TError; data: infer TData } ? UseSuspenseQueryResult : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseSuspenseQueryResult : T extends { data: infer TData; error?: infer TError } ? UseSuspenseQueryResult : // Part 2: responsible for mapping explicit type parameter to function result, if tuple T extends [any, infer TError, infer TData] ? UseSuspenseQueryResult : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryResult : T extends [infer TQueryFnData] ? UseSuspenseQueryResult : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseSuspenseQueryResult< unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? UseSuspenseQueryResult< TQueryFnData, unknown extends TError ? DefaultError : TError > : // Fallback UseSuspenseQueryResult /** * SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type SuspenseQueriesOptions< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryOptions] : T extends [infer Head, ...infer Tails] ? SuspenseQueriesOptions< [...Tails], [...TResults, GetUseSuspenseQueryOptions], [...TDepth, 1] > : Array extends T ? T : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! // use this to infer the param types in the case of Array.map() argument T extends Array< UseSuspenseQueryOptions< infer TQueryFnData, infer TError, infer TData, infer TQueryKey > > ? Array< UseSuspenseQueryOptions > : // Fallback Array /** * SuspenseQueriesResults reducer recursively maps type param to results */ export type SuspenseQueriesResults< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryResult] : T extends [infer Head, ...infer Tails] ? SuspenseQueriesResults< [...Tails], [...TResults, GetUseSuspenseQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetUseSuspenseQueryResult } export function useSuspenseQueries< T extends Array, TCombinedResult = SuspenseQueriesResults, >( options: { queries: | readonly [...SuspenseQueriesOptions] | readonly [...{ [K in keyof T]: GetUseSuspenseQueryOptions }] combine?: (result: SuspenseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, ): TCombinedResult export function useSuspenseQueries< T extends Array, TCombinedResult = SuspenseQueriesResults, >( options: { queries: readonly [...SuspenseQueriesOptions] combine?: (result: SuspenseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, ): TCombinedResult export function useSuspenseQueries(options: any, queryClient?: QueryClient) { return useQueries( { ...options, queries: options.queries.map((query: any) => { if (process.env.NODE_ENV !== 'production') { if (query.queryFn === skipToken) { console.error('skipToken is not allowed for useSuspenseQueries') } } return { ...query, suspense: true, throwOnError: defaultThrowOnError, enabled: true, placeholderData: undefined, } }), }, queryClient, ) } ================================================ FILE: packages/react-query/src/useSuspenseQuery.ts ================================================ 'use client' import { QueryObserver, skipToken } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import { defaultThrowOnError } from './suspense' import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' export function useSuspenseQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UseSuspenseQueryOptions, queryClient?: QueryClient, ): UseSuspenseQueryResult { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseQuery') } } return useBaseQuery( { ...options, enabled: true, suspense: true, throwOnError: defaultThrowOnError, placeholderData: undefined, }, QueryObserver, queryClient, ) as UseSuspenseQueryResult } ================================================ FILE: packages/react-query/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' import { act, cleanup as cleanupRTL } from '@testing-library/react' import { cleanup as cleanupRRS } from '@testing-library/react-render-stream' import { afterEach } from 'vitest' import { notifyManager } from '@tanstack/query-core' // https://testing-library.com/docs/react-testing-library/api#cleanup afterEach(() => { cleanupRTL() cleanupRRS() }) // Wrap notifications with act to make sure React knows about React Query updates notifyManager.setNotifyFunction((fn) => { act(fn) }) ================================================ FILE: packages/react-query/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "react-jsx" }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"] } ================================================ FILE: packages/react-query/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__tests__"], "references": [{ "path": "../query-persist-client-core" }] } ================================================ FILE: packages/react-query/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/react-query/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), legacyConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), ]) ================================================ FILE: packages/react-query/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import packageJson from './package.json' export default defineConfig({ plugins: [react()], // 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: true, provider: 'istanbul', include: ['src/**/*'], exclude: ['src/__tests__/**'], }, typecheck: { enabled: true }, restoreMocks: true, retry: process.env.CI ? 3 : 0, }, }) ================================================ FILE: packages/react-query-devtools/.attw.json ================================================ { "ignoreRules": ["no-resolution"] } ================================================ FILE: packages/react-query-devtools/CHANGELOG.md ================================================ # @tanstack/react-query-devtools ## 5.91.3 ### Patch Changes - Updated dependencies [[`83366c4`](https://github.com/TanStack/query/commit/83366c46a6825b5c591399c705d8128743c527dd)]: - @tanstack/query-devtools@5.93.0 ## 5.91.2 ### Patch Changes - Updated dependencies [[`f9fc56a`](https://github.com/TanStack/query/commit/f9fc56a9b8724bcfae46f8f6cb229123478eb4db), [`0b29b6f`](https://github.com/TanStack/query/commit/0b29b6f877d4b3a6d05b1c85fb9cb1e6ea736291)]: - @tanstack/query-devtools@5.92.0 - @tanstack/react-query@5.90.14 ## 5.91.1 ### Patch Changes - Updated dependencies [[`b261b6f`](https://github.com/TanStack/query/commit/b261b6f29eee2a9bdbe1bc20035fe9b83b15376b)]: - @tanstack/query-devtools@5.91.1 ## 5.91.0 ### Minor Changes - feat(devtools): allow passing a theme via prop ([#9887](https://github.com/TanStack/query/pull/9887)) ### Patch Changes - Updated dependencies [[`0e9d5b5`](https://github.com/TanStack/query/commit/0e9d5b565276f0de2a1a14ffbb079b5988581c27)]: - @tanstack/query-devtools@5.91.0 ================================================ FILE: packages/react-query-devtools/eslint.config.js ================================================ // @ts-check import pluginReact from '@eslint-react/eslint-plugin' import reactHooks from 'eslint-plugin-react-hooks' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, // @ts-expect-error wtf ...reactHooks.configs['recommended-latest'], { files: ['**/*.{ts,tsx}'], ...pluginReact.configs.recommended, rules: { '@eslint-react/no-context-provider': 'off', // We need to be React 18 compatible 'react-hooks/exhaustive-deps': 'error', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/unsupported-syntax': 'error', 'react-hooks/incompatible-library': 'error', }, }, ] ================================================ FILE: packages/react-query-devtools/package.json ================================================ { "name": "@tanstack/react-query-devtools", "version": "5.91.3", "description": "Developer tools to interact with and visualize the TanStack/react-query cache", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/react-query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json", "build:dev": "tsup --watch" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./production": { "import": { "types": "./build/modern/production.d.ts", "default": "./build/modern/production.js" }, "require": { "types": "./build/modern/production.d.cts", "default": "./build/modern/production.cjs" } }, "./build/modern/production.js": { "import": { "types": "./build/modern/production.d.ts", "default": "./build/modern/production.js" }, "require": { "types": "./build/modern/production.d.cts", "default": "./build/modern/production.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-devtools": "workspace:*" }, "devDependencies": { "@tanstack/react-query": "workspace:*", "@testing-library/react": "^16.1.0", "@types/react": "^19.2.7", "@vitejs/plugin-react": "^4.3.4", "npm-run-all2": "^5.0.0", "react": "^19.2.1" }, "peerDependencies": { "@tanstack/react-query": "workspace:^", "react": "^18 || ^19" } } ================================================ FILE: packages/react-query-devtools/src/ReactQueryDevtools.tsx ================================================ 'use client' import * as React from 'react' import { onlineManager, useQueryClient } from '@tanstack/react-query' import { TanstackQueryDevtools } from '@tanstack/query-devtools' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, Theme, } from '@tanstack/query-devtools' import type { QueryClient } from '@tanstack/react-query' export interface DevtoolsOptions { /** * Set this true if you want the dev tools to default to being open */ initialIsOpen?: boolean /** * The position of the React Query logo to open and close the devtools panel. * 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' * Defaults to 'bottom-right'. */ buttonPosition?: DevtoolsButtonPosition /** * The position of the React Query devtools panel. * 'top' | 'bottom' | 'left' | 'right' * Defaults to 'bottom'. */ position?: DevtoolsPosition /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean /** * Set this to 'light', 'dark', or 'system' to change the theme of the devtools panel. * Defaults to 'system'. */ theme?: Theme } export function ReactQueryDevtools( props: DevtoolsOptions, ): React.ReactElement | null { const queryClient = useQueryClient(props.client) const ref = React.useRef(null) const { buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, theme, } = props const [devtools] = React.useState( new TanstackQueryDevtools({ client: queryClient, queryFlavor: 'React Query', version: '5', onlineManager, buttonPosition, position, initialIsOpen, errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, theme, }), ) React.useEffect(() => { devtools.setClient(queryClient) }, [queryClient, devtools]) React.useEffect(() => { if (buttonPosition) { devtools.setButtonPosition(buttonPosition) } }, [buttonPosition, devtools]) React.useEffect(() => { if (position) { devtools.setPosition(position) } }, [position, devtools]) React.useEffect(() => { devtools.setInitialIsOpen(initialIsOpen || false) }, [initialIsOpen, devtools]) React.useEffect(() => { devtools.setErrorTypes(errorTypes || []) }, [errorTypes, devtools]) React.useEffect(() => { devtools.setTheme(theme) }, [theme, devtools]) React.useEffect(() => { if (ref.current) { devtools.mount(ref.current) } return () => { devtools.unmount() } }, [devtools]) return
    } ================================================ FILE: packages/react-query-devtools/src/ReactQueryDevtoolsPanel.tsx ================================================ 'use client' import * as React from 'react' import { onlineManager, useQueryClient } from '@tanstack/react-query' import { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' import type { DevtoolsErrorType, Theme } from '@tanstack/query-devtools' import type { QueryClient } from '@tanstack/react-query' export interface DevtoolsPanelOptions { /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Custom styles for the devtools panel * @default { height: '500px' } * @example { height: '100%' } * @example { height: '100%', width: '100%' } */ style?: React.CSSProperties /** * Callback function that is called when the devtools panel is closed */ onClose?: () => unknown /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean /** * Set this to 'light', 'dark', or 'system' to change the theme of the devtools panel. * Defaults to 'system'. */ theme?: Theme } export function ReactQueryDevtoolsPanel( props: DevtoolsPanelOptions, ): React.ReactElement | null { const queryClient = useQueryClient(props.client) const ref = React.useRef(null) const { errorTypes, styleNonce, shadowDOMTarget, hideDisabledQueries, theme, } = props const [devtools] = React.useState( new TanstackQueryDevtoolsPanel({ client: queryClient, queryFlavor: 'React Query', version: '5', onlineManager, buttonPosition: 'bottom-left', position: 'bottom', initialIsOpen: true, errorTypes, styleNonce, shadowDOMTarget, onClose: props.onClose, hideDisabledQueries, theme, }), ) React.useEffect(() => { devtools.setClient(queryClient) }, [queryClient, devtools]) React.useEffect(() => { devtools.setOnClose(props.onClose ?? (() => {})) }, [props.onClose, devtools]) React.useEffect(() => { devtools.setErrorTypes(errorTypes || []) }, [errorTypes, devtools]) React.useEffect(() => { devtools.setTheme(theme) }, [theme, devtools]) React.useEffect(() => { if (ref.current) { devtools.mount(ref.current) } return () => { devtools.unmount() } }, [devtools]) return (
    ) } ================================================ FILE: packages/react-query-devtools/src/__tests__/devtools.test.tsx ================================================ import { describe, expect, it } from 'vitest' describe('ReactQueryDevtools', () => { it('should be able to open and close devtools', () => { expect(1).toBe(1) }) }) ================================================ FILE: packages/react-query-devtools/src/__tests__/not-development.test.tsx ================================================ import { describe, expect, it } from 'vitest' import { ReactQueryDevtools } from '..' describe('ReactQueryDevtools not in process.env.NODE_ENV=development', () => { it('should return null', () => { expect(process.env.NODE_ENV).not.toBe('development') expect(ReactQueryDevtools({})).toBeNull() }) }) ================================================ FILE: packages/react-query-devtools/src/index.ts ================================================ 'use client' import * as Devtools from './ReactQueryDevtools' import * as DevtoolsPanel from './ReactQueryDevtoolsPanel' export const ReactQueryDevtools: (typeof Devtools)['ReactQueryDevtools'] = process.env.NODE_ENV !== 'development' ? function () { return null } : Devtools.ReactQueryDevtools export const ReactQueryDevtoolsPanel: (typeof DevtoolsPanel)['ReactQueryDevtoolsPanel'] = process.env.NODE_ENV !== 'development' ? function () { return null } : DevtoolsPanel.ReactQueryDevtoolsPanel export type DevtoolsPanelOptions = DevtoolsPanel.DevtoolsPanelOptions ================================================ FILE: packages/react-query-devtools/src/production.ts ================================================ 'use client' import * as Devtools from './ReactQueryDevtools' import * as DevtoolsPanel from './ReactQueryDevtoolsPanel' export const ReactQueryDevtools = Devtools.ReactQueryDevtools export const ReactQueryDevtoolsPanel = DevtoolsPanel.ReactQueryDevtoolsPanel ================================================ FILE: packages/react-query-devtools/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' import { act, cleanup } from '@testing-library/react' import { afterEach } from 'vitest' import { notifyManager } from '@tanstack/react-query' // https://testing-library.com/docs/react-testing-library/api#cleanup afterEach(() => cleanup()) // Wrap notifications with act to make sure React knows about React Query updates notifyManager.setNotifyFunction((fn) => { act(fn) }) ================================================ FILE: packages/react-query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": ".", "jsx": "react-jsx" }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"], "references": [{ "path": "../query-devtools" }, { "path": "../react-query" }] } ================================================ FILE: packages/react-query-devtools/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__tests__"], "references": [{ "path": "../query-devtools" }, { "path": "../react-query" }] } ================================================ FILE: packages/react-query-devtools/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../", "customConditions": [] } } ================================================ FILE: packages/react-query-devtools/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), legacyConfig({ entry: ['src/*.ts', 'src/*.tsx'] }), ]) ================================================ FILE: packages/react-query-devtools/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import packageJson from './package.json' export default defineConfig({ plugins: [react()], // 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: true, provider: 'istanbul', include: ['src/**/*'], exclude: ['src/__tests__/**'], }, typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/react-query-next-experimental/CHANGELOG.md ================================================ # @tanstack/react-query-next-experimental ## 5.91.0 ### Minor Changes - feat(react-query-next-experimental): support Next.js 16 ([#9868](https://github.com/TanStack/query/pull/9868)) ================================================ FILE: packages/react-query-next-experimental/eslint.config.js ================================================ // @ts-check import pluginReact from '@eslint-react/eslint-plugin' import reactHooks from 'eslint-plugin-react-hooks' import rootConfig from './root.eslint.config.js' export default [ ...rootConfig, // @ts-expect-error wtf ...reactHooks.configs['recommended-latest'], { files: ['**/*.{ts,tsx}'], ...pluginReact.configs.recommended, rules: { '@eslint-react/no-context-provider': 'off', // We need to be React 18 compatible 'react-hooks/exhaustive-deps': 'error', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/unsupported-syntax': 'error', 'react-hooks/incompatible-library': 'error', }, }, ] ================================================ FILE: packages/react-query-next-experimental/package.json ================================================ { "name": "@tanstack/react-query-next-experimental", "version": "5.91.0", "description": "Hydration utils for React Query in the NextJs app directory", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/react-query-next-experimental" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "devDependencies": { "@tanstack/react-query": "workspace:*", "@types/react": "^19.2.7", "@vitejs/plugin-react": "^4.3.4", "next": "^16.0.1", "npm-run-all2": "^5.0.0", "react": "^19.2.1" }, "peerDependencies": { "@tanstack/react-query": "workspace:^", "next": "^13 || ^14 || ^15 || ^16", "react": "^18 || ^19" } } ================================================ FILE: packages/react-query-next-experimental/src/HydrationStreamProvider.tsx ================================================ 'use client' import { isServer } from '@tanstack/react-query' import { useServerInsertedHTML } from 'next/navigation' import * as React from 'react' import { htmlEscapeJsonString } from './htmlescape' const serializedSymbol = Symbol('serialized') interface DataTransformer { serialize: (object: any) => any deserialize: (object: any) => any } type Serialized = unknown & { [serializedSymbol]: TData } interface TypedDataTransformer { serialize: (obj: TData) => Serialized deserialize: (obj: Serialized) => TData } interface HydrationStreamContext { id: string stream: { /** * **Server method** * Push a new entry to the stream * Will be ignored on the client */ push: (...shape: Array) => void } } export interface HydrationStreamProviderProps { children: React.ReactNode /** * Optional transformer to serialize/deserialize the data * Example devalue, superjson et al */ transformer?: DataTransformer /** * **Client method** * Called in the browser when new entries are received */ onEntries: (entries: Array) => void /** * **Server method** * onFlush is called on the server when the cache is flushed */ onFlush?: () => Array /** * A nonce that'll allow the inline script to be executed when Content Security Policy is enforced */ nonce?: string } export function createHydrationStreamProvider() { const context = React.createContext>( null as any, ) /** * 1. (Happens on server): `useServerInsertedHTML()` is called **on the server** whenever a `Suspense`-boundary completes * - This means that we might have some new entries in the cache that needs to be flushed * - We pass these to the client by inserting a ` {@render children()} ================================================ FILE: packages/svelte-query/src/QueryClientProvider.svelte ================================================ {@render children()} ================================================ FILE: packages/svelte-query/src/containers.svelte.ts ================================================ import { SvelteSet, createSubscriber } from 'svelte/reactivity' type VoidFn = () => void type Subscriber = (update: VoidFn) => void | VoidFn export type Box = { current: T } export class ReactiveValue implements Box { #fn #subscribe constructor(fn: () => T, onSubscribe: Subscriber) { this.#fn = fn this.#subscribe = createSubscriber((update) => onSubscribe(update)) } get current() { this.#subscribe() return this.#fn() } } /** * Makes all of the top-level keys of an object into $state.raw fields whose initial values * are the same as in the original object. Does not mutate the original object. Provides an `update` * function that _can_ (but does not have to be) be used to replace all of the object's top-level keys * with the values of the new object, while maintaining the original root object's reference. */ export function createRawRef>( init: T, ): [T, (newValue: T) => void] { const refObj = (Array.isArray(init) ? [] : {}) as T const hiddenKeys = new SvelteSet() const out = new Proxy(refObj, { set(target, prop, value, receiver) { hiddenKeys.delete(prop) if (prop in target) { return Reflect.set(target, prop, value, receiver) } let state = $state.raw(value) Object.defineProperty(target, prop, { configurable: true, enumerable: true, get: () => { // If this is a lazy value, we need to call it. // We can't do something like typeof state === 'function' // because the value could actually be a function that we don't want to call. return state && isBranded(state) ? state() : state }, set: (v) => { state = v }, }) return true }, has: (target, prop) => { if (hiddenKeys.has(prop)) { return false } return prop in target }, ownKeys(target) { return Reflect.ownKeys(target).filter((key) => !hiddenKeys.has(key)) }, getOwnPropertyDescriptor(target, prop) { if (hiddenKeys.has(prop)) { return undefined } return Reflect.getOwnPropertyDescriptor(target, prop) }, deleteProperty(target, prop) { if (prop in target) { // @ts-expect-error // We need to set the value to undefined to signal to the listeners that the value has changed. // If we just deleted it, the reactivity system wouldn't have any idea that the value was gone. target[prop] = undefined hiddenKeys.add(prop) if (Array.isArray(target)) { target.length-- } return true } return false }, }) function update(newValue: T) { const existingKeys = Object.keys(out) const newKeys = Object.keys(newValue) const keysToRemove = existingKeys.filter((key) => !newKeys.includes(key)) for (const key of keysToRemove) { // @ts-expect-error delete out[key] } for (const key of newKeys) { // @ts-expect-error // This craziness is required because Tanstack Query defines getters for all of the keys on the object. // These getters track property access, so if we access all of them here, we'll end up tracking everything. // So we wrap the property access in a special function that we can identify later to lazily access the value. // (See above) out[key] = brand(() => newValue[key]) } } // we can't pass `init` directly into the proxy because it'll never set the state fields // (because (prop in target) will always be true) update(init) return [out, update] } const lazyBrand = Symbol('LazyValue') type Branded unknown> = T & { [lazyBrand]: true } function brand unknown>(fn: T): Branded { // @ts-expect-error fn[lazyBrand] = true return fn as Branded } function isBranded unknown>(fn: T): fn is Branded { return Boolean((fn as Branded)[lazyBrand]) } ================================================ FILE: packages/svelte-query/src/context.ts ================================================ import { getContext, setContext } from 'svelte' import type { QueryClient } from '@tanstack/query-core' import type { Box } from './containers.svelte' const _contextKey = Symbol('QueryClient') /** Retrieves a Client from Svelte's context */ export const getQueryClientContext = (): QueryClient => { const client = getContext(_contextKey) if (!client) { throw new Error( 'No QueryClient was found in Svelte context. Did you forget to wrap your component with QueryClientProvider?', ) } return client } /** Sets a QueryClient on Svelte's context */ export const setQueryClientContext = (client: QueryClient): void => { setContext(_contextKey, client) } const _isRestoringContextKey = Symbol('isRestoring') /** Retrieves a `isRestoring` from Svelte's context */ export const getIsRestoringContext = (): Box => { try { const isRestoring = getContext | undefined>( _isRestoringContextKey, ) return isRestoring ?? { current: false } } catch (error) { return { current: false } } } /** Sets a `isRestoring` on Svelte's context */ export const setIsRestoringContext = (isRestoring: Box): void => { setContext(_isRestoringContextKey, isRestoring) } ================================================ FILE: packages/svelte-query/src/createBaseQuery.svelte.ts ================================================ import { useIsRestoring } from './useIsRestoring.js' import { useQueryClient } from './useQueryClient.js' import { createRawRef } from './containers.svelte.js' import { watchChanges } from './utils.svelte.js' import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core' import type { Accessor, CreateBaseQueryOptions, CreateBaseQueryResult, } from './types.js' /** * Base implementation for `createQuery` and `createInfiniteQuery` * @param options - A function that returns query options * @param Observer - The observer from query-core * @param queryClient - Custom query client which overrides provider */ export function createBaseQuery< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, >( options: Accessor< CreateBaseQueryOptions >, Observer: typeof QueryObserver, queryClient?: Accessor, ): CreateBaseQueryResult { /** Load query client */ const client = $derived(useQueryClient(queryClient?.())) const isRestoring = useIsRestoring() const resolvedOptions = $derived.by(() => { const opts = client.defaultQueryOptions(options()) opts._optimisticResults = isRestoring.current ? 'isRestoring' : 'optimistic' return opts }) /** Creates the observer */ // svelte-ignore state_referenced_locally - intentional, initial value let observer = $state( new Observer( client, resolvedOptions, ), ) watchChanges( () => client, 'pre', () => { observer = new Observer< TQueryFnData, TError, TData, TQueryData, TQueryKey >(client, resolvedOptions) }, ) function createResult() { const result = observer.getOptimisticResult(resolvedOptions) return !resolvedOptions.notifyOnChangeProps ? observer.trackResult(result) : result } const [query, update] = createRawRef( // svelte-ignore state_referenced_locally - intentional, initial value createResult(), ) $effect(() => { const unsubscribe = isRestoring.current ? () => undefined : observer.subscribe(() => update(createResult())) observer.updateResult() return unsubscribe }) watchChanges( () => resolvedOptions, 'pre', () => { observer.setOptions(resolvedOptions) }, ) watchChanges( () => [resolvedOptions, observer], 'pre', () => { // The only reason this is necessary is because of `isRestoring`. // Because we don't subscribe while restoring, the following can occur: // - `isRestoring` is true // - `isRestoring` becomes false // - `observer.subscribe` and `observer.updateResult` is called in the above effect, // but the subsequent `fetch` has already completed // - `result` misses the intermediate restored-but-not-fetched state // // this could technically be its own effect but that doesn't seem necessary update(createResult()) }, ) return query } ================================================ FILE: packages/svelte-query/src/createInfiniteQuery.ts ================================================ import { InfiniteQueryObserver } from '@tanstack/query-core' import { createBaseQuery } from './createBaseQuery.svelte.js' import type { DefaultError, InfiniteData, QueryClient, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { Accessor, CreateInfiniteQueryOptions, CreateInfiniteQueryResult, } from './types.js' export function createInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: Accessor< CreateInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, queryClient?: Accessor, ): CreateInfiniteQueryResult { return createBaseQuery( options, InfiniteQueryObserver as typeof QueryObserver, queryClient, ) as CreateInfiniteQueryResult } ================================================ FILE: packages/svelte-query/src/createMutation.svelte.ts ================================================ import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' import { watchChanges } from './utils.svelte.js' import type { Accessor, CreateMutateFunction, CreateMutationOptions, CreateMutationResult, } from './types.js' import type { DefaultError, QueryClient } from '@tanstack/query-core' /** * @param options - A function that returns mutation options * @param queryClient - Custom query client which overrides provider */ export function createMutation< TData = unknown, TError = DefaultError, TVariables = void, TContext = unknown, >( options: Accessor>, queryClient?: Accessor, ): CreateMutationResult { const client = $derived(useQueryClient(queryClient?.())) // svelte-ignore state_referenced_locally - intentional, initial value let observer = $state( // svelte-ignore state_referenced_locally - intentional, initial value new MutationObserver( client, options(), ), ) watchChanges( () => client, 'pre', () => { observer = new MutationObserver(client, options()) }, ) $effect.pre(() => { observer.setOptions(options()) }) const mutate = >(( variables, mutateOptions, ) => { observer.mutate(variables, mutateOptions).catch(noop) }) let result = $state(observer.getCurrentResult()) watchChanges( () => observer, 'pre', () => { result = observer.getCurrentResult() }, ) $effect.pre(() => { const unsubscribe = observer.subscribe((val) => { notifyManager.batchCalls(() => { Object.assign(result, val) })() }) return unsubscribe }) const resultProxy = $derived( new Proxy(result, { get: (_, prop) => { const r = { ...result, mutate, mutateAsync: result.mutate, } if (prop == 'value') return r // @ts-expect-error return r[prop] }, }), ) // @ts-expect-error return resultProxy } ================================================ FILE: packages/svelte-query/src/createQueries.svelte.ts ================================================ import { QueriesObserver } from '@tanstack/query-core' import { useIsRestoring } from './useIsRestoring.js' import { createRawRef } from './containers.svelte.js' import { useQueryClient } from './useQueryClient.js' import type { Accessor, CreateQueryOptions, CreateQueryResult, DefinedCreateQueryResult, } from './types.js' import type { DefaultError, OmitKeyof, QueriesObserverOptions, QueriesPlaceholderDataFunction, QueryClient, QueryFunction, QueryKey, ThrowOnError, } from '@tanstack/query-core' // This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function always gets undefined passed type CreateQueryOptionsForCreateQueries< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< CreateQueryOptions, 'placeholderData' > & { placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction } // 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 SkipTokenForCreateQueries = symbol type GetCreateQueryOptionsForCreateQueries = // 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 } ? CreateQueryOptionsForCreateQueries : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? CreateQueryOptionsForCreateQueries : T extends { data: infer TData; error?: infer TError } ? CreateQueryOptionsForCreateQueries : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] ? CreateQueryOptionsForCreateQueries : T extends [infer TQueryFnData, infer TError] ? CreateQueryOptionsForCreateQueries : T extends [infer TQueryFnData] ? CreateQueryOptionsForCreateQueries : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForCreateQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? CreateQueryOptionsForCreateQueries< TQueryFnData, unknown extends TError ? DefaultError : TError, unknown extends TData ? TQueryFnData : TData, TQueryKey > : // Fallback CreateQueryOptionsForCreateQueries // A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult type GetDefinedOrUndefinedQueryResult = T extends { initialData?: infer TInitialData } ? unknown extends TInitialData ? CreateQueryResult : TInitialData extends TData ? DefinedCreateQueryResult : TInitialData extends () => infer TInitialDataResult ? unknown extends TInitialDataResult ? CreateQueryResult : TInitialDataResult extends TData ? DefinedCreateQueryResult : CreateQueryResult : CreateQueryResult : CreateQueryResult type GetCreateQueryResult = // Part 1: responsible for mapping explicit type parameter to function result, if object T extends { queryFnData: any; error?: infer TError; data: infer TData } ? GetDefinedOrUndefinedQueryResult : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? GetDefinedOrUndefinedQueryResult : T extends { data: infer TData; error?: infer TError } ? GetDefinedOrUndefinedQueryResult : // Part 2: responsible for mapping explicit type parameter to function result, if tuple T extends [any, infer TError, infer TData] ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData, infer TError] ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData] ? GetDefinedOrUndefinedQueryResult : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForCreateQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : // Fallback CreateQueryResult /** * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type QueriesOptions< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryOptionsForCreateQueries] : T extends [infer Head, ...infer Tails] ? QueriesOptions< [...Tails], [...TResults, GetCreateQueryOptionsForCreateQueries], [...TDepth, 1] > : ReadonlyArray extends T ? T : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! // use this to infer the param types in the case of Array.map() argument T extends Array< CreateQueryOptionsForCreateQueries< infer TQueryFnData, infer TError, infer TData, infer TQueryKey > > ? Array< CreateQueryOptionsForCreateQueries< TQueryFnData, TError, TData, TQueryKey > > : // Fallback Array /** * QueriesResults reducer recursively maps type param to results */ export type QueriesResults< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResult] : T extends [infer Head, ...infer Tails] ? QueriesResults< [...Tails], [...TResults, GetCreateQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetCreateQueryResult } export function createQueries< T extends Array, TCombinedResult = QueriesResults, >( createQueriesOptions: Accessor<{ queries: | readonly [...QueriesOptions] | readonly [ ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] combine?: (result: QueriesResults) => TCombinedResult }>, queryClient?: Accessor, ): TCombinedResult { const client = $derived(useQueryClient(queryClient?.())) const isRestoring = useIsRestoring() const { queries, combine } = $derived.by(createQueriesOptions) const resolvedQueryOptions = $derived( queries.map((opts) => { const resolvedOptions = client.defaultQueryOptions(opts) // Make sure the results are already in fetching state before subscribing or updating options resolvedOptions._optimisticResults = isRestoring.current ? 'isRestoring' : 'optimistic' return resolvedOptions }), ) // can't do same as createMutation, as QueriesObserver has no `setOptions` method const observer = $derived( new QueriesObserver( client, resolvedQueryOptions, combine as QueriesObserverOptions, ), ) function createResult() { const [_, getCombinedResult, trackResult] = observer.getOptimisticResult( resolvedQueryOptions, combine as QueriesObserverOptions['combine'], ) return getCombinedResult(trackResult()) } // @ts-expect-error - the crazy-complex TCombinedResult type doesn't like being called an array // svelte-ignore state_referenced_locally const [results, update] = createRawRef(createResult()) $effect(() => { const unsubscribe = isRestoring.current ? () => undefined : observer.subscribe(() => update(createResult())) return unsubscribe }) $effect.pre(() => { observer.setQueries(resolvedQueryOptions, { combine, } as QueriesObserverOptions) update(createResult()) }) return results } ================================================ FILE: packages/svelte-query/src/createQuery.ts ================================================ import { QueryObserver } from '@tanstack/query-core' import { createBaseQuery } from './createBaseQuery.svelte.js' import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' import type { Accessor, CreateQueryOptions, CreateQueryResult, DefinedCreateQueryResult, } from './types.js' import type { DefinedInitialDataOptions, UndefinedInitialDataOptions, } from './queryOptions.js' export function createQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: Accessor< UndefinedInitialDataOptions >, queryClient?: Accessor, ): CreateQueryResult export function createQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: Accessor< DefinedInitialDataOptions >, queryClient?: Accessor, ): DefinedCreateQueryResult export function createQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: Accessor>, queryClient?: Accessor, ): CreateQueryResult export function createQuery( options: Accessor, queryClient?: Accessor, ) { return createBaseQuery(options, QueryObserver, queryClient) } ================================================ FILE: packages/svelte-query/src/index.ts ================================================ /* istanbul ignore file */ // Re-export core export * from '@tanstack/query-core' // Svelte Query export * from './types.js' export * from './context.js' export { createQuery } from './createQuery.js' export type { QueriesResults, QueriesOptions } from './createQueries.svelte.js' export type { DefinedInitialDataOptions, UndefinedInitialDataOptions, } from './queryOptions.js' export { queryOptions } from './queryOptions.js' export { createQueries } from './createQueries.svelte.js' export { createInfiniteQuery } from './createInfiniteQuery.js' export { infiniteQueryOptions } from './infiniteQueryOptions.js' export { mutationOptions } from './mutationOptions.js' export { createMutation } from './createMutation.svelte.js' export { useMutationState } from './useMutationState.svelte.js' export { useQueryClient } from './useQueryClient.js' export { useIsFetching } from './useIsFetching.svelte.js' export { useIsMutating } from './useIsMutating.svelte.js' export { useIsRestoring } from './useIsRestoring.js' export { useHydrate } from './useHydrate.js' export { default as HydrationBoundary } from './HydrationBoundary.svelte' export { default as QueryClientProvider } from './QueryClientProvider.svelte' ================================================ FILE: packages/svelte-query/src/infiniteQueryOptions.ts ================================================ import type { DefaultError, InfiniteData, QueryKey } from '@tanstack/query-core' import type { CreateInfiniteQueryOptions } from './types.js' export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: CreateInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): CreateInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > { return options } ================================================ FILE: packages/svelte-query/src/mutationOptions.ts ================================================ import type { DefaultError, WithRequired } from '@tanstack/query-core' import type { CreateMutationOptions } from './types.js' export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: WithRequired< CreateMutationOptions, 'mutationKey' >, ): WithRequired< CreateMutationOptions, 'mutationKey' > export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: Omit< CreateMutationOptions, 'mutationKey' >, ): Omit< CreateMutationOptions, 'mutationKey' > export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( options: CreateMutationOptions, ): CreateMutationOptions { return options } ================================================ FILE: packages/svelte-query/src/queryOptions.ts ================================================ import type { DataTag, DefaultError, InitialDataFunction, NonUndefinedGuard, QueryKey, } from '@tanstack/query-core' import type { CreateQueryOptions } from './types.js' export type UndefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = CreateQueryOptions & { initialData?: undefined | InitialDataFunction> } export type DefinedInitialDataOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = CreateQueryOptions & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions, ): DefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, ): UndefinedInitialDataOptions & { queryKey: DataTag } export function queryOptions(options: unknown) { return options } ================================================ FILE: packages/svelte-query/src/types.ts ================================================ import type { Snippet } from 'svelte' import type { DefaultError, DefinedQueryObserverResult, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutateFunction, Mutation, MutationFilters, MutationObserverOptions, MutationObserverResult, MutationState, OmitKeyof, Override, QueryClient, QueryKey, QueryObserverOptions, QueryObserverResult, } from '@tanstack/query-core' export type Accessor = () => T /** Options for createBaseQuery */ export type CreateBaseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = QueryObserverOptions /** Result from createBaseQuery */ export type CreateBaseQueryResult< TData = unknown, TError = DefaultError, > = QueryObserverResult /** Options for createQuery */ export type CreateQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = CreateBaseQueryOptions /** Result from createQuery */ export type CreateQueryResult< TData = unknown, TError = DefaultError, > = CreateBaseQueryResult /** Options for createInfiniteQuery */ export type CreateInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > /** Result from createInfiniteQuery */ export type CreateInfiniteQueryResult< TData = unknown, TError = DefaultError, > = InfiniteQueryObserverResult /** Options for createBaseQuery with initialData */ export type DefinedCreateBaseQueryResult< TData = unknown, TError = DefaultError, > = DefinedQueryObserverResult /** Options for createQuery with initialData */ export type DefinedCreateQueryResult< TData = unknown, TError = DefaultError, > = DefinedCreateBaseQueryResult /** Options for createMutation */ export type CreateMutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = OmitKeyof< MutationObserverOptions, '_defaulted' > export type CreateMutateFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( ...args: Parameters< MutateFunction > ) => void export type CreateMutateAsyncFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = MutateFunction export type CreateBaseMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = Override< MutationObserverResult, { mutate: CreateMutateFunction } > & { mutateAsync: CreateMutateAsyncFunction< TData, TError, TVariables, TOnMutateResult > } /** Result from createMutation */ export type CreateMutationResult< TData = unknown, TError = DefaultError, TVariables = unknown, TOnMutateResult = unknown, > = CreateBaseMutationResult /** Options for useMutationState */ export type MutationStateOptions = { filters?: MutationFilters select?: ( mutation: Mutation, ) => TResult } export type QueryClientProviderProps = { client: QueryClient children: Snippet } ================================================ FILE: packages/svelte-query/src/useHydrate.ts ================================================ import { hydrate } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' import type { HydrateOptions, QueryClient } from '@tanstack/query-core' export function useHydrate( state?: unknown, options?: HydrateOptions, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (state) { hydrate(client, state, options) } } ================================================ FILE: packages/svelte-query/src/useIsFetching.svelte.ts ================================================ import { ReactiveValue } from './containers.svelte.js' import { useQueryClient } from './useQueryClient.js' import type { QueryClient, QueryFilters } from '@tanstack/query-core' export function useIsFetching( filters?: QueryFilters, queryClient?: QueryClient, ): ReactiveValue { const client = useQueryClient(queryClient) const queryCache = client.getQueryCache() return new ReactiveValue( () => client.isFetching(filters), (update) => queryCache.subscribe(update), ) } ================================================ FILE: packages/svelte-query/src/useIsMutating.svelte.ts ================================================ import { useQueryClient } from './useQueryClient.js' import { ReactiveValue } from './containers.svelte.js' import type { MutationFilters, QueryClient } from '@tanstack/query-core' export function useIsMutating( filters?: MutationFilters, queryClient?: QueryClient, ): ReactiveValue { const client = useQueryClient(queryClient) const cache = client.getMutationCache() return new ReactiveValue( () => client.isMutating(filters), (update) => cache.subscribe(update), ) } ================================================ FILE: packages/svelte-query/src/useIsRestoring.ts ================================================ import { getIsRestoringContext } from './context.js' import type { Box } from './containers.svelte.js' export function useIsRestoring(): Box { return getIsRestoringContext() } ================================================ FILE: packages/svelte-query/src/useMutationState.svelte.ts ================================================ import { replaceEqualDeep } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' import type { MutationCache, MutationState, QueryClient, } from '@tanstack/query-core' import type { MutationStateOptions } from './types.js' function getResult( mutationCache: MutationCache, options: MutationStateOptions, ): Array { return mutationCache .findAll(options.filters) .map( (mutation): TResult => (options.select ? options.select(mutation) : mutation.state) as TResult, ) } export function useMutationState( options: MutationStateOptions = {}, queryClient?: QueryClient, ): Array { const mutationCache = useQueryClient(queryClient).getMutationCache() const result = $state(getResult(mutationCache, options)) $effect(() => { const unsubscribe = mutationCache.subscribe(() => { const nextResult = replaceEqualDeep( result, getResult(mutationCache, options), ) if (result !== nextResult) { Object.assign(result, nextResult) } }) return unsubscribe }) /* $effect(() => { mutationCache.subscribe(() => { const nextResult = replaceEqualDeep( result.current, getResult(mutationCache, optionsRef), ) if (result.current !== nextResult) { result = nextResult //notifyManager.schedule(onStoreChange) } }) }) */ return result } ================================================ FILE: packages/svelte-query/src/useQueryClient.ts ================================================ import { getQueryClientContext } from './context.js' import type { QueryClient } from '@tanstack/query-core' export function useQueryClient(queryClient?: QueryClient): QueryClient { if (queryClient) return queryClient return getQueryClientContext() } ================================================ FILE: packages/svelte-query/src/utils.svelte.ts ================================================ import { untrack } from 'svelte' // modified from the great https://github.com/svecosystem/runed function runEffect( flush: 'post' | 'pre', effect: () => void | VoidFunction, ): void { switch (flush) { case 'post': $effect(effect) break case 'pre': $effect.pre(effect) break } } type Getter = () => T export const watchChanges = ( sources: Getter | Array>, flush: 'post' | 'pre', effect: ( values: T | Array, previousValues: T | undefined | Array, ) => void, ) => { let active = false let previousValues: T | undefined | Array = Array.isArray( sources, ) ? [] : undefined runEffect(flush, () => { const values = Array.isArray(sources) ? sources.map((source) => source()) : sources() if (!active) { active = true previousValues = values return } const cleanup = untrack(() => effect(values, previousValues)) previousValues = values return cleanup }) } ================================================ FILE: packages/svelte-query/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { preprocess: vitePreprocess(), compilerOptions: { runes: true, }, } export default config ================================================ FILE: packages/svelte-query/tests/HydrationBoundary/BaseExample.svelte ================================================
    data: {query.data ?? 'undefined'}
    ================================================ FILE: packages/svelte-query/tests/HydrationBoundary/HydrationBoundary.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/svelte' import { QueryClient, dehydrate } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import BaseExample from './BaseExample.svelte' describe('HydrationBoundary', () => { let stringifiedState: string beforeEach(async () => { vi.useFakeTimers() const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['string'], queryFn: () => sleep(10).then(() => 'stringCached'), }) await vi.advanceTimersByTimeAsync(10) const dehydrated = dehydrate(queryClient) stringifiedState = JSON.stringify(dehydrated) queryClient.clear() }) afterEach(() => { vi.useRealTimers() }) it('should hydrate queries to the cache on context', async () => { const dehydratedState = JSON.parse(stringifiedState) const rendered = render(BaseExample, { props: { dehydratedState, queryFn: () => sleep(20).then(() => 'string'), }, }) expect(rendered.getByText('data: stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('data: string')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/ProviderWrapper.svelte ================================================ {@render children()} ================================================ FILE: packages/svelte-query/tests/QueryClientProvider/ChildComponent.svelte ================================================
    Data: {query.data ?? 'undefined'}
    ================================================ FILE: packages/svelte-query/tests/QueryClientProvider/ParentComponent.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { render } from '@testing-library/svelte' import { QueryClient } from '@tanstack/query-core' import ParentComponent from './ParentComponent.svelte' describe('QueryClientProvider', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Sets a specific cache for all queries to use', async () => { const queryClient = new QueryClient() const queryCache = queryClient.getQueryCache() const rendered = render(ParentComponent, { props: { queryClient: queryClient, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Data: test')).toBeInTheDocument() expect(queryCache.find({ queryKey: ['hello'] })).toBeDefined() }) }) ================================================ FILE: packages/svelte-query/tests/containers.svelte.test.ts ================================================ import { flushSync } from 'svelte' import { describe, expect, it } from 'vitest' import { createRawRef } from '../src/containers.svelte.js' import { withEffectRoot } from './utils.svelte.js' describe('createRawRef', () => { it('should create a reactive reference', () => { const [ref, update] = createRawRef({ a: 1, b: 2 }) expect(ref).toEqual({ a: 1, b: 2 }) update({ a: 3, b: 4 }) expect(ref).toEqual({ a: 3, b: 4 }) ref.a = 5 expect(ref).toEqual({ a: 5, b: 4 }) }) it('should handle nested objects', () => { const [ref, update] = createRawRef<{ a: any }>({ a: { b: { c: 1 } } }) expect(ref).toEqual({ a: { b: { c: 1 } } }) // update with same structure update({ a: { b: { c: 2 } } }) expect(ref).toEqual({ a: { b: { c: 2 } } }) ref.a.b.c = 3 expect(ref).toEqual({ a: { b: { c: 3 } } }) // update with different structure should wipe out everything below the first level update({ a: { b: 3 } }) expect(ref).toEqual({ a: { b: 3 } }) }) it('should remove properties when a new object is assigned', () => { const [ref, update] = createRawRef>({ a: 1, b: 2, }) expect(ref).toEqual({ a: 1, b: 2 }) update({ a: 3 }) expect(ref).toEqual({ a: 3 }) }) it( 'should not break reactivity when removing keys', withEffectRoot(() => { const [ref, update] = createRawRef>({ a: 1, b: 2 }) const states: Array = [] $effect(() => { states.push(ref.b) }) // these flushSync calls force the effect to run and push the value to the states array flushSync() update({ a: 3 }) // should remove b, and should rerun the effect flushSync() update({ a: 3, b: 4 }) // should add b back, and should rerun the effect flushSync() delete ref.b // should remove b, and should rerun the effect flushSync() delete ref.a // should remove a, and should _not_ rerun the effect expect(states).toEqual([2, undefined, 4, undefined]) }), ) it( 'should correctly trap calls to `in`', withEffectRoot(() => { const [ref, update] = createRawRef>({ a: 1, b: 2, }) expect('b' in ref).toBe(true) delete ref.b expect('b' in ref).toBe(false) update({}) expect('a' in ref).toBe(false) update({ a: 1, b: 2 }) expect('b' in ref).toBe(true) expect('a' in ref).toBe(true) }), ) it('should correctly trap calls to `ownKeys`', () => { const [ref, update] = createRawRef>({ a: 1, b: 2, }) expect(Object.keys(ref)).toEqual(['a', 'b']) delete ref.b expect(Reflect.ownKeys(ref)).toEqual(['a']) update({}) expect(Object.keys(ref)).toEqual([]) update({ a: 1, b: 2 }) expect(Object.keys(ref)).toEqual(['a', 'b']) }) it('should correctly trap calls to `getOwnPropertyDescriptor`', () => { const [ref, update] = createRawRef>({ a: 1, b: 2, }) expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual({ configurable: true, enumerable: true, get: expect.any(Function), set: expect.any(Function), }) delete ref.b expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual(undefined) update({}) expect(Reflect.getOwnPropertyDescriptor(ref, 'a')).toEqual(undefined) update({ a: 1, b: 2 }) expect(Reflect.getOwnPropertyDescriptor(ref, 'a')).toEqual({ configurable: true, enumerable: true, get: expect.any(Function), set: expect.any(Function), }) expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual({ configurable: true, enumerable: true, get: expect.any(Function), set: expect.any(Function), }) }) it('should lazily access values when using `update`', () => { let aAccessed = false let bAccessed = false const [ref, update] = createRawRef({ get a() { aAccessed = true return 1 }, get b() { bAccessed = true return 2 }, }) expect(aAccessed).toBe(false) expect(bAccessed).toBe(false) expect(ref.a).toBe(1) expect(aAccessed).toBe(true) expect(bAccessed).toBe(false) aAccessed = false bAccessed = false update({ get a() { aAccessed = true return 2 }, get b() { bAccessed = true return 3 }, }) expect(aAccessed).toBe(false) expect(bAccessed).toBe(false) expect(ref.a).toBe(2) expect(aAccessed).toBe(true) expect(bAccessed).toBe(false) }) it('should handle arrays', () => { const [ref, update] = createRawRef([1, 2, 3]) expect(ref).toEqual([1, 2, 3]) ref[0] = 4 expect(ref).toEqual([4, 2, 3]) update([5, 6]) expect(ref).toEqual([5, 6]) update([7, 8, 9]) expect(ref).toEqual([7, 8, 9]) }) it('should behave like a regular object when not using `update`', () => { const [ref] = createRawRef>({ a: 1, b: 2 }) expect(ref).toEqual({ a: 1, b: 2 }) ref.a = 3 expect(ref).toEqual({ a: 3, b: 2 }) ref.b = 4 expect(ref).toEqual({ a: 3, b: 4 }) ref.c = 5 expect(ref).toEqual({ a: 3, b: 4, c: 5 }) ref.fn = () => 6 expect(ref).toEqual({ a: 3, b: 4, c: 5, fn: expect.any(Function) }) expect((ref.fn as () => number)()).toBe(6) }) }) ================================================ FILE: packages/svelte-query/tests/context/BaseExample.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/context/context.svelte.test.ts ================================================ import { describe, expect, test } from 'vitest' import { render } from '@testing-library/svelte' import { getIsRestoringContext } from '../../src/index.js' import BaseExample from './BaseExample.svelte' describe('getQueryClientContext', () => { test('Throw when called without a client in context', () => { expect(() => render(BaseExample)).toThrowError( 'No QueryClient was found in Svelte context. Did you forget to wrap your component with QueryClientProvider?', ) }) }) describe('getIsRestoringContext', () => { test('Do not throw when called outside of a component', () => { expect(() => getIsRestoringContext()).not.toThrowError() }) }) ================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/BaseExample.svelte ================================================
    Status: {query.status}
    ================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/ChangeClient.svelte ================================================
    Data: {JSON.stringify(query.data)}
    ================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/SelectExample.svelte ================================================
    {query.data?.pages.join(',')}
    ================================================ FILE: packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import { QueryClient } from '@tanstack/query-core' import { ref } from '../utils.svelte.js' import BaseExample from './BaseExample.svelte' import SelectExample from './SelectExample.svelte' import ChangeClient from './ChangeClient.svelte' import type { QueryObserverResult } from '@tanstack/query-core' describe('createInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return the correct states for a successful query', async () => { let states = ref>([]) const rendered = render(BaseExample, { props: { states, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: success')).toBeInTheDocument() expect(states.value).toHaveLength(2) expect(states.value[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: false, hasPreviousPage: false, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: true, isPending: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, isEnabled: true, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states.value[1]).toEqual({ data: { pages: [0], pageParams: [0] }, dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, fetchNextPage: expect.any(Function), fetchPreviousPage: expect.any(Function), hasNextPage: true, hasPreviousPage: false, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isFetchNextPageError: false, isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: false, isPending: false, isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, isEnabled: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }) }) it('should be able to select a part of the data', async () => { let states = ref>([]) const rendered = render(SelectExample, { props: { states, }, }) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('count: 1')).toBeInTheDocument() expect(states.value).toHaveLength(2) expect(states.value[0]).toMatchObject({ data: undefined, isSuccess: false, }) expect(states.value[1]).toMatchObject({ data: { pages: ['count: 1'] }, isSuccess: true, }) }) it('should be able to set new pages with the query client', async () => { const queryClient = new QueryClient() const rendered = render(ChangeClient, { props: { queryClient, }, }) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('Data: {"pages":[0],"pageParams":[0]}'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setPages/i })) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('Data: {"pages":[7,8],"pageParams":[7,8]}'), ).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/createMutation/FailureExample.svelte ================================================
    Data: {mutation.data?.count ?? 'undefined'}
    Status: {mutation.status}
    Failure Count: {mutation.failureCount}
    Failure Reason: {mutation.failureReason ?? 'undefined'}
    ================================================ FILE: packages/svelte-query/tests/createMutation/OnSuccessExample.svelte ================================================
    Count: {count}
    ================================================ FILE: packages/svelte-query/tests/createMutation/ResetExample.svelte ================================================
    Error: {mutation.error?.message ?? 'undefined'}
    ================================================ FILE: packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { flushSync } from 'svelte' import { fireEvent, render } from '@testing-library/svelte' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { createMutation } from '../../src/index.js' import { withEffectRoot } from '../utils.svelte.js' import ResetExample from './ResetExample.svelte' import OnSuccessExample from './OnSuccessExample.svelte' import FailureExample from './FailureExample.svelte' describe('createMutation', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Able to reset `error`', async () => { const rendered = render(ResetExample) expect(rendered.queryByText('Error: undefined')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Error: Expected mock error')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Reset/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Error: undefined')).toBeInTheDocument() }) test('Able to call `onSuccess` and `onSettled` after each successful mutate', async () => { const onSuccessMock = vi.fn() const onSettledMock = vi.fn() const rendered = render(OnSuccessExample, { props: { onSuccessMock, onSettledMock, }, }) expect(rendered.queryByText('Count: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.queryByText('Count: 3')).toBeInTheDocument() expect(onSuccessMock).toHaveBeenCalledTimes(3) expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1) expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2) expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3) expect(onSettledMock).toHaveBeenCalledTimes(3) expect(onSettledMock).toHaveBeenNthCalledWith(1, 1) expect(onSettledMock).toHaveBeenNthCalledWith(2, 2) expect(onSettledMock).toHaveBeenNthCalledWith(3, 3) }) test('Set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => { type Value = { count: number } const mutationFn = vi.fn<(value: Value) => Promise>() mutationFn.mockImplementationOnce(() => sleep(20).then(() => Promise.reject(`Expected mock error`)), ) mutationFn.mockImplementation((value) => sleep(10).then(() => value)) const rendered = render(FailureExample, { props: { mutationFn, }, }) expect(rendered.queryByText('Data: undefined')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) expect(rendered.getByText('Data: undefined')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(21) expect(rendered.getByText('Status: error')).toBeInTheDocument() expect(rendered.getByText('Failure Count: 1')).toBeInTheDocument() expect( rendered.getByText('Failure Reason: Expected mock error'), ).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Status: pending')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('Status: success')).toBeInTheDocument() expect(rendered.getByText('Data: 2')).toBeInTheDocument() expect(rendered.getByText('Failure Count: 0')).toBeInTheDocument() expect(rendered.getByText('Failure Reason: undefined')).toBeInTheDocument() }) test( 'should recreate observer when queryClient changes', withEffectRoot(async () => { const queryClient1 = new QueryClient() const queryClient2 = new QueryClient() let queryClient = $state(queryClient1) const mutation = createMutation( () => ({ mutationFn: (params: string) => sleep(10).then(() => params), }), () => queryClient, ) mutation.mutate('first') await vi.advanceTimersByTimeAsync(11) expect(mutation.status).toBe('success') expect(mutation.data).toBe('first') queryClient = queryClient2 flushSync() expect(mutation.status).toBe('idle') expect(mutation.data).toBeUndefined() }), ) }) ================================================ FILE: packages/svelte-query/tests/createQueries.svelte.test.ts ================================================ import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' import { QueryClient, createQueries } from '../src/index.js' import { promiseWithResolvers, withEffectRoot } from './utils.svelte.js' import type { CreateQueryOptions, CreateQueryResult, QueryFunction, QueryFunctionContext, QueryKey, skipToken, } from '../src/index.js' describe('createQueries', () => { const queryClient = new QueryClient() afterEach(() => { queryClient.clear() }) it( 'should return the correct states', withEffectRoot(async () => { const key1 = ['test-1'] const key2 = ['test-2'] const results: Array> = [] const { promise: promise1, resolve: resolve1 } = promiseWithResolvers() const { promise: promise2, resolve: resolve2 } = promiseWithResolvers() const result = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => promise1, }, { queryKey: key2, queryFn: () => promise2, }, ], }), () => queryClient, ) $effect(() => { results.push([{ ...result[0] }, { ...result[1] }]) }) resolve1(1) await vi.waitFor(() => expect(result[0].data).toBe(1)) resolve2(2) await vi.waitFor(() => expect(result[1].data).toBe(2)) 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 }]) }), ) it( 'handles type parameter - tuple of tuples', withEffectRoot(() => { const key1 = ['test-key-1'] const key2 = ['test-key-2'] const key3 = ['test-key-3'] const result1 = createQueries< [[number], [string], [Array, boolean]] >( () => ({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }), () => queryClient, ) expectTypeOf(result1[0]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result1[1]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result1[2]).toEqualTypeOf< CreateQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // TData (3rd element) takes precedence over TQueryFnData (1st element) const result2 = createQueries< [[string, unknown, string], [string, unknown, number]] >( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }), () => queryClient, ) expectTypeOf(result2[0]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result2[1]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // types should be enforced createQueries<[[string, unknown, string], [string, boolean, number]]>( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }), () => queryClient, ) // field names should be enforced createQueries<[[string]]>( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', }, ], }), () => queryClient, ) }), ) it( 'handles type parameter - tuple of objects', withEffectRoot(() => { const key1 = ['test-key-1'] const key2 = ['test-key-2'] const key3 = ['test-key-3'] const result1 = createQueries< [ { queryFnData: number }, { queryFnData: string }, { queryFnData: Array; error: boolean }, ] >( () => ({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }), () => queryClient, ) expectTypeOf(result1[0]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result1[1]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result1[2]).toEqualTypeOf< CreateQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // TData (data prop) takes precedence over TQueryFnData (queryFnData prop) const result2 = createQueries< [ { queryFnData: string; data: string }, { queryFnData: string; data: number }, ] >( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }), () => queryClient, ) expectTypeOf(result2[0]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result2[1]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // can pass only TData (data prop) although TQueryFnData will be left unknown const result3 = createQueries<[{ data: string }, { data: number }]>( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as string }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as number }, }, ], }), () => queryClient, ) expectTypeOf(result3[0]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result3[1]).toEqualTypeOf< CreateQueryResult >() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() // types should be enforced createQueries< [ { queryFnData: string; data: string }, { queryFnData: string; data: number; error: boolean }, ] >( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }), () => queryClient, ) // field names should be enforced createQueries<[{ queryFnData: string }]>( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', }, ], }), () => queryClient, ) }), ) it( 'handles array literal without type parameter to infer result type', withEffectRoot(() => { const key1 = ['test-key-1'] const key2 = ['test-key-2'] const key3 = ['test-key-3'] const key4 = ['test-key-4'] // Array.map preserves TQueryFnData const result1 = createQueries( () => ({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, })), }), () => queryClient, ) expectTypeOf(result1).toEqualTypeOf< Array> >() if (result1[0]) { expectTypeOf(result1[0].data).toEqualTypeOf() } // Array.map preserves TData const result2 = createQueries( () => ({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }), () => queryClient, ) expectTypeOf(result2).toEqualTypeOf< Array> >() const result3 = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], select: () => 123, }, ], }), () => queryClient, ) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[2]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() // select takes precedence over queryFn expectTypeOf(result3[2].data).toEqualTypeOf() // initialData/placeholderData are enforced createQueries( () => ({ 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, }, ], }), () => queryClient, ) // select params are "indirectly" enforced createQueries( () => ({ 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), }, ], }), () => queryClient, ) // callbacks are also indirectly enforced with Array.map createQueries( () => ({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }), () => queryClient, ) // results inference works when all the handlers are defined const result4 = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), }, ], }), () => queryClient, ) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[2]).toEqualTypeOf>() // handles when queryFn returns a Promise const result5 = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => Promise.resolve('string'), }, ], }), () => queryClient, ) expectTypeOf(result5[0]).toEqualTypeOf>() // Array as const does not throw error const result6 = createQueries( () => ({ queries: [ { queryKey: ['key1'], queryFn: () => 'string', }, { queryKey: ['key1'], queryFn: () => 123, }, ], }) as const, () => queryClient, ) expectTypeOf(result6[0]).toEqualTypeOf>() expectTypeOf(result6[1]).toEqualTypeOf>() // field names should be enforced - array literal createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => 'string', }, ], }), () => queryClient, ) // field names should be enforced - Array.map() result createQueries( () => ({ // @ts-expect-error (invalidField) queries: Array(10).map(() => ({ someInvalidField: '', })), }), () => queryClient, ) // supports queryFn using fetch() to return Promise - Array.map() result createQueries( () => ({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => fetch('return Promise').then((resp) => resp.json()), })), }), () => queryClient, ) // supports queryFn using fetch() to return Promise - array literal createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }, ], }), () => queryClient, ) }), ) it( 'handles strongly typed queryFn factories and createQueries wrappers', withEffectRoot(() => { // QueryKey + queryFn factory type QueryKeyA = ['queryA'] const getQueryKeyA = (): QueryKeyA => ['queryA'] type GetQueryFunctionA = () => QueryFunction 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 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< CreateQueryOptions >, ) { return createQueries( () => ({ queries: queries.map( // no need to type the mapped query (query) => { const { queryFn: fn, queryKey: key } = query expectTypeOf(fn).toEqualTypeOf< | typeof skipToken | QueryFunction | undefined >() return { queryKey: key, queryFn: fn ? (ctx: QueryFunctionContext) => { // eslint-disable-next-line vitest/valid-expect expectTypeOf(ctx.queryKey) return ( fn as QueryFunction ).call({}, ctx) } : undefined, } }, ), }), () => queryClient, ) } const result = createQueries( () => ({ queries: [ { queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), }, { queryKey: getQueryKeyB('id'), queryFn: getQueryFunctionB(), }, ], }), () => queryClient, ) expectTypeOf(result[0]).toEqualTypeOf>() expectTypeOf(result[1]).toEqualTypeOf>() const withSelector = createQueries( () => ({ queries: [ { queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), select: getSelectorA(), }, { queryKey: getQueryKeyB('id'), queryFn: getQueryFunctionB(), select: getSelectorB(), }, ], }), () => queryClient, ) expectTypeOf(withSelector[0]).toEqualTypeOf< CreateQueryResult<[number, string], Error> >() expectTypeOf(withSelector[1]).toEqualTypeOf< CreateQueryResult<[string, number], Error> >() const withWrappedQueries = useWrappedQueries( Array(10).map(() => ({ queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), select: getSelectorA(), })), ) expectTypeOf(withWrappedQueries).toEqualTypeOf< Array> >() }), ) it( 'should track results', withEffectRoot(async () => { const key1 = ['test-track-results'] const results: Array> = [] let count = 0 const result = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => Promise.resolve(++count), }, ], }), () => queryClient, ) $effect(() => { results.push([result[0]]) }) await vi.waitFor(() => expect(result[0].data).toBe(1)) expect(results.length).toBe(2) expect(results[0]).toMatchObject([{ data: undefined }]) expect(results[1]).toMatchObject([{ data: 1 }]) // Trigger refetch result[0].refetch() await vi.waitFor(() => expect(result[0].data).toBe(2)) // Only one render for data update, no render for isFetching transition expect(results.length).toBe(3) expect(results[2]).toMatchObject([{ data: 2 }]) }), ) it( 'should combine queries', withEffectRoot(async () => { const key1 = ['test-combine-1'] const key2 = ['test-combine-2'] const { promise: promise1, resolve: resolve1 } = promiseWithResolvers() const { promise: promise2, resolve: resolve2 } = promiseWithResolvers() const queries = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => promise1, }, { queryKey: key2, queryFn: () => promise2, }, ], combine: (results) => { return { combined: true, res: results .flatMap((res) => (res.data ? [res.data] : [])) .join(','), } }, }), () => queryClient, ) // Initially both queries are loading expect(queries).toEqual({ combined: true, res: '', }) // Resolve the first query resolve1('first result') await vi.waitFor(() => expect(queries.res).toBe('first result')) // Resolve the second query resolve2('second result') await vi.waitFor(() => expect(queries.res).toBe('first result,second result'), ) expect(queries).toEqual({ combined: true, res: 'first result,second result', }) }), ) it( 'should track property access through combine function', withEffectRoot(async () => { const key1 = ['test-track-combine-1'] const key2 = ['test-track-combine-2'] let count = 0 const results: Array = [] const { promise: promise1, resolve: resolve1 } = promiseWithResolvers() const { promise: promise2, resolve: resolve2 } = promiseWithResolvers() const { promise: promise3, resolve: resolve3 } = promiseWithResolvers() const { promise: promise4, resolve: resolve4 } = promiseWithResolvers() const queries = createQueries( () => ({ queries: [ { queryKey: key1, queryFn: () => (count === 0 ? promise1 : promise3), }, { queryKey: key2, queryFn: () => (count === 0 ? promise2 : promise4), }, ], combine: (queryResults) => { return { combined: true, refetch: () => Promise.all(queryResults.map((res) => res.refetch())), res: queryResults .flatMap((res) => (res.data ? [res.data] : [])) .join(','), } }, }), () => queryClient, ) $effect(() => { results.push({ ...queries }) }) // Initially both queries are loading await vi.waitFor(() => expect(results[0]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: '', }), ) // Resolve the first query resolve1('first result ' + count) await vi.waitFor(() => expect(queries.res).toBe('first result 0')) expect(results[1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0', }) // Resolve the second query resolve2('second result ' + count) await vi.waitFor(() => expect(queries.res).toBe('first result 0,second result 0'), ) expect(results[2]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0,second result 0', }) // Increment count and refetch count++ queries.refetch() // Resolve the refetched queries resolve3('first result ' + count) resolve4('second result ' + count) await vi.waitFor(() => expect(queries.res).toBe('first result 1,second result 1'), ) const length = results.length expect(results.at(-1)).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 1,second result 1', }) // Refetch again but with the same data await queries.refetch() // No further re-render because data didn't change expect(results.length).toBe(length) }), ) }) ================================================ FILE: packages/svelte-query/tests/createQueries.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { createQueries, queryOptions } from '../src/index.js' import type { CreateQueryResult } from '../src/index.js' describe('createQueries', () => { it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = createQueries(() => ({ queries: [...queries1List, { ...Queries2.get() }], })) expectTypeOf(result).toEqualTypeOf< [ ...Array>, CreateQueryResult, ] >() }) }) ================================================ FILE: packages/svelte-query/tests/createQuery.svelte.test.ts ================================================ import { flushSync } from 'svelte' import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, createQuery, keepPreviousData } from '../src/index.js' import { promiseWithResolvers, withEffectRoot } from './utils.svelte.js' import type { CreateQueryResult } from '../src/index.js' describe('createQuery', () => { const queryClient = new QueryClient() const queryCache = queryClient.getQueryCache() afterEach(() => { queryClient.clear() }) it( 'should return the correct states for a successful query', withEffectRoot(async () => { const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: ['test'], queryFn: () => promise, }), () => queryClient, ) if (query.isPending) { expectTypeOf(query.data).toEqualTypeOf() expectTypeOf(query.error).toEqualTypeOf() } else if (query.isLoadingError) { expectTypeOf(query.data).toEqualTypeOf() expectTypeOf(query.error).toEqualTypeOf() } else { expectTypeOf(query.data).toEqualTypeOf() expectTypeOf(query.error).toEqualTypeOf() } const promise1 = query.promise expect(query).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isEnabled: true, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) resolve('resolved') await vi.waitFor(() => expect(query).toEqual({ data: 'resolved', dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isEnabled: true, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', promise: expect.any(Promise), }), ) expect(promise1).toBe(query.promise) }), ) it( 'should return the correct states for an unsuccessful query', withEffectRoot(async () => { let count = 0 const states: Array = [] const query = createQuery( () => ({ queryKey: ['test'], queryFn: () => { return Promise.reject(new Error('rejected #' + ++count)) }, retry: 1, retryDelay: 1, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => expect(query.isError).toBe(true)) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isEnabled: true, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 1, failureReason: new Error('rejected #1'), errorUpdateCount: 0, isEnabled: true, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isPending: true, isInitialLoading: true, isLoading: true, isLoadingError: false, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', promise: expect.any(Promise), }) expect(states[2]).toEqual({ data: undefined, dataUpdatedAt: 0, error: new Error('rejected #2'), errorUpdatedAt: expect.any(Number), failureCount: 2, failureReason: new Error('rejected #2'), errorUpdateCount: 1, isEnabled: true, isError: true, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isPending: false, isInitialLoading: false, isLoading: false, isLoadingError: true, isPlaceholderData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', promise: expect.any(Promise), }) }), ) it('should set isFetchedAfterMount to true after a query has been fetched', async () => { const key = ['test'] await queryClient.prefetchQuery({ queryKey: key, queryFn: () => Promise.resolve('prefetched'), }) await withEffectRoot(async () => { const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: key, queryFn: () => promise, }), () => queryClient, ) expect(query).toEqual( expect.objectContaining({ data: 'prefetched', isFetched: true, isFetchedAfterMount: false, }), ) resolve('resolved') await vi.waitFor(() => expect(query).toEqual( expect.objectContaining({ data: 'resolved', isFetched: true, isFetchedAfterMount: true, }), ), ) })() }) it( 'should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', withEffectRoot(async () => { const key = ['test'] let fetchCount = 0 const { promise, resolve } = promiseWithResolvers() const { refetch } = createQuery( () => ({ queryKey: key, queryFn: () => { fetchCount++ return promise }, enabled: false, initialData: 'initial', }), () => queryClient, ) refetch() refetch({ cancelRefetch: false }) resolve('resolved') await promise expect(fetchCount).toBe(1) }), ) it( 'should cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we have data already', withEffectRoot(async () => { const key = ['test'] let fetchCount = 0 const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: key, queryFn: async () => { fetchCount++ return promise }, enabled: false, initialData: 'initialData', }), () => queryClient, ) // Trigger two refetch close together query.refetch() query.refetch() resolve('resolved') await promise expect(fetchCount).toBe(2) }), ) it( 'should not cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we do not have data yet', withEffectRoot(async () => { const key = ['test'] let fetchCount = 0 const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: key, queryFn: async () => { fetchCount++ return promise }, enabled: false, }), () => queryClient, ) // Trigger two refetch close together query.refetch() query.refetch() resolve('resolved') await promise expect(fetchCount).toBe(1) }), ) it( 'should be able to watch a query without providing a query function', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] queryClient.setQueryDefaults(key, { queryFn: () => 'data', }) const query = createQuery( () => ({ queryKey: key }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe('data') }) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'data' }) }), ) it('should pick up a query when re-mounting with gcTime 0', async () => { // this needs to be split into two different effect roots because // effects won't pick up dependencies created after the first `await` // -- the two roots effectively emulate two consecutive components being rendered await withEffectRoot(async () => { const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: ['test'], queryFn: () => promise, gcTime: 0, notifyOnChangeProps: 'all', }), () => queryClient, ) expect(query).toMatchObject({ isPending: true, isSuccess: false, isFetching: true, }) resolve('resolved: 1') await vi.waitFor(() => expect(query.data).toBe('resolved: 1')) expect(query).toMatchObject({ isPending: false, isSuccess: true, isFetching: false, }) })() await withEffectRoot(async () => { const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: ['test'], queryFn: () => promise, gcTime: 0, notifyOnChangeProps: 'all', }), () => queryClient, ) expect(query).toMatchObject({ data: 'resolved: 1', isPending: false, isSuccess: true, isFetching: true, }) resolve('resolved: 2') await vi.waitFor(() => expect(query.data).toBe('resolved: 2')) expect(query).toMatchObject({ data: 'resolved: 2', isPending: false, isSuccess: true, isFetching: false, }) })() }) it('should not get into an infinite loop when removing a query with gcTime 0 and rerendering', async () => { const key = ['test'] const states: Array> = [] // First mount: render the query and let it fetch await withEffectRoot(async () => { const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('data'), gcTime: 0, notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe('data') }) })() // Simulate rerender by removing the query and mounting again await withEffectRoot(async () => { queryClient.removeQueries({ queryKey: key }) const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('data'), gcTime: 0, notifyOnChangeProps: ['isPending', 'isSuccess', 'data'], }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe('data') }) // Give it time to catch any accidental infinite updates await new Promise((r) => setTimeout(r, 100)) })() expect(states.length).toBe(4) expect(states[0]).toMatchObject({ isPending: true, isSuccess: false, data: undefined, }) expect(states[1]).toMatchObject({ isPending: false, isSuccess: true, data: 'data', }) expect(states[2]).toMatchObject({ isPending: true, isSuccess: false, data: undefined, }) expect(states[3]).toMatchObject({ isPending: false, isSuccess: true, data: 'data', }) }) it( 'should fetch when refetchOnMount is false and nothing has been fetched yet', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] const query = createQuery( () => ({ queryKey: key, queryFn: () => 'test', refetchOnMount: false, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe('test') }) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }), ) it( 'should not fetch when refetchOnMount is false and data has been fetched already', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] queryClient.setQueryData(key, 'prefetched') const query = createQuery( () => ({ queryKey: key, queryFn: () => 'test', refetchOnMount: false, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe('prefetched') }) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: 'prefetched' }) }), ) it( 'should be able to select a part of the data with select', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] const query = createQuery<{ name: string }, Error, string>( () => ({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe('test') }) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }), ) it( 'should throw an error when a selector throws', withEffectRoot(async () => { const key = ['test'] const error = new Error('Select Error') const states: Array> = [] const query = createQuery<{ name: string }, Error, string>( () => ({ queryKey: key, queryFn: () => ({ name: 'test' }), select: () => { throw error }, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.status).toBe('error') }) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ status: 'pending', data: undefined }) expect(states[1]).toMatchObject({ status: 'error', error }) }), ) it( 'should be able to remove a query', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => ++count, notifyOnChangeProps: 'all', }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => expect(query.data).toBe(1)) queryClient.removeQueries({ queryKey: key }) await query.refetch() await vi.waitFor(() => expect(query.data).toBe(2)) expect(states.length).toBe(4) expect(states[0]).toMatchObject({ status: 'pending', data: undefined, dataUpdatedAt: 0, }) expect(states[1]).toMatchObject({ status: 'success', data: 1 }) expect(states[2]).toMatchObject({ status: 'pending', data: undefined, dataUpdatedAt: 0, }) expect(states[3]).toMatchObject({ status: 'success', data: 2 }) }), ) it( 'keeps up-to-date with query key changes', withEffectRoot(async () => { let search = $state('') const states: Array> = [] const query = createQuery( () => ({ queryKey: ['products', search], queryFn: async () => Promise.resolve(search), placeholderData: keepPreviousData, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => expect(query.data).toBe('')) search = 'phone' await vi.waitFor(() => expect(query.data).toBe('phone')) expect(states.length).toBe(4) expect(states[0]).toMatchObject({ status: 'pending', fetchStatus: 'fetching', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: '', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: '', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'phone', }) }), ) it( 'should create a new query when refetching a removed query', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve(++count), }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => { expect(query.data).toBe(1) }) queryClient.removeQueries({ queryKey: key }) await query.refetch() await vi.waitFor(() => { expect(query.data).toBe(2) }) expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[1]).toMatchObject({ data: 1 }) // Switch expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[3]).toMatchObject({ data: 2 }) }), ) it( 'should share equal data structures between query results', withEffectRoot(async () => { const key = ['test'] const result1 = [ { id: '1', done: false }, { id: '2', done: false }, ] const result2 = [ { id: '1', done: false }, { id: '2', done: true }, ] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => { count++ return Promise.resolve(count === 1 ? result1 : result2) }, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => expect(query.data?.[1]?.done).toBe(false)) await query.refetch() await vi.waitFor(() => expect(query.data?.[1]?.done).toBe(true)) expect(states.length).toBe(4) const todos = states[1]?.data const todo1 = todos?.[0] const todo2 = todos?.[1] const newTodos = states[3]?.data const newTodo1 = newTodos?.[0] const newTodo2 = newTodos?.[1] expect(todos).toEqual(result1) expect(newTodos).toEqual(result2) expect(newTodos).not.toBe(todos) expect(newTodo1).toBe(todo1) expect(newTodo2).not.toBe(todo2) }), ) it( 'should use query function from hook when the existing query does not have a query function', withEffectRoot(async () => { const key = ['test'] queryClient.setQueryData(key, 'set') const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('fetched'), initialData: 'initial', staleTime: Infinity, }), () => queryClient, ) await vi.waitFor(() => expect(query.data).toBe('set')) queryClient.refetchQueries({ queryKey: key }) await vi.waitFor(() => expect(query.data).toBe('fetched')) }), ) it( 'should update query stale state and refetch when invalidated with invalidateQueries', withEffectRoot(async () => { const key = ['test'] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve(++count), staleTime: Infinity, }), () => queryClient, ) await vi.waitFor(() => expect(query).toEqual( expect.objectContaining({ data: 1, isStale: false, isFetching: false, }), ), ) queryClient.invalidateQueries({ queryKey: key }) await vi.waitFor(() => expect(query).toEqual( expect.objectContaining({ data: 1, isStale: true, isFetching: true, }), ), ) await vi.waitFor(() => expect(query).toEqual( expect.objectContaining({ data: 2, isStale: false, isFetching: false, }), ), ) }), ) it( 'should not update disabled query when refetching with refetchQueries', withEffectRoot(async () => { const key = ['test'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve(++count), enabled: false, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await sleep(50) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isSuccess: false, isFetching: false, isStale: false, }) }), ) it( 'should not refetch disabled query when invalidated with invalidateQueries', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve(++count), enabled: false, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) queryClient.invalidateQueries({ queryKey: key }) // Wait long enough for the invalidation and potential refetch await sleep(100) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: false, }) }), ) it( 'should not fetch when switching to a disabled query', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = $state(0) const query = createQuery( () => ({ queryKey: [key, count], queryFn: () => Promise.resolve(count), enabled: count === 0, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) await vi.waitFor(() => expect(query.data).toBe(0)) count = 1 await vi.waitFor(() => expect(states.length).toBe(3)) // Fetch query expect(states[0]).toMatchObject({ isFetching: true, isSuccess: false, }) // Fetched query expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, }) // Switch to disabled query expect(states[2]).toMatchObject({ isFetching: false, isSuccess: false, }) }), ) it( 'should keep the previous data when placeholderData is set', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = $state(0) const query = createQuery( () => ({ queryKey: [key, count], queryFn: () => Promise.resolve(count), placeholderData: keepPreviousData, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the initial fetch to complete await vi.waitFor(() => expect(query.data).toBe(0)) // Update count to trigger a new fetch count = 1 // Wait for all state updates to complete await vi.waitFor(() => expect(states.length).toBe(4)) // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }), ) it( 'should not show initial data from next query if placeholderData is set', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = $state(0) const query = createQuery( () => ({ queryKey: [key, count], queryFn: () => Promise.resolve(count), initialData: 99, placeholderData: keepPreviousData, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the initial fetch to complete await vi.waitFor(() => expect(query.data).toBe(0)) // Update count to trigger a new fetch count = 1 // Wait for the new fetch to complete await vi.waitFor(() => expect(query.data).toBe(1)) // Wait for all state updates to complete await vi.waitFor(() => expect(states.length).toBe(4)) // Initial expect(states[0]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPlaceholderData: false, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }), ) it( 'should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] // Set initial query data queryClient.setQueryData([key, 10], 10) let count = $state(10) const query = createQuery( () => ({ queryKey: [key, count], queryFn: () => Promise.resolve(count), enabled: false, placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // let that effect ^ run to push the initial state flushSync() flushSync(() => (count = 11)) flushSync(() => (count = 12)) await query.refetch() // Wait for all operations to complete await vi.waitFor(() => expect(query.data).toBe(12)) // Disabled query expect(states[0]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: false, }) // Set state (11) expect(states[1]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // Set state (12) expect(states[2]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPlaceholderData: true, }) // Refetch expect(states[3]).toMatchObject({ data: 10, isFetching: true, isSuccess: true, isPlaceholderData: true, }) // Refetch done expect(states[4]).toMatchObject({ data: 12, isFetching: false, isSuccess: true, isPlaceholderData: false, }) }), ) it( 'should use the correct query function when components use different configurations', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] const { promise, resolve } = promiseWithResolvers() // Simulate FirstComponent const firstQuery = createQuery( () => ({ queryKey: key, queryFn: () => promise, }), () => queryClient, ) $effect(() => { states.push({ ...firstQuery }) }) // Simulate SecondComponent createQuery( () => ({ queryKey: key, queryFn: () => 2, }), () => queryClient, ) // Resolve the first query resolve(1) // Wait for the first query to complete await vi.waitFor(() => expect(firstQuery.data).toBe(1)) // Refetch the first query await firstQuery.refetch() // Wait for all state updates to complete await vi.waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ data: undefined, }) expect(states[1]).toMatchObject({ data: 1, }) expect(states[2]).toMatchObject({ data: 1, }) // This state should be 1 instead of 2 expect(states[3]).toMatchObject({ data: 1, }) }), ) it.todo( 'should be able to set different stale times for a query', async () => { /** * TODO: There's a super weird bug with this test, and I think it's caused by a race condition in query-core. * * If you add this to the top `updateResult` in `packages/query-core/src/queryObserver.ts:647`: * ``` * for (let i = 0; i < 10_000_000; i++) { * continue * } * ``` * * This test will miraculously start to pass. I'm suspicious that there's some race condition between props * being tracked and `updateResult` being called, but that _should_ be fixed by `notifyOnChangeProps: 'all'`, * and that's not doing anything. * * This test will also start to magically pass if you put `$inspect(firstQuery)` before `vi.waitFor` near * the end of the test. */ const key = ['test-key'] const states1: Array> = [] const states2: Array> = [] // Prefetch the query await queryClient.prefetchQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'prefetch' }, }) await vi.waitFor(() => expect(queryClient.getQueryState(key)?.data).toBe('prefetch'), ) await withEffectRoot(async () => { const firstQuery = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('one'), staleTime: 100, }), () => queryClient, ) $effect(() => { states1.push({ ...firstQuery }) }) const secondQuery = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('two'), staleTime: 10, }), () => queryClient, ) $effect(() => { states2.push({ ...secondQuery }) }) await vi.waitFor(() => { expect(firstQuery).toMatchObject({ data: 'two', isStale: true }) expect(secondQuery).toMatchObject({ data: 'two', isStale: true }) }) expect(states1).toMatchObject([ // First render { data: 'prefetch', isStale: false, }, // Second createQuery started fetching { data: 'prefetch', isStale: false, }, // Second createQuery data came in { data: 'two', isStale: false, }, // Data became stale after 100ms { data: 'two', isStale: true, }, ]) expect(states2).toMatchObject([ // First render, data is stale and starts fetching { data: 'prefetch', isStale: true, }, // Second createQuery data came in { data: 'two', isStale: false, }, // Data became stale after 10ms { data: 'two', isStale: true, }, ]) })() }, ) it( 'should re-render when a query becomes stale', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] const query = createQuery( () => ({ queryKey: key, queryFn: () => 'test', staleTime: 50, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the query to become stale await sleep(100) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ isStale: true }) expect(states[1]).toMatchObject({ isStale: false }) expect(states[2]).toMatchObject({ isStale: true }) }), ) it( 'should not re-render when it should only re-render on data changes and the data did not change', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: key, queryFn: () => promise, notifyOnChangeProps: ['data'], }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) resolve('test') // Refetch the query setTimeout(() => { query.refetch() }, 10) await vi.waitFor(() => { expect(states.length).toBe(2) }) expect(states[0]).toMatchObject({ data: undefined, status: 'pending', isFetching: true, }) expect(states[1]).toMatchObject({ data: 'test', status: 'success', isFetching: false, }) }), ) it( 'should track properties and only re-render when a tracked property changes', withEffectRoot(async () => { const key = ['test-key'] const states: Array = [] const { promise, resolve } = promiseWithResolvers() const query = createQuery( () => ({ queryKey: key, queryFn: () => promise, }), () => queryClient, ) $effect(() => { states.push(query.data) }) // Resolve the promise after a delay setTimeout(() => { resolve('test') }, 10) await vi.waitFor(() => expect(query.data).toBe('test')) // Refetch after data is available setTimeout(() => { if (query.data) { query.refetch() } }, 20) // Wait for refetch to complete await sleep(30) expect(states.length).toBe(2) expect(states[0]).toBe(undefined) expect(states[1]).toBe('test') }), ) it( 'should always re-render if we are tracking props but not using any', withEffectRoot(async () => { const key = ['test-key'] let renderCount = 0 const states: Array> = [] const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('test'), }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Track changes to the query state $effect(() => { // @ts-expect-error const _ = { ...query } renderCount++ }) await vi.waitFor(() => expect(query.data).toBe('test')) expect(renderCount).toBe(2) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }), ) it( 'should update query options', withEffectRoot(() => { const key = ['test-key'] const queryFn = async () => { await sleep(10) return 'data1' } // Create two queries with the same key but different options createQuery( () => ({ queryKey: key, queryFn, retryDelay: 10 }), () => queryClient, ) createQuery( () => ({ queryKey: key, queryFn, retryDelay: 20 }), () => queryClient, ) // The last options should win expect(queryCache.find({ queryKey: key })!.options.retryDelay).toBe(20) }), ) it( 'should start with status pending, fetchStatus idle if enabled is false', withEffectRoot(async () => { const key1 = ['test-key-1'] const key2 = ['test-key-2'] const states1: Array> = [] const states2: Array> = [] const query1 = createQuery( () => ({ queryKey: key1, queryFn: () => 'data', enabled: false, }), () => queryClient, ) const query2 = createQuery( () => ({ queryKey: key2, queryFn: () => 'data', }), () => queryClient, ) $effect(() => { states1.push({ ...query1 }) }) $effect(() => { states2.push({ ...query2 }) }) // Check initial states expect(query1.status).toBe('pending') expect(query1.fetchStatus).toBe('idle') // Wait for second query to complete await vi.waitFor(() => { expect(query2.status).toBe('success') expect(query2.fetchStatus).toBe('idle') }) // Verify the state transitions for the second query expect(states2[0]?.status).toBe('pending') expect(states2[0]?.fetchStatus).toBe('fetching') }), ) it( 'should be in "pending" state by default', withEffectRoot(() => { const key = ['test-key'] const query = createQuery( () => ({ queryKey: key, queryFn: () => new Promise(() => {}), }), () => queryClient, ) expect(query.status).toBe('pending') }), ) it( 'should not refetch query on focus when `enabled` is set to `false`', withEffectRoot(async () => { const key = ['test-key'] const queryFn = vi.fn().mockReturnValue('data') const query = createQuery( () => ({ queryKey: key, queryFn, enabled: false, }), () => queryClient, ) // Wait a bit to ensure the query has time to settle await sleep(10) // Simulate window focus window.dispatchEvent(new Event('visibilitychange')) // Wait a bit more to ensure no refetch happens await sleep(10) // The query function should not have been called expect(queryFn).not.toHaveBeenCalled() // Data should be undefined since the query is disabled expect(query.data).toBeUndefined() }), ) it( 'should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => count++, staleTime: 0, refetchOnWindowFocus: false, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the initial fetch to complete await vi.waitFor(() => expect(query.data).toBe(0)) // Simulate window focus window.dispatchEvent(new Event('visibilitychange')) // Wait a bit to ensure no refetch happens await sleep(10) // Should only have 2 states: initial and after fetch expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) // Count should still be 0 since no refetch occurred expect(count).toBe(1) }), ) it( 'should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => count++, staleTime: 0, refetchOnWindowFocus: () => false, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the initial fetch to complete await vi.waitFor(() => expect(query.data).toBe(0)) // Simulate window focus window.dispatchEvent(new Event('visibilitychange')) // Wait a bit to ensure no refetch happens await sleep(10) // Should only have 2 states: initial and after fetch expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) // Count should still be 0 since no refetch occurred expect(count).toBe(1) }), ) it( 'should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', withEffectRoot(async () => { const key = ['test-key'] const states: Array> = [] let count = 0 const query = createQuery( () => ({ queryKey: key, queryFn: () => count++, staleTime: Infinity, refetchOnWindowFocus: true, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the initial fetch to complete await vi.waitFor(() => expect(query.data).toBe(0)) // Simulate window focus window.dispatchEvent(new Event('visibilitychange')) // Wait a bit to ensure no refetch happens await sleep(10) // Should only have 2 states: initial and after fetch expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) // Count should still be 0 since no refetch occurred expect(count).toBe(1) }), ) it('should refetch fresh query when refetchOnMount is set to always', async () => { const key = ['test-key'] const states: Array> = [] // Prefetch the query await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) await withEffectRoot(async () => { const query = createQuery( () => ({ queryKey: key, queryFn: () => 'data', refetchOnMount: 'always', staleTime: Infinity, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the refetch to complete await vi.waitFor(() => expect(query.data).toBe('data')) // Should have 2 states: initial (with prefetched data) and after refetch expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) })() }) it('should refetch stale query when refetchOnMount is set to true', async () => { const key = ['test-key'] const states: Array> = [] // Prefetch the query await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'prefetched', }) await withEffectRoot(async () => { const query = createQuery( () => ({ queryKey: key, queryFn: () => 'data', refetchOnMount: true, staleTime: 0, }), () => queryClient, ) $effect(() => { states.push({ ...query }) }) // Wait for the refetch to complete await vi.waitFor(() => expect(query.data).toBe('data')) // Should have 2 states: initial (with prefetched data) and after refetch expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) })() }) it( 'should set status to error if queryFn throws', withEffectRoot(async () => { const key = ['test-key'] // Declare key variable const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.reject(new Error('Error test')), retry: false, }), () => queryClient, ) await vi.waitFor(() => expect(query.status).toBe('error')) expect(query.error?.message).toBe('Error test') consoleMock.mockRestore() }), ) it( 'should set status to error instead of throwing when error should not be thrown', withEffectRoot(async () => { const key = ['test-key'] // Declare key variable const query = createQuery( () => ({ queryKey: key, queryFn: () => Promise.reject(new Error('Local Error')), retry: false, throwOnError: (err) => err.message !== 'Local Error', }), () => queryClient, ) await vi.waitFor(() => expect(query.status).toBe('error')) expect(query.error?.message).toBe('Local Error') }), ) it( 'should support changing provided query client', withEffectRoot(async () => { const queryClient1 = new QueryClient() const queryClient2 = new QueryClient() let queryClient = $state(queryClient1) const key = ['test'] createQuery( () => ({ queryKey: key, queryFn: () => Promise.resolve('prefetched'), }), () => queryClient, ) expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined() queryClient = queryClient2 flushSync() expect(queryClient2.getQueryCache().find({ queryKey: key })).toBeDefined() }), ) }) ================================================ FILE: packages/svelte-query/tests/createQuery.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { createQuery, queryOptions } from '../src/index.js' describe('initialData', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const { data } = createQuery(() => ({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, })) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => ({ wow: true }), initialData: { wow: true }, }) const { data } = createQuery(() => options) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should have undefined in the union when initialData is NOT provided', () => { const { data } = createQuery(() => ({ queryKey: ['key'], 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 } = createQuery(() => ({ queryKey: ['key'], 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 } = createQuery(() => ({ queryKey: ['key'], 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 } = createQuery(() => ({ queryKey: ['key'], 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 } = createQuery(() => ({ queryKey: ['key'], 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 } = createQuery(() => ({ queryKey: ['key'], queryFn: () => ({ wow: true }), })) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) }) }) ================================================ FILE: packages/svelte-query/tests/infiniteQueryOptions.svelte.test.ts ================================================ import { describe, expect, it } from 'vitest' import { infiniteQueryOptions } from '../src/index.js' import type { CreateInfiniteQueryOptions } from '../src/types.js' describe('infiniteQueryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: CreateInfiniteQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), getNextPageParam: () => null, initialPageParam: null, } as const expect(infiniteQueryOptions(object)).toBe(object) }) }) ================================================ FILE: packages/svelte-query/tests/infiniteQueryOptions.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { createInfiniteQuery, infiniteQueryOptions } from '../src/index.js' import type { InfiniteData } from '@tanstack/query-core' describe('queryOptions', () => { test('Should not allow excess properties', () => { infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), getNextPageParam: () => 1, initialPageParam: 1, // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }) }) test('Should infer types for callbacks', () => { infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), staleTime: 1000, getNextPageParam: () => 1, initialPageParam: 1, select: (data) => { expectTypeOf(data).toEqualTypeOf>() }, }) }) test('Should work when passed to createInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const query = createInfiniteQuery(() => options) // known issue: type of pageParams is unknown when returned from useInfiniteQuery expectTypeOf(query.data).toEqualTypeOf< InfiniteData | undefined >() }) test('Should work when passed to fetchInfiniteQuery', async () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const data = await new QueryClient().fetchInfiniteQuery(options) expectTypeOf(data).toEqualTypeOf>() }) }) ================================================ FILE: packages/svelte-query/tests/mutationOptions/BaseExample.svelte ================================================
    isMutating: {isMutating.current}
    clientIsMutating: {clientIsMutating}
    mutationState: {JSON.stringify(mutationState.map((state) => state.data))}
    ================================================ FILE: packages/svelte-query/tests/mutationOptions/MultiExample.svelte ================================================
    isMutating: {isMutating.current}
    clientIsMutating: {clientIsMutating}
    mutationState: {JSON.stringify(mutationState.map((state) => state.data))}
    ================================================ FILE: packages/svelte-query/tests/mutationOptions/mutationOptions.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import { sleep } from '@tanstack/query-test-utils' import { mutationOptions } from '../../src/index.js' import BaseExample from './BaseExample.svelte' import MultiExample from './MultiExample.svelte' describe('mutationOptions', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { 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)).toStrictEqual(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)).toStrictEqual(object) }) it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data'), }) const rendered = render(BaseExample, { props: { mutationOpts: () => mutationOpts }, }) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data'), }) const rendered = render(BaseExample, { props: { mutationOpts: () => mutationOpts }, }) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with useIsMutating', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) const rendered = render(MultiExample, { props: { mutationOpts1: () => mutationOpts1, mutationOpts2: () => mutationOpts2, }, }) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isMutating: 2')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['key'], mutationFn: () => sleep(50).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(50).then(() => 'data2'), }) const rendered = render(MultiExample, { props: { mutationOpts1: () => mutationOpts1, mutationOpts2: () => mutationOpts2, isMutatingFilters: { mutationKey: mutationOpts1.mutationKey }, }, }) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(51) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data'), }) const rendered = render(BaseExample, { props: { mutationOpts: () => mutationOpts, isMutatingFilters: { mutationKey: mutationOpts.mutationKey }, }, }) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('clientIsMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(501) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data'), }) const rendered = render(BaseExample, { props: { mutationOpts: () => mutationOpts }, }) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('clientIsMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(501) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with queryClient.isMutating', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) const rendered = render(MultiExample, { props: { mutationOpts1: () => mutationOpts1, mutationOpts2: () => mutationOpts2, }, }) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('clientIsMutating: 2')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(501) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() }) it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpts1.mutationKey)', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(500).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(500).then(() => 'data2'), }) const rendered = render(MultiExample, { props: { mutationOpts1: () => mutationOpts1, mutationOpts2: () => mutationOpts2, isMutatingFilters: { mutationKey: mutationOpts1.mutationKey }, }, }) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('clientIsMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(501) expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument() }) it('should return mutation states when used with useMutationState (with mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data'), }) const rendered = render(BaseExample, { props: { mutationOpts: () => mutationOpts, mutationStateOpts: { filters: { mutationKey: mutationOpts.mutationKey, status: 'success', }, }, }, }) expect(rendered.getByText('mutationState: []')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('mutationState: ["data"]')).toBeInTheDocument() }) it('should return mutation states when used with useMutationState (without mutationKey in mutationOptions)', async () => { const mutationOpts = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data'), }) const rendered = render(BaseExample, { props: { mutationOpts: () => mutationOpts, mutationStateOpts: { filters: { status: 'success' }, }, }, }) expect(rendered.getByText('mutationState: []')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('mutationState: ["data"]')).toBeInTheDocument() }) it('should return mutation states when used with useMutationState', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data2'), }) const rendered = render(MultiExample, { props: { mutationOpts1: () => mutationOpts1, mutationOpts2: () => mutationOpts2, mutationStateOpts: { filters: { status: 'success' }, }, }, }) expect(rendered.getByText('mutationState: []')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect( rendered.getByText('mutationState: ["data1","data2"]'), ).toBeInTheDocument() }) it('should return mutation states when used with useMutationState (filter mutationOpts1.mutationKey)', async () => { const mutationOpts1 = mutationOptions({ mutationKey: ['mutation'], mutationFn: () => sleep(10).then(() => 'data1'), }) const mutationOpts2 = mutationOptions({ mutationFn: () => sleep(10).then(() => 'data2'), }) const rendered = render(MultiExample, { props: { mutationOpts1: () => mutationOpts1, mutationOpts2: () => mutationOpts2, mutationStateOpts: { filters: { mutationKey: mutationOpts1.mutationKey, status: 'success', }, }, }, }) expect(rendered.getByText('mutationState: []')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('mutationState: ["data1"]')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/mutationOptions/mutationOptions.test-d.ts ================================================ import { assertType, describe, expectTypeOf, test } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { createMutation, mutationOptions, useIsMutating, useMutationState, } from '../../src/index.js' import type { DefaultError, MutationFunctionContext, MutationState, WithRequired, } from '@tanstack/query-core' import type { CreateMutationOptions, CreateMutationResult, } from '../../src/types.js' describe('mutationOptions', () => { test('Should not allow excess properties', () => { // @ts-expect-error this is a good error, because onMutates does not exist! mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutates: 1000, onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) test('Should infer types for callbacks', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) test('Should infer types for onError callback', () => { mutationOptions({ mutationFn: () => { throw new Error('fail') }, mutationKey: ['key'], onError: (error) => { expectTypeOf(error).toEqualTypeOf() }, }) }) test('Should infer types for variables', () => { mutationOptions({ mutationFn: (vars) => { expectTypeOf(vars).toEqualTypeOf<{ id: string }>() return Promise.resolve(5) }, mutationKey: ['with-vars'], }) }) test('Should infer result type correctly', () => { mutationOptions({ mutationFn: () => Promise.resolve(5), mutationKey: ['key'], onMutate: () => { return { name: 'onMutateResult' } }, onSuccess: (_data, _variables, onMutateResult) => { expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>() }, }) }) test('Should infer context type correctly', () => { mutationOptions({ mutationFn: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() return Promise.resolve(5) }, mutationKey: ['key'], onMutate: (_variables, context) => { expectTypeOf(context).toEqualTypeOf() }, onSuccess: (_data, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onError: (_error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, onSettled: (_data, _error, _variables, _onMutateResult, context) => { expectTypeOf(context).toEqualTypeOf() }, }) }) test('Should error if mutationFn return type mismatches TData', () => { assertType( mutationOptions({ // @ts-expect-error this is a good error, because return type is string, not number mutationFn: async () => Promise.resolve('wrong return'), }), ) }) test('Should allow mutationKey to be omitted', () => { return mutationOptions({ mutationFn: () => Promise.resolve(123), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) test('Should infer all types when not explicitly provided', () => { expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), mutationKey: ['key'], onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< WithRequired< CreateMutationOptions, 'mutationKey' > >() expectTypeOf( mutationOptions({ mutationFn: (id: string) => Promise.resolve(id.length), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ).toEqualTypeOf< Omit, 'mutationKey'> >() }) test('Should work when used with createMutation', () => { const mutation = createMutation(() => mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) expectTypeOf(mutation).toEqualTypeOf< CreateMutationResult >() createMutation(() => // should allow when used with createMutation without mutationKey mutationOptions({ mutationFn: () => Promise.resolve('data'), onSuccess: (data) => { expectTypeOf(data).toEqualTypeOf() }, }), ) }) test('Should work when used with useIsMutating', () => { const isMutating = useIsMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating.current).toEqualTypeOf() useIsMutating( // @ts-expect-error filters should have mutationKey mutationOptions({ mutationFn: () => Promise.resolve(5), }), ) }) test('Should work when used with queryClient.isMutating', () => { const queryClient = new QueryClient() const isMutating = queryClient.isMutating( mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), ) expectTypeOf(isMutating).toEqualTypeOf() queryClient.isMutating( // @ts-expect-error filters should have mutationKey mutationOptions({ mutationFn: () => Promise.resolve(5), }), ) }) test('Should work when used with useMutationState', () => { const mutationState = useMutationState({ filters: mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5), }), }) expectTypeOf(mutationState).toEqualTypeOf< Array> >() useMutationState({ // @ts-expect-error filters should have mutationKey filters: mutationOptions({ mutationFn: () => Promise.resolve(5), }), }) }) }) ================================================ FILE: packages/svelte-query/tests/queryOptions.svelte.test.ts ================================================ import { describe, expect, it } from 'vitest' import { queryOptions } from '../src/index.js' 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) }) }) ================================================ FILE: packages/svelte-query/tests/queryOptions.test-d.ts ================================================ import { describe, expectTypeOf, test } from 'vitest' import { QueriesObserver, QueryClient, dataTagSymbol, skipToken, } from '@tanstack/query-core' import { createQueries, queryOptions } from '../src/index.js' import type { QueryObserverResult } from '@tanstack/query-core' describe('queryOptions', () => { test('Should not allow excess properties', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }) }) test('Should infer types for callbacks', () => { queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) test('Should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const data = await new QueryClient().fetchQuery(options) expectTypeOf(data).toEqualTypeOf() }) test('Should work when passed to createQueries', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queries = createQueries(() => ({ queries: [options], })) expectTypeOf(queries[0].data).toEqualTypeOf() }) test('Should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) test('Should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) test('Should return the proper type when passed to getQueryState', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const state = queryClient.getQueryState(queryKey) expectTypeOf(state?.data).toEqualTypeOf() }) test('Should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) test('Should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) test('Should infer even if there is a conditional skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) test('Should infer to unknown if we disable a query with just a skipToken', () => { const options = queryOptions({ queryKey: ['key'], queryFn: skipToken, }) const queryClient = new QueryClient() const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) test('Should return the proper type when passed to QueriesObserver', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const queriesObserver = new QueriesObserver(queryClient, [options]) expectTypeOf(queriesObserver).toEqualTypeOf< QueriesObserver> >() }) test('Should allow undefined response in initialData', () => { return (id: string | null) => queryOptions({ queryKey: ['todo', id], queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry', }), initialData: () => !id ? undefined : { id, title: 'Initial Data', }, }) }) }) ================================================ FILE: packages/svelte-query/tests/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' ================================================ FILE: packages/svelte-query/tests/useIsFetching/BaseExample.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/useIsFetching/FetchStatus.svelte ================================================
    isFetching: {isFetching.current}
    ================================================ FILE: packages/svelte-query/tests/useIsFetching/Query.svelte ================================================
    Data: {query.data ?? 'undefined'}
    ================================================ FILE: packages/svelte-query/tests/useIsFetching/useIsFetching.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import BaseExample from './BaseExample.svelte' describe('useIsFetching', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should update as queries start and stop fetching', async () => { const rendered = render(BaseExample) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /setReady/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/useIsMutating/BaseExample.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/useIsMutating/MutatingStatus.svelte ================================================
    isMutating: {isMutating.current}
    ================================================ FILE: packages/svelte-query/tests/useIsMutating/Query.svelte ================================================ ================================================ FILE: packages/svelte-query/tests/useIsMutating/useIsMutating.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import BaseExample from './BaseExample.svelte' describe('useIsMutating', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should update as queries start and stop mutating', async () => { const rendered = render(BaseExample) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Trigger/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('isMutating: 1')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('isMutating: 0')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/useMutationState/BaseExample.svelte ================================================
    Data: {JSON.stringify(mutationState.map((state) => state.status))}
    ================================================ FILE: packages/svelte-query/tests/useMutationState/SelectExample.svelte ================================================
    Variables: {JSON.stringify(variables)}
    ================================================ FILE: packages/svelte-query/tests/useMutationState/useMutationState.svelte.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { fireEvent, render } from '@testing-library/svelte' import { sleep } from '@tanstack/query-test-utils' import BaseExample from './BaseExample.svelte' import SelectExample from './SelectExample.svelte' import type { Mutation } from '@tanstack/query-core' describe('useMutationState', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('Run few mutation functions and check from useMutationState', async () => { const successMutationFn = vi.fn(() => sleep(10).then(() => 'data')) const errorMutationFn = vi .fn() .mockImplementation(() => sleep(20).then(() => Promise.reject(new Error('error'))), ) const rendered = render(BaseExample, { props: { successMutationOpts: () => ({ mutationKey: ['success'], mutationFn: successMutationFn, }), errorMutationOpts: () => ({ mutationKey: ['error'], mutationFn: errorMutationFn, }), }, }) fireEvent.click(rendered.getByRole('button', { name: /Success/i })) await vi.advanceTimersByTimeAsync(11) expect(successMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('Data: ["success"]')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Error/i })) await vi.advanceTimersByTimeAsync(21) expect(errorMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('Data: ["success","error"]')).toBeInTheDocument() }) test('Can select specific type of mutation ( i.e: error only )', async () => { const successMutationFn = vi.fn(() => sleep(10).then(() => 'data')) const errorMutationFn = vi .fn() .mockImplementation(() => sleep(20).then(() => Promise.reject(new Error('error'))), ) const rendered = render(BaseExample, { props: { successMutationOpts: () => ({ mutationKey: ['success'], mutationFn: successMutationFn, }), errorMutationOpts: () => ({ mutationKey: ['error'], mutationFn: errorMutationFn, }), mutationStateOpts: { filters: { status: 'error' }, }, }, }) fireEvent.click(rendered.getByRole('button', { name: /Success/i })) await vi.advanceTimersByTimeAsync(11) expect(successMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('Data: []')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Error/i })) await vi.advanceTimersByTimeAsync(21) expect(errorMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('Data: ["error"]')).toBeInTheDocument() }) test('should return selected value when using select option', async () => { const mutationKey = ['select'] const rendered = render(SelectExample, { props: { mutationOpts: () => ({ mutationKey, mutationFn: () => sleep(10).then(() => 'data'), }), mutationStateOpts: { filters: { mutationKey }, select: (mutation: Mutation) => mutation.state.status, }, }, }) expect(rendered.getByText('Variables: []')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate/i })) await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText('Variables: ["pending"]')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('Variables: ["success"]')).toBeInTheDocument() }) test('Can select specific mutation using mutation key', async () => { const successMutationFn = vi.fn(() => sleep(10).then(() => 'data')) const errorMutationFn = vi .fn() .mockImplementation(() => sleep(20).then(() => Promise.reject(new Error('error'))), ) const rendered = render(BaseExample, { props: { successMutationOpts: () => ({ mutationKey: ['success'], mutationFn: successMutationFn, }), errorMutationOpts: () => ({ mutationKey: ['error'], mutationFn: errorMutationFn, }), mutationStateOpts: { filters: { mutationKey: ['success'] }, }, }, }) fireEvent.click(rendered.getByRole('button', { name: /Success/i })) await vi.advanceTimersByTimeAsync(11) expect(successMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('Data: ["success"]')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /Error/i })) await vi.advanceTimersByTimeAsync(21) expect(errorMutationFn).toHaveBeenCalledTimes(1) expect(rendered.getByText('Data: ["success"]')).toBeInTheDocument() }) }) ================================================ FILE: packages/svelte-query/tests/utils.svelte.ts ================================================ export function ref(initial: T) { let value = $state(initial) return { get value() { return value }, set value(newValue) { value = newValue }, } } export function promiseWithResolvers() { let resolve: (value: T) => void let reject: (reason?: any) => void const promise = new Promise((res, rej) => { resolve = res reject = rej }) return { promise, resolve: resolve!, reject: reject! } } export function withEffectRoot(fn: () => void | Promise) { return async () => { let promise: void | Promise = Promise.resolve() const cleanup = $effect.root(() => { promise = fn() }) await promise cleanup() } } ================================================ FILE: packages/svelte-query/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "tests", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/svelte-query/vite.config.ts ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vitest/config' import { svelteTesting } from '@testing-library/svelte/vite' import packageJson from './package.json' export default defineConfig({ plugins: [svelte(), svelteTesting()], // 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: './tests', watch: false, environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, }, }) ================================================ FILE: packages/svelte-query-devtools/.attw.json ================================================ { "ignoreRules": ["cjs-resolves-to-esm", "internal-resolution-error"] } ================================================ FILE: packages/svelte-query-devtools/CHANGELOG.md ================================================ # @tanstack/svelte-query-devtools ## 6.0.4 ### Patch Changes - Updated dependencies [[`83366c4`](https://github.com/TanStack/query/commit/83366c46a6825b5c591399c705d8128743c527dd)]: - @tanstack/query-devtools@5.93.0 ## 6.0.3 ### Patch Changes - Updated dependencies [[`f9fc56a`](https://github.com/TanStack/query/commit/f9fc56a9b8724bcfae46f8f6cb229123478eb4db), [`0b29b6f`](https://github.com/TanStack/query/commit/0b29b6f877d4b3a6d05b1c85fb9cb1e6ea736291)]: - @tanstack/query-devtools@5.92.0 - @tanstack/svelte-query@6.0.12 ## 6.0.2 ### Patch Changes - Updated dependencies [[`b261b6f`](https://github.com/TanStack/query/commit/b261b6f29eee2a9bdbe1bc20035fe9b83b15376b)]: - @tanstack/query-devtools@5.91.1 ## 6.0.1 ### Patch Changes - Updated dependencies [[`0e9d5b5`](https://github.com/TanStack/query/commit/0e9d5b565276f0de2a1a14ffbb079b5988581c27)]: - @tanstack/query-devtools@5.91.0 ## 6.0.0 ### Major Changes - BREAKING: Migrate to svelte runes (signals). Requires [Svelte v5.25.0](https://github.com/sveltejs/svelte/releases/tag/svelte%405.25.0) or newer. Please see the [migration guide](https://tanstack.com/query/latest/docs/framework/svelte/migrate-from-v5-to-v6). ([#9694](https://github.com/TanStack/query/pull/9694)) ### Patch Changes - Updated dependencies [[`1056ba6`](https://github.com/TanStack/query/commit/1056ba63b30b9d9a66fa813c7d7fb1395e377c55)]: - @tanstack/svelte-query@6.0.0 ================================================ FILE: packages/svelte-query-devtools/eslint.config.js ================================================ // @ts-check import tsParser from '@typescript-eslint/parser' import pluginSvelte from 'eslint-plugin-svelte' import rootConfig from './root.eslint.config.js' import svelteConfig from './svelte.config.js' export default [ ...rootConfig, ...pluginSvelte.configs['recommended'], { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], languageOptions: { parserOptions: { parser: tsParser, extraFileExtensions: ['.svelte'], svelteConfig, }, }, }, { rules: { 'svelte/block-lang': ['error', { script: ['ts'] }], 'svelte/no-svelte-internal': 'error', 'svelte/valid-compile': 'off', }, }, ] ================================================ FILE: packages/svelte-query-devtools/package.json ================================================ { "name": "@tanstack/svelte-query-devtools", "version": "6.0.4", "description": "Developer tools to interact with and visualize the TanStack/svelte-query cache", "author": "Lachlan Collins", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/svelte-query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "tanstack", "query", "svelte" ], "scripts": { "clean": "premove ./dist ./coverage ./.svelte-kit ./dist-ts", "compile": "tsc --build", "test:types": "svelte-check --tsconfig ./tsconfig.json", "test:eslint": "eslint --concurrency=auto ./src", "test:build": "publint --strict && attw --pack", "build": "svelte-package --input ./src --output ./dist" }, "type": "module", "types": "dist/index.d.ts", "module": "dist/index.js", "svelte": "./dist/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "import": "./dist/index.js" }, "./package.json": "./package.json" }, "files": [ "dist", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-devtools": "workspace:*", "esm-env": "^1.2.1" }, "devDependencies": { "@sveltejs/package": "^2.4.0", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tanstack/svelte-query": "workspace:*", "@typescript-eslint/parser": "^8.48.0", "eslint-plugin-svelte": "^3.11.0", "svelte": "^5.39.3", "svelte-check": "^4.4.5", "typescript": "5.9.3" }, "peerDependencies": { "@tanstack/svelte-query": "workspace:^", "svelte": "^5.25.0" } } ================================================ FILE: packages/svelte-query-devtools/src/Devtools.svelte ================================================
    ================================================ FILE: packages/svelte-query-devtools/src/index.ts ================================================ export { default as SvelteQueryDevtools } from './Devtools.svelte' ================================================ FILE: packages/svelte-query-devtools/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { preprocess: vitePreprocess(), compilerOptions: { runes: true, }, } export default config ================================================ FILE: packages/svelte-query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../query-devtools" }, { "path": "../svelte-query" }] } ================================================ FILE: packages/svelte-query-devtools/vite.config.ts ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vite' export default defineConfig({ plugins: [svelte()], // fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660 resolve: { conditions: ['@tanstack/custom-condition'], }, environments: { ssr: { resolve: { conditions: ['@tanstack/custom-condition'], }, }, }, }) ================================================ FILE: packages/svelte-query-persist-client/.attw.json ================================================ { "ignoreRules": ["cjs-resolves-to-esm", "internal-resolution-error"] } ================================================ FILE: packages/svelte-query-persist-client/CHANGELOG.md ================================================ # @tanstack/svelte-query-persist-client ## 6.0.25 ### 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-persist-client-core@5.92.4 - @tanstack/svelte-query@6.1.3 ## 6.0.24 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.92.3 - @tanstack/svelte-query@6.1.2 ## 6.0.23 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.92.2 - @tanstack/svelte-query@6.1.1 ## 6.0.22 ### Patch Changes - Updated dependencies [[`e505568`](https://github.com/TanStack/query/commit/e505568f4d51c8281d38e9687091094b7d32a405)]: - @tanstack/query-persist-client-core@5.92.1 ## 6.0.21 ### Patch Changes - Updated dependencies [[`978fc52`](https://github.com/TanStack/query/commit/978fc52728a8b9eb33f0a82f4ddf42a95815bd7f)]: - @tanstack/query-persist-client-core@5.92.0 ## 6.0.20 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.19 - @tanstack/svelte-query@6.0.18 ## 6.0.19 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.18 - @tanstack/svelte-query@6.0.17 ## 6.0.18 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.17 - @tanstack/svelte-query@6.0.16 ## 6.0.17 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.16 - @tanstack/svelte-query@6.0.15 ## 6.0.16 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.15 - @tanstack/svelte-query@6.0.14 ## 6.0.15 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.14 - @tanstack/svelte-query@6.0.13 ## 6.0.14 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.13 - @tanstack/svelte-query@6.0.12 ## 6.0.13 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.12 - @tanstack/svelte-query@6.0.11 ## 6.0.12 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.11 - @tanstack/svelte-query@6.0.10 ## 6.0.11 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.10 - @tanstack/svelte-query@6.0.9 ## 6.0.10 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.9 - @tanstack/svelte-query@6.0.8 ## 6.0.9 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.8 - @tanstack/svelte-query@6.0.7 ## 6.0.8 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.7 - @tanstack/svelte-query@6.0.6 ## 6.0.7 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.6 - @tanstack/svelte-query@6.0.5 ## 6.0.6 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.5 - @tanstack/svelte-query@6.0.4 ## 6.0.5 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.4 - @tanstack/svelte-query@6.0.3 ## 6.0.4 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.3 - @tanstack/svelte-query@6.0.2 ## 6.0.3 ### Patch Changes - Updated dependencies []: - @tanstack/query-persist-client-core@5.91.2 - @tanstack/svelte-query@6.0.1 ## 6.0.2 ### Patch Changes - Updated dependencies [[`846d53d`](https://github.com/TanStack/query/commit/846d53d98992d50606c40634efa43dea9965b787)]: - @tanstack/query-persist-client-core@5.91.1 ## 6.0.1 ### Patch Changes - Updated dependencies [[`5cd86c6`](https://github.com/TanStack/query/commit/5cd86c6ef1720b87b13e1ab70ee823616f1f029a)]: - @tanstack/query-persist-client-core@5.91.0 ## 6.0.0 ### Major Changes - BREAKING: Migrate to svelte runes (signals). Requires [Svelte v5.25.0](https://github.com/sveltejs/svelte/releases/tag/svelte%405.25.0) or newer. Please see the [migration guide](https://tanstack.com/query/latest/docs/framework/svelte/migrate-from-v5-to-v6). ([#9694](https://github.com/TanStack/query/pull/9694)) ### Patch Changes - Updated dependencies [[`1056ba6`](https://github.com/TanStack/query/commit/1056ba63b30b9d9a66fa813c7d7fb1395e377c55)]: - @tanstack/svelte-query@6.0.0 ================================================ FILE: packages/svelte-query-persist-client/eslint.config.js ================================================ // @ts-check import tsParser from '@typescript-eslint/parser' import pluginSvelte from 'eslint-plugin-svelte' import rootConfig from './root.eslint.config.js' import svelteConfig from './svelte.config.js' export default [ ...rootConfig, ...pluginSvelte.configs['recommended'], { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], languageOptions: { parserOptions: { parser: tsParser, extraFileExtensions: ['.svelte'], svelteConfig, }, }, }, { rules: { 'svelte/block-lang': ['error', { script: ['ts'] }], 'svelte/no-svelte-internal': 'error', 'svelte/valid-compile': 'off', }, }, ] ================================================ FILE: packages/svelte-query-persist-client/package.json ================================================ { "name": "@tanstack/svelte-query-persist-client", "version": "6.0.25", "description": "Svelte bindings to work with persisters in TanStack/svelte-query", "author": "Lachlan Collins", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/svelte-query-persist-client" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "tanstack", "query", "svelte" ], "scripts": { "clean": "premove ./dist ./coverage ./.svelte-kit ./dist-ts", "compile": "tsc --build", "test:types": "svelte-check --tsconfig ./tsconfig.json", "test:eslint": "eslint --concurrency=auto ./src", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "svelte-package --input ./src --output ./dist" }, "type": "module", "types": "dist/index.d.ts", "module": "dist/index.js", "svelte": "./dist/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "import": "./dist/index.js" }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { "@sveltejs/package": "^2.4.0", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tanstack/query-test-utils": "workspace:*", "@tanstack/svelte-query": "workspace:*", "@testing-library/svelte": "^5.2.8", "@typescript-eslint/parser": "^8.48.0", "eslint-plugin-svelte": "^3.11.0", "svelte": "^5.39.3", "svelte-check": "^4.4.5", "typescript": "5.9.3" }, "peerDependencies": { "@tanstack/svelte-query": "workspace:^", "svelte": "^5.25.0" } } ================================================ FILE: packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte ================================================ {@render children()} ================================================ FILE: packages/svelte-query-persist-client/src/index.ts ================================================ // Re-export core export * from '@tanstack/query-persist-client-core' export { default as PersistQueryClientProvider } from './PersistQueryClientProvider.svelte' ================================================ FILE: packages/svelte-query-persist-client/src/utils.svelte.ts ================================================ type Box = { current: T } export function box(initial: T): Box { let current = $state(initial) return { get current() { return current }, set current(newValue) { current = newValue }, } } ================================================ FILE: packages/svelte-query-persist-client/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { preprocess: vitePreprocess(), compilerOptions: { runes: true, }, } export default config ================================================ FILE: packages/svelte-query-persist-client/tests/AwaitOnSuccess/AwaitOnSuccess.svelte ================================================
    {query.data}
    fetchStatus: {query.fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/AwaitOnSuccess/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/FreshData/FreshData.svelte ================================================
    data: {query.data ?? 'null'}
    fetchStatus: {query.fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/FreshData/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/InitialData/InitialData.svelte ================================================
    {query.data}
    fetchStatus: {query.fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/InitialData/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/OnSuccess/OnSuccess.svelte ================================================
    {query.data}
    fetchStatus: {query.fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/OnSuccess/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/PersistQueryClientProvider.svelte.test.ts ================================================ import { render } from '@testing-library/svelte' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { QueryClient } from '@tanstack/svelte-query' import { persistQueryClientSave } from '@tanstack/query-persist-client-core' import { sleep } from '@tanstack/query-test-utils' import AwaitOnSuccess from './AwaitOnSuccess/Provider.svelte' import FreshData from './FreshData/Provider.svelte' import OnSuccess from './OnSuccess/Provider.svelte' import InitialData from './InitialData/Provider.svelte' import RemoveCache from './RemoveCache/Provider.svelte' import RestoreCache from './RestoreCache/Provider.svelte' import UseQueries from './UseQueries/Provider.svelte' import { StatelessRef } from './utils.svelte.js' import type { PersistedClient, Persister, } from '@tanstack/query-persist-client-core' import type { StatusResult } from './utils.svelte.js' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const createMockPersister = (): Persister => { let storedState: PersistedClient | undefined return { persistClient(persistClient: PersistedClient) { storedState = persistClient }, async restoreClient() { return sleep(10).then(() => storedState) }, removeClient() { storedState = undefined }, } } const createMockErrorPersister = ( removeClient: Persister['removeClient'], ): [Error, Persister] => { const error = new Error('restore failed') return [ error, { async persistClient() { // noop }, async restoreClient() { return sleep(10).then(() => Promise.reject(error)) }, removeClient, }, ] } describe('PersistQueryClientProvider', () => { test('restores cache from persister', async () => { const states = new StatelessRef>>([]) const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'hydrated'), }) await vi.advanceTimersByTimeAsync(10) const persister = createMockPersister() persistQueryClientSave({ queryClient, persister }) await vi.advanceTimersByTimeAsync(0) queryClient.clear() const rendered = render(RestoreCache, { props: { queryClient, persistOptions: { persister }, states, }, }) expect(rendered.getByText('fetchStatus: idle')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('hydrated')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('fetched')).toBeInTheDocument() expect(states.current).toHaveLength(3) expect(states.current[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states.current[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states.current[2]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should also put useQueries into idle state', async () => { const states = new StatelessRef>>([]) const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'hydrated'), }) await vi.advanceTimersByTimeAsync(10) const persister = createMockPersister() persistQueryClientSave({ queryClient, persister }) await vi.advanceTimersByTimeAsync(0) queryClient.clear() const rendered = render(UseQueries, { props: { queryClient, persistOptions: { persister }, states, }, }) expect(rendered.getByText('fetchStatus: idle')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('hydrated')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('fetched')).toBeInTheDocument() expect(states.current).toHaveLength(3) expect(states.current[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states.current[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states.current[2]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should show initialData while restoring', async () => { const states = new StatelessRef>>([]) const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'hydrated'), }) await vi.advanceTimersByTimeAsync(10) const persister = createMockPersister() persistQueryClientSave({ queryClient, persister }) await vi.advanceTimersByTimeAsync(0) queryClient.clear() const rendered = render(InitialData, { props: { queryClient, persistOptions: { persister }, states, }, }) expect(rendered.getByText('initial')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('hydrated')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('fetched')).toBeInTheDocument() expect(states.current).toHaveLength(3) expect(states.current[0]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'initial', }) expect(states.current[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states.current[2]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should not refetch after restoring when data is fresh', async () => { const states = new StatelessRef>>([]) const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'hydrated'), }) await vi.advanceTimersByTimeAsync(10) const persister = createMockPersister() persistQueryClientSave({ queryClient, persister }) await vi.advanceTimersByTimeAsync(0) queryClient.clear() let fetched = false const rendered = render(FreshData, { props: { queryClient, persistOptions: { persister }, states, onFetch: () => { fetched = true }, }, }) expect(rendered.getByText('data: null')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data: hydrated')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data: hydrated')).toBeInTheDocument() expect(states.current).toHaveLength(2) expect(fetched).toBe(false) expect(states.current[0]).toMatchObject({ status: 'pending', fetchStatus: 'idle', data: undefined, }) expect(states.current[1]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'hydrated', }) }) test('should call onSuccess after successful restoring', async () => { const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'hydrated'), }) await vi.advanceTimersByTimeAsync(10) const persister = createMockPersister() persistQueryClientSave({ queryClient, persister }) await vi.advanceTimersByTimeAsync(0) queryClient.clear() const onSuccess = vi.fn() const rendered = render(OnSuccess, { props: { queryClient, persistOptions: { persister }, onSuccess, }, }) expect(onSuccess).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('hydrated')).toBeInTheDocument() expect(onSuccess).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('fetched')).toBeInTheDocument() }) test('should await onSuccess after successful restoring', async () => { const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: ['test'], queryFn: () => sleep(10).then(() => 'hydrated'), }) await vi.advanceTimersByTimeAsync(10) const persister = createMockPersister() persistQueryClientSave({ queryClient, persister }) await vi.advanceTimersByTimeAsync(0) queryClient.clear() const states = new StatelessRef>([]) const rendered = render(AwaitOnSuccess, { props: { queryClient, persistOptions: { persister }, states, onSuccess: async () => { states.current.push('onSuccess') await sleep(5) states.current.push('onSuccess done') }, }, }) await vi.advanceTimersByTimeAsync(15) expect(rendered.getByText('hydrated')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('fetched')).toBeInTheDocument() expect(states.current).toEqual([ 'onSuccess', 'onSuccess done', 'fetching', 'fetched', ]) }) test('should remove cache after non-successful restoring', async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const consoleWarn = vi .spyOn(console, 'warn') .mockImplementation(() => undefined) const queryClient = new QueryClient() const removeClient = vi.fn() const onSuccess = vi.fn() const onError = vi.fn() const [error, persister] = createMockErrorPersister(removeClient) const rendered = render(RemoveCache, { props: { queryClient, persistOptions: { persister }, onError, onSuccess }, }) await vi.advanceTimersByTimeAsync(10) await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('fetched')).toBeInTheDocument() expect(removeClient).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledTimes(0) expect(onError).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenNthCalledWith(1, error) consoleMock.mockRestore() consoleWarn.mockRestore() }) }) ================================================ FILE: packages/svelte-query-persist-client/tests/RemoveCache/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/RemoveCache/RemoveCache.svelte ================================================
    {query.data}
    fetchStatus: {query.fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/RestoreCache/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/RestoreCache/RestoreCache.svelte ================================================
    {query.data}
    fetchStatus: {query.fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/UseQueries/Provider.svelte ================================================ ================================================ FILE: packages/svelte-query-persist-client/tests/UseQueries/UseQueries.svelte ================================================
    {queries[0].data}
    fetchStatus: {queries[0].fetchStatus}
    ================================================ FILE: packages/svelte-query-persist-client/tests/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' ================================================ FILE: packages/svelte-query-persist-client/tests/utils.svelte.ts ================================================ export type StatusResult = { status: string fetchStatus: string data: T | undefined } export class StatelessRef { current: T constructor(value: T) { this.current = value } } ================================================ FILE: packages/svelte-query-persist-client/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "tests", "*.config.*", "package.json"], "references": [ { "path": "../query-persist-client-core" }, { "path": "../svelte-query" } ] } ================================================ FILE: packages/svelte-query-persist-client/vite.config.ts ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vitest/config' import { svelteTesting } from '@testing-library/svelte/vite' import packageJson from './package.json' export default defineConfig({ plugins: [svelte(), svelteTesting()], // 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: './tests', watch: false, environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], typecheck: { enabled: true }, restoreMocks: true, }, }) ================================================ FILE: packages/vue-query/CHANGELOG.md ================================================ # @tanstack/vue-query ## 5.92.12 ### 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.92.11 ### Patch Changes - Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]: - @tanstack/query-core@5.91.1 ## 5.92.10 ### Patch Changes - fix(vue-query/useMutation): add missing '\_defaulted' omit in 'UseMutationOptionsBase' ([#10215](https://github.com/TanStack/query/pull/10215)) - Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]: - @tanstack/query-core@5.91.0 ## 5.92.9 ### Patch Changes - Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]: - @tanstack/query-core@5.90.20 ## 5.92.8 ### Patch Changes - Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]: - @tanstack/query-core@5.90.19 ## 5.92.7 ### Patch Changes - Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]: - @tanstack/query-core@5.90.18 ## 5.92.6 ### Patch Changes - Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]: - @tanstack/query-core@5.90.17 ## 5.92.5 ### Patch Changes - Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]: - @tanstack/query-core@5.90.16 ## 5.92.4 ### Patch Changes - Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]: - @tanstack/query-core@5.90.15 ## 5.92.3 ### Patch Changes - Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]: - @tanstack/query-core@5.90.14 ## 5.92.2 ### Patch Changes - Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]: - @tanstack/query-core@5.90.13 ## 5.92.1 ### Patch Changes - Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]: - @tanstack/query-core@5.90.12 ## 5.92.0 ### Minor Changes - feat(vue-query): allow options getters in additional composables ([#9914](https://github.com/TanStack/query/pull/9914)) ## 5.91.4 ### Patch Changes - Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]: - @tanstack/query-core@5.90.11 ## 5.91.3 ### Patch Changes - Include TPageParam in enabled of InfiniteQueryObserverOptions ([#9898](https://github.com/TanStack/query/pull/9898)) ## 5.91.2 ### 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.91.1 ### Patch Changes - Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]: - @tanstack/query-core@5.90.9 ## 5.91.0 ### Minor Changes - feat(vue-query): support useQuery options getter ([#9866](https://github.com/TanStack/query/pull/9866)) ## 5.90.8 ### Patch Changes - Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]: - @tanstack/query-core@5.90.8 ## 5.90.7 ### Patch Changes - Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]: - @tanstack/query-core@5.90.7 ## 5.90.6 ### Patch Changes - Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]: - @tanstack/query-core@5.90.6 ## 5.90.5 ### Patch Changes - Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]: - @tanstack/query-core@5.90.5 ## 5.90.4 ### Patch Changes - Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]: - @tanstack/query-core@5.90.4 ## 5.90.3 ### Patch Changes - Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]: - @tanstack/query-core@5.90.3 ================================================ FILE: packages/vue-query/README.md ================================================ [![Vue Query logo](https://raw.githubusercontent.com/TanStack/query/main/packages/vue-query/media/vue-query.png)](https://github.com/TanStack/query/tree/main/packages/vue-query) [![npm version](https://img.shields.io/npm/v/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query) [![npm license](https://img.shields.io/npm/l/@tanstack/vue-query)](https://github.com/TanStack/query/blob/main/LICENSE) [![bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) [![npm](https://img.shields.io/npm/dm/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query) # Vue Query Hooks for fetching, caching and updating asynchronous data in Vue. Support for Vue 2.x via [vue-demi](https://github.com/vueuse/vue-demi) # Documentation Visit https://tanstack.com/query/latest/docs/vue/overview # 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 - (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching - (experimental) SSR support - Dedicated Devtools - [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) (depending on features imported) # Quick Start 1. Install `vue-query` ```bash $ npm i @tanstack/vue-query ``` or ```bash $ pnpm add @tanstack/vue-query ``` or ```bash $ yarn add @tanstack/vue-query ``` or ```bash $ bun add @tanstack/vue-query ``` > If you are using Vue 2.6, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api) 2. Initialize **Vue Query** via **VueQueryPlugin** ```tsx import { createApp } from 'vue' import { VueQueryPlugin } from '@tanstack/vue-query' import App from './App.vue' createApp(App).use(VueQueryPlugin).mount('#app') ``` 3. Use query ```tsx import { defineComponent } from 'vue' import { useQuery } from '@tanstack/vue-query' export default defineComponent({ name: 'MyComponent', setup() { const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }) return { query, } }, }) ``` 4. If you need to update options on your query dynamically, make sure to pass them as reactive variables ```tsx const id = ref(1) const enabled = ref(false) const query = useQuery({ queryKey: ['todos', id], queryFn: () => getTodos(id), enabled, }) ``` ================================================ FILE: packages/vue-query/eslint.config.js ================================================ // @ts-check import pluginVue from 'eslint-plugin-vue' import rootConfig from './root.eslint.config.js' export default [...rootConfig, ...pluginVue.configs['flat/base']] ================================================ FILE: packages/vue-query/package.json ================================================ { "name": "@tanstack/vue-query", "version": "5.92.12", "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue", "author": "Damian Osipiuk", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/vue-query" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "vue-demi-switch 3 && pnpm run \"/^test:types:ts[0-9]{2}$/\"", "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json", "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json", "test:types:tscurrent": "tsc --build", "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build tsconfig.legacy.json", "test:lib": "pnpm run test:lib:2 && pnpm run test:lib:2.7 && pnpm run test:lib:3", "test:lib:2": "vue-demi-switch 2 vue2 && vitest", "test:lib:2.7": "vue-demi-switch 2.7 vue2.7 && vitest", "test:lib:3": "vue-demi-switch 3 && vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict && attw --pack", "build": "tsup --tsconfig tsconfig.prod.json" }, "type": "module", "types": "build/legacy/index.d.ts", "main": "build/legacy/index.cjs", "module": "build/legacy/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "import": { "types": "./build/modern/index.d.ts", "default": "./build/modern/index.js" }, "require": { "types": "./build/modern/index.d.cts", "default": "./build/modern/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "build", "src", "!src/__tests__" ], "dependencies": { "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/query-core": "workspace:*", "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "devDependencies": { "@tanstack/query-test-utils": "workspace:*", "@vitejs/plugin-vue": "^5.2.4", "@vue/composition-api": "1.7.2", "eslint-plugin-vue": "^10.5.0", "vue": "^3.4.27", "vue2": "npm:vue@2.6", "vue2.7": "npm:vue@2.7" }, "peerDependencies": { "@vue/composition-api": "^1.1.2", "vue": "^2.6.0 || ^3.3.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true } } } ================================================ FILE: packages/vue-query/src/__mocks__/useBaseQuery.ts ================================================ import { vi } from 'vitest' import type * as UseBaseQueryModule from '../useBaseQuery' const { useBaseQuery: originImpl } = await vi.importActual('../useBaseQuery') export const useBaseQuery = vi.fn(originImpl) ================================================ FILE: packages/vue-query/src/__mocks__/useQueryClient.ts ================================================ import { vi } from 'vitest' import { QueryClient } from '../queryClient' const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity }, }, }) export const useQueryClient = vi.fn(() => queryClient) ================================================ FILE: packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient, dataTagSymbol } from '@tanstack/query-core' import { reactive } from 'vue-demi' import { infiniteQueryOptions } from '../infiniteQueryOptions' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' describe('infiniteQueryOptions', () => { it('should not allow excess properties', () => { assertType( infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), getNextPageParam: () => 1, initialPageParam: 1, // @ts-expect-error this is a good error, because stallTime does not exist! stallTime: 1000, }), ) }) it('should infer types for callbacks', () => { infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('data'), staleTime: 1000, getNextPageParam: () => 1, initialPageParam: 1, select: (data) => { expectTypeOf(data).toEqualTypeOf>() }, }) }) it('should work when passed to useInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const { data } = reactive(useInfiniteQuery(options)) expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => 'string', getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>() }) it('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), select: (data) => data.pages, getNextPageParam: () => 1, initialPageParam: 1, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf>() }) it('should return the proper type when passed to getQueryData', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) it('should properly type when passed to setQueryData', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf< InfiniteData | undefined >() return prev }) expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >() }) }) ================================================ FILE: packages/vue-query/src/__tests__/mutationCache.test.ts ================================================ import { beforeAll, describe, expect, test, vi } from 'vitest' import { ref } from 'vue-demi' import { MutationCache as MutationCacheOrigin } from '@tanstack/query-core' import { MutationCache } from '../mutationCache' describe('MutationCache', () => { beforeAll(() => { vi.spyOn(MutationCacheOrigin.prototype, 'find') vi.spyOn(MutationCacheOrigin.prototype, 'findAll') }) describe('find', () => { test('should properly unwrap parameters', () => { const mutationCache = new MutationCache() mutationCache.find({ mutationKey: ref(['baz']), }) expect(MutationCacheOrigin.prototype.find).toBeCalledWith({ mutationKey: ['baz'], }) }) }) describe('findAll', () => { test('should properly unwrap parameters', () => { const mutationCache = new MutationCache() mutationCache.findAll({ mutationKey: ref(['baz']), }) expect(MutationCacheOrigin.prototype.findAll).toBeCalledWith({ mutationKey: ['baz'], }) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryCache.test.ts ================================================ import { beforeAll, describe, expect, test, vi } from 'vitest' import { ref } from 'vue-demi' import { QueryCache as QueryCacheOrigin } from '@tanstack/query-core' import { QueryCache } from '../queryCache' describe('QueryCache', () => { beforeAll(() => { vi.spyOn(QueryCacheOrigin.prototype, 'find') vi.spyOn(QueryCacheOrigin.prototype, 'findAll') }) describe('find', () => { test('should properly unwrap parameters', () => { const queryCache = new QueryCache() queryCache.find({ queryKey: ['foo', ref('bar')], }) expect(QueryCacheOrigin.prototype.find).toBeCalledWith({ queryKey: ['foo', 'bar'], }) }) }) describe('findAll', () => { test('should properly unwrap two parameters', () => { const queryCache = new QueryCache() queryCache.findAll({ queryKey: ['foo', ref('bar')], }) expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith({ queryKey: ['foo', 'bar'], }) }) test('should default to empty filters', () => { const queryCache = new QueryCache() queryCache.findAll() expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith({}) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryClient.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '../queryClient' import type { DataTag, InfiniteData } from '@tanstack/query-core' describe('getQueryData', () => { it('should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic if passed', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should only allow Arrays to be passed', () => { assertType>([ // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'QueryKey' { queryKey: 'key' }, ]) }) }) describe('setQueryData', () => { it('updater should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('value should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '1') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '1') const data = queryClient.setQueryData(queryKey, 1) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown for updater if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should infer unknown for value if key is not tagged', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, 'foo') expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic if passed', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should infer passed generic for value', () => { const queryKey = ['key'] as const const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, 'foo') expectTypeOf(data).toEqualTypeOf() }) it('should preserve updater parameter type inference when used in functions with explicit return types', () => { const queryKey = ['key'] as DataTag, number> const queryClient = new QueryClient() // Simulate usage inside a function with explicit return type // The outer function returns 'unknown' but this shouldn't affect the updater's type inference ;(() => queryClient.setQueryData(queryKey, (data) => { expectTypeOf(data).toEqualTypeOf() return data })) satisfies () => unknown }) }) describe('fetchInfiniteQuery', () => { it('should allow passing pages', async () => { const data = await new QueryClient().fetchInfiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, pages: 5, }) expectTypeOf(data).toEqualTypeOf>() }) it('should not allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], queryFn: () => Promise.resolve('string'), initialPageParam: 1, getNextPageParam: () => 1, }, ]) }) it('should not allow passing pages without getNextPageParam', () => { assertType>([ // @ts-expect-error Property 'getNextPageParam' is missing { queryKey: ['key'], queryFn: () => Promise.resolve('string'), initialPageParam: 1, pages: 5, }, ]) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryClient.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { ref } from 'vue-demi' import { QueryClient as QueryClientOrigin } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { infiniteQueryOptions } from '../infiniteQueryOptions' vi.mock('@tanstack/query-core', async () => { const actual = await vi.importActual<{ QueryClient: typeof QueryClientOrigin }>('@tanstack/query-core') // Get the prototype methods dynamically const prototypeMethods = Object.getOwnPropertyNames( actual.QueryClient.prototype, ).filter((prop): prop is keyof typeof actual.QueryClient.prototype => { const descriptor = Object.getOwnPropertyDescriptor( actual.QueryClient.prototype, prop, ) return typeof descriptor?.value === 'function' && prop !== 'constructor' }) // Spy on all methods in the prototype prototypeMethods.forEach((method) => { vi.spyOn(actual.QueryClient.prototype, method) }) return actual }) const queryKeyRef = ['foo', ref('bar')] const queryKeyUnref = ['foo', 'bar'] const fn = () => 'mock' describe('QueryCache', () => { beforeEach(() => { vi.useFakeTimers() vi.clearAllMocks() }) afterEach(() => { vi.useRealTimers() }) describe('isFetching', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.isFetching({ queryKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.isFetching).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('isMutating', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.isMutating({ mutationKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.isMutating).toBeCalledWith({ mutationKey: queryKeyUnref, }) }) }) describe('getQueryData', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.getQueryData(queryKeyRef) expect(QueryClientOrigin.prototype.getQueryData).toBeCalledWith( queryKeyUnref, ) }) }) describe('ensureQueryData', () => { test('should properly unwrap parameter', () => { const queryClient = new QueryClient() queryClient.ensureQueryData({ queryKey: queryKeyRef, queryFn: fn, }) expect(QueryClientOrigin.prototype.ensureQueryData).toBeCalledWith({ queryKey: queryKeyUnref, queryFn: fn, }) }) }) describe('getQueriesData', () => { test('should properly unwrap queryKey param', () => { const queryClient = new QueryClient() queryClient.getQueriesData({ queryKey: queryKeyRef }) expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith({ queryKey: queryKeyUnref, }) }) test('should properly unwrap filters param', () => { const queryClient = new QueryClient() queryClient.getQueriesData({ queryKey: queryKeyRef }) expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('setQueryData', () => { test('should properly unwrap 3 parameter', () => { const queryClient = new QueryClient() queryClient.setQueryData(queryKeyRef, fn, { updatedAt: ref(3), }) expect(QueryClientOrigin.prototype.setQueryData).toBeCalledWith( queryKeyUnref, fn, { updatedAt: 3 }, ) }) }) describe('setQueriesData', () => { test('should properly unwrap params with queryKey', () => { const queryClient = new QueryClient() queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, { updatedAt: ref(3), }) expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith( { queryKey: queryKeyUnref }, fn, { updatedAt: 3 }, ) }) test('should properly unwrap params with filters', () => { const queryClient = new QueryClient() queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, { updatedAt: ref(3), }) expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith( { queryKey: queryKeyUnref }, fn, { updatedAt: 3 }, ) }) }) describe('getQueryState', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.getQueryState(queryKeyRef) expect(QueryClientOrigin.prototype.getQueryState).toBeCalledWith( queryKeyUnref, ) }) }) describe('removeQueries', () => { test('should properly unwrap 1 parameter', () => { const queryClient = new QueryClient() queryClient.removeQueries({ queryKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.removeQueries).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('resetQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.resetQueries( { queryKey: queryKeyRef, }, { cancelRefetch: ref(false) }, ) expect(QueryClientOrigin.prototype.resetQueries).toBeCalledWith( { queryKey: queryKeyUnref, }, { cancelRefetch: false }, ) }) }) describe('cancelQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.cancelQueries( { queryKey: queryKeyRef, }, { revert: ref(false) }, ) expect(QueryClientOrigin.prototype.cancelQueries).toBeCalledWith( { queryKey: queryKeyUnref, }, { revert: false }, ) }) }) describe('invalidateQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.invalidateQueries( { queryKey: queryKeyRef, }, { cancelRefetch: ref(false) }, ) expect(QueryClientOrigin.prototype.invalidateQueries).toBeCalledWith( { queryKey: queryKeyUnref, refetchType: 'none', }, { cancelRefetch: false }, ) }) // #7694 test('should call invalidateQueries immediately and refetchQueries after sleep', async () => { const invalidateQueries = vi.spyOn( QueryClientOrigin.prototype, 'invalidateQueries', ) const refetchQueries = vi.spyOn( QueryClientOrigin.prototype, 'refetchQueries', ) const queryClient = new QueryClient() queryClient.invalidateQueries({ queryKey: queryKeyRef, }) expect(invalidateQueries).toBeCalled() expect(refetchQueries).not.toBeCalled() await vi.advanceTimersByTimeAsync(0) expect(refetchQueries).toBeCalled() }) test('should call invalidateQueries immediately and not call refetchQueries', async () => { const invalidateQueries = vi.spyOn( QueryClientOrigin.prototype, 'invalidateQueries', ) const refetchQueries = vi.spyOn( QueryClientOrigin.prototype, 'refetchQueries', ) const queryClient = new QueryClient() queryClient.invalidateQueries({ queryKey: queryKeyRef, refetchType: 'none', }) expect(invalidateQueries).toBeCalled() expect(refetchQueries).not.toBeCalled() await vi.advanceTimersByTimeAsync(0) expect(refetchQueries).not.toBeCalled() }) }) describe('refetchQueries', () => { test('should properly unwrap 2 parameter', () => { const queryClient = new QueryClient() queryClient.refetchQueries( { queryKey: queryKeyRef, }, { cancelRefetch: ref(false) }, ) expect(QueryClientOrigin.prototype.refetchQueries).toBeCalledWith( { queryKey: queryKeyUnref, }, { cancelRefetch: false }, ) }) }) describe('fetchQuery', () => { test('should properly unwrap parameter', () => { const queryClient = new QueryClient() queryClient.fetchQuery({ queryKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith({ queryKey: queryKeyUnref, }) }) }) describe('prefetchQuery', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.prefetchQuery({ queryKey: queryKeyRef, queryFn: fn }) expect(QueryClientOrigin.prototype.prefetchQuery).toBeCalledWith({ queryKey: queryKeyUnref, queryFn: fn, }) }) }) describe('fetchInfiniteQuery', () => { test('should properly unwrap parameter', () => { const queryClient = new QueryClient() queryClient.fetchInfiniteQuery({ queryKey: queryKeyRef, initialPageParam: 0, }) expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith( expect.objectContaining({ initialPageParam: 0, queryKey: queryKeyUnref, }), ) }) test('should properly unwrap parameter using infiniteQueryOptions with unref', () => { const queryClient = new QueryClient() const options = infiniteQueryOptions({ queryKey: queryKeyUnref, initialPageParam: 0, getNextPageParam: () => 12, }) queryClient.fetchInfiniteQuery(options) expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith( expect.objectContaining({ initialPageParam: 0, queryKey: queryKeyUnref, }), ) }) }) describe('prefetchInfiniteQuery', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.prefetchInfiniteQuery({ queryKey: queryKeyRef, queryFn: fn, initialPageParam: 0, }) expect(QueryClientOrigin.prototype.prefetchInfiniteQuery).toBeCalledWith({ initialPageParam: 0, queryKey: queryKeyUnref, queryFn: fn, }) }) }) describe('setDefaultOptions', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.setDefaultOptions({ queries: { enabled: ref(false), }, }) expect(QueryClientOrigin.prototype.setDefaultOptions).toBeCalledWith({ queries: { enabled: false, }, }) }) }) describe('setQueryDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.setQueryDefaults(queryKeyRef, { enabled: ref(false), }) expect(QueryClientOrigin.prototype.setQueryDefaults).toBeCalledWith( queryKeyUnref, { enabled: false, }, ) }) }) describe('getQueryDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.getQueryDefaults(queryKeyRef) expect(QueryClientOrigin.prototype.getQueryDefaults).toBeCalledWith( queryKeyUnref, ) }) }) describe('setMutationDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.setMutationDefaults(queryKeyRef, { mutationKey: queryKeyRef, }) expect(QueryClientOrigin.prototype.setMutationDefaults).toBeCalledWith( queryKeyUnref, { mutationKey: queryKeyUnref, }, ) }) }) describe('getMutationDefaults', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() queryClient.getMutationDefaults(queryKeyRef) expect(QueryClientOrigin.prototype.getMutationDefaults).toBeCalledWith( queryKeyUnref, ) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryOptions.test-d.ts ================================================ import { assertType, describe, expectTypeOf, it } from 'vitest' import { reactive, ref } from 'vue-demi' import { dataTagSymbol } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { queryOptions } from '../queryOptions' import { useQuery } from '../useQuery' describe('queryOptions', () => { it('should not allow excess properties', () => { assertType( queryOptions({ queryKey: ['key'], 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: ['key'], queryFn: () => Promise.resolve(5), staleTime: 1000, select: (data) => { expectTypeOf(data).toEqualTypeOf() }, }) }) it('should work when passed to useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const { data } = reactive(useQuery(options)) expectTypeOf(data).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => 5, }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with unknown if there is no queryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn if select is used', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should return the proper type when passed to getQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.getQueryData(queryKey) expectTypeOf(data).toEqualTypeOf() }) it('should properly type updaterFn when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() const data = queryClient.setQueryData(queryKey, (prev) => { expectTypeOf(prev).toEqualTypeOf() return prev }) expectTypeOf(data).toEqualTypeOf() }) it('should properly type value when passed to setQueryData', () => { const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, '5') // @ts-expect-error value should be a number queryClient.setQueryData(queryKey, () => '5') const data = queryClient.setQueryData(queryKey, 5) expectTypeOf(data).toEqualTypeOf() }) it('should allow to be passed to QueryClient methods while containing ref in queryKey', () => { const options = queryOptions({ queryKey: ['key', ref(1), { nested: ref(2) }], queryFn: () => Promise.resolve(5), }) const queryClient = new QueryClient() // Should not error const data = queryClient.invalidateQueries(options) // Should not error const data2 = queryClient.fetchQuery(options) expectTypeOf(data).toEqualTypeOf>() expectTypeOf(data2).toEqualTypeOf>() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { 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 } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { 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 } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const { data, isSuccess } = reactive( useQuery( queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ), ) if (isSuccess) { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() } }) it('data should not have undefined when initialData is provided', () => { const { data } = reactive( useQuery( queryOptions({ queryKey: ['query-key'], initialData: 42, }), ), ) expectTypeOf(data).toEqualTypeOf() }) }) ================================================ FILE: packages/vue-query/src/__tests__/queryOptions.test.ts ================================================ import { describe, expect, it } from 'vitest' import { queryOptions } from '../queryOptions' import type { UseQueryOptions } from '../useQuery' describe('queryOptions', () => { it('should return the object received as a parameter without any modification.', () => { const object: UseQueryOptions = { queryKey: ['key'], queryFn: () => Promise.resolve(5), } as const expect(queryOptions(object)).toBe(object) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useInfiniteQuery.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { computed, reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useInfiniteQuery } from '../useInfiniteQuery' import { infiniteQueryOptions } from '../infiniteQueryOptions' import type { InfiniteData } from '@tanstack/query-core' describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(query.data).toEqualTypeOf< InfiniteData | undefined >() }) it('data should be defined when query is success', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isSuccess) { // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(query.data).toEqualTypeOf>() } }) it('error should be null when query is success', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isSuccess) { expectTypeOf(query.error).toEqualTypeOf() } }) it('data should be undefined when query is pending', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isPending) { expectTypeOf(query.data).toEqualTypeOf() } }) it('error should be defined when query is error', () => { const query = reactive( useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) if (query.isError) { expectTypeOf(query.error).toEqualTypeOf() } }) it('should accept computed options', () => { const options = computed(() => ({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, })) const query = reactive(useInfiniteQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf>() } }) it('should accept computed options using infiniteQueryOptions', () => { const options = computed(() => infiniteQueryOptions({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }), ) const query = reactive(useInfiniteQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf>() } }) it('should accept plain options using infiniteQueryOptions', () => { const options = () => infiniteQueryOptions({ queryKey: ['infiniteQuery'], queryFn: () => sleep(0).then(() => 'Some data'), getNextPageParam: () => undefined, initialPageParam: 0, }) const query = reactive(useInfiniteQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf>() } }) }) ================================================ FILE: packages/vue-query/src/__tests__/useInfiniteQuery.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { useInfiniteQuery } from '../useInfiniteQuery' import { infiniteQueryOptions } from '../infiniteQueryOptions' vi.mock('../useQueryClient') describe('useInfiniteQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly execute infinite query', async () => { const { data, fetchNextPage, status } = useInfiniteQuery({ queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, }) expect(data.value).toStrictEqual(undefined) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0], pages: ['data on page 0'], }) expect(status.value).toStrictEqual('success') fetchNextPage() await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0, 12], pages: ['data on page 0', 'data on page 12'], }) expect(status.value).toStrictEqual('success') }) test('should properly execute infinite query using infiniteQueryOptions', async () => { const options = infiniteQueryOptions({ queryKey: ['infiniteQueryOptions'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, }) const { data, fetchNextPage, status } = useInfiniteQuery(options) expect(data.value).toStrictEqual(undefined) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0], pages: ['data on page 0'], }) expect(status.value).toStrictEqual('success') fetchNextPage() await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual({ pageParams: [0, 12], pages: ['data on page 0', 'data on page 12'], }) expect(status.value).toStrictEqual('success') }) }) ================================================ FILE: packages/vue-query/src/__tests__/useIsFetching.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { onScopeDispose, reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useIsFetching } from '../useIsFetching' import type { MockedFunction } from 'vitest' vi.mock('../useQueryClient') describe('useIsFetching', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly return isFetching state', async () => { const { isFetching: isFetchingQuery } = useQuery({ queryKey: ['isFetching1'], queryFn: () => sleep(0).then(() => 'Some data'), }) useQuery({ queryKey: ['isFetching2'], queryFn: () => sleep(0).then(() => 'Some data'), }) const isFetching = useIsFetching() expect(isFetchingQuery.value).toStrictEqual(true) expect(isFetching.value).toStrictEqual(2) await vi.advanceTimersByTimeAsync(0) expect(isFetchingQuery.value).toStrictEqual(false) expect(isFetching.value).toStrictEqual(0) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementation((fn) => fn()) const { status } = useQuery({ queryKey: ['onScopeDispose'], queryFn: () => sleep(0).then(() => 'Some data'), }) const isFetching = useIsFetching() expect(status.value).toStrictEqual('pending') expect(isFetching.value).toStrictEqual(1) await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') expect(isFetching.value).toStrictEqual(1) await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') expect(isFetching.value).toStrictEqual(1) onScopeDisposeMock.mockReset() }) test('should properly update filters', async () => { const filter = reactive({ stale: false, queryKey: ['isFetchingFilter'] }) useQuery({ queryKey: ['isFetchingFilter'], queryFn: () => sleep(10).then(() => 'Some data'), }) const isFetching = useIsFetching(filter) expect(isFetching.value).toStrictEqual(0) filter.stale = true await vi.advanceTimersByTimeAsync(0) expect(isFetching.value).toStrictEqual(1) }) test('should work with options getter and be reactive', async () => { const staleRef = ref(false) useQuery({ queryKey: ['isFetchingGetter'], queryFn: () => sleep(10).then(() => 'Some data'), }) const isFetching = useIsFetching(() => ({ stale: staleRef.value, queryKey: ['isFetchingGetter'], })) expect(isFetching.value).toStrictEqual(0) staleRef.value = true await vi.advanceTimersByTimeAsync(0) expect(isFetching.value).toStrictEqual(1) }) test('should warn when used outside of setup function in development mode', () => { vi.stubEnv('NODE_ENV', 'development') const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) try { useIsFetching() expect(warnSpy).toHaveBeenCalledWith( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } finally { warnSpy.mockRestore() vi.unstubAllEnvs() } }) }) ================================================ FILE: packages/vue-query/src/__tests__/useIsMutating.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { onScopeDispose, reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' import { useIsMutating } from '../useMutationState' import type { MockedFunction } from 'vitest' vi.mock('../useQueryClient') describe('useIsMutating', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly return isMutating state', async () => { const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) const mutation2 = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) const isMutating = useIsMutating() expect(isMutating.value).toStrictEqual(0) mutation.mutateAsync('a') mutation2.mutateAsync('b') await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(2) await vi.advanceTimersByTimeAsync(10) expect(isMutating.value).toStrictEqual(0) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementation((fn) => fn()) const mutation = useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) const mutation2 = useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) const isMutating = useIsMutating() expect(isMutating.value).toStrictEqual(0) mutation.mutateAsync('a') mutation2.mutateAsync('b') await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(0) await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(0) onScopeDisposeMock.mockReset() }) test('should properly update filters', async () => { const filter = reactive({ mutationKey: ['foo'] }) const { mutate } = useMutation({ mutationKey: ['isMutating'], mutationFn: (params: string) => sleep(10).then(() => params), }) mutate('foo') const isMutating = useIsMutating(filter) expect(isMutating.value).toStrictEqual(0) filter.mutationKey = ['isMutating'] await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(1) }) test('should work with options getter and be reactive', async () => { const keyRef = ref('isMutatingGetter2') const { mutate } = useMutation({ mutationKey: ['isMutatingGetter'], mutationFn: (params: string) => sleep(10).then(() => params), }) mutate('foo') const isMutating = useIsMutating(() => ({ mutationKey: [keyRef.value], })) expect(isMutating.value).toStrictEqual(0) keyRef.value = 'isMutatingGetter' await vi.advanceTimersByTimeAsync(0) expect(isMutating.value).toStrictEqual(1) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useMutation.test-d.tsx ================================================ import { describe, expectTypeOf, it } from 'vitest' import { reactive } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) expectTypeOf(mutation.data).toEqualTypeOf() }) it('data should be defined when mutation is success', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isSuccess) { expectTypeOf(mutation.data).toEqualTypeOf() } }) it('error should be null when mutation is success', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isSuccess) { expectTypeOf(mutation.error).toEqualTypeOf() } }) it('data should be undefined when mutation is pending', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isPending) { expectTypeOf(mutation.data).toEqualTypeOf() } }) it('error should be defined when mutation is error', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isError) { expectTypeOf(mutation.error).toEqualTypeOf() } }) it('should narrow variables', () => { const mutation = reactive( useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }), ) if (mutation.isIdle) { expectTypeOf(mutation.variables).toEqualTypeOf() return } if (mutation.isPending) { expectTypeOf(mutation.variables).toEqualTypeOf() return } if (mutation.isSuccess) { expectTypeOf(mutation.variables).toEqualTypeOf() return } expectTypeOf(mutation.variables).toEqualTypeOf() }) }) ================================================ FILE: packages/vue-query/src/__tests__/useMutation.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' import { useQueryClient } from '../useQueryClient' vi.mock('../useQueryClient') describe('useMutation', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should be in idle state initially', () => { const mutation = useMutation({ mutationFn: (params) => sleep(0).then(() => params), }) expect(mutation).toMatchObject({ isIdle: { value: true }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: false }, }) }) test('should change state after invoking mutate', () => { const result = 'Mock data' const mutation = useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) mutation.mutate(result) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: true }, isError: { value: false }, isSuccess: { value: false }, data: { value: undefined }, error: { value: null }, }) }) test('should return error when request fails', async () => { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) mutation.mutate() await vi.advanceTimersByTimeAsync(10) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: true }, isSuccess: { value: false }, data: { value: undefined }, error: { value: Error('Some error') }, }) }) test('should return data when request succeeds', async () => { const result = 'Mock data' const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) mutation.mutate(result) await vi.advanceTimersByTimeAsync(10) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: true }, data: { value: 'Mock data' }, error: { value: null }, }) }) test('should work with options getter and be reactive', async () => { const result = 'Mock data' const keyRef = ref('key01') const fnMock = vi.fn((params: string) => sleep(10).then(() => params)) const mutation = useMutation(() => ({ mutationKey: [keyRef.value], mutationFn: fnMock, })) mutation.mutate(result) await vi.advanceTimersByTimeAsync(10) expect(fnMock).toHaveBeenCalledTimes(1) expect(fnMock).toHaveBeenNthCalledWith( 1, result, expect.objectContaining({ mutationKey: ['key01'] }), ) keyRef.value = 'key02' await vi.advanceTimersByTimeAsync(0) mutation.mutate(result) await vi.advanceTimersByTimeAsync(10) expect(fnMock).toHaveBeenCalledTimes(2) expect(fnMock).toHaveBeenNthCalledWith( 2, result, expect.objectContaining({ mutationKey: ['key02'] }), ) }) test('should update reactive options', async () => { const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() const options = reactive({ mutationKey: ['foo'], mutationFn: (params: string) => sleep(10).then(() => params), }) const mutation = useMutation(options) options.mutationKey = ['bar'] await vi.advanceTimersByTimeAsync(10) mutation.mutate('xyz') await vi.advanceTimersByTimeAsync(10) const mutations = mutationCache.find({ mutationKey: ['bar'] }) expect(mutations?.options.mutationKey).toEqual(['bar']) }) test('should update reactive options deeply', async () => { type MutationKeyTest = { entity: string otherObject: { name: string } } const mutationKey = ref>([ { entity: 'test', otherObject: { name: 'objectName' }, }, ]) const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() const options = reactive({ mutationKey, mutationFn: (params: string) => sleep(10).then(() => params), }) const mutation = useMutation(options) mutationKey.value[0]!.otherObject.name = 'someOtherObjectName' await vi.advanceTimersByTimeAsync(10) mutation.mutate('xyz') await vi.advanceTimersByTimeAsync(10) const mutations = mutationCache.getAll() const relevantMutation = mutations.find((m) => { return ( Array.isArray(m.options.mutationKey) && !!m.options.mutationKey[0].otherObject ) }) expect( (relevantMutation?.options.mutationKey as Array)[0] ?.otherObject.name === 'someOtherObjectName', ).toBe(true) }) test('should allow for non-options object (mutationFn or mutationKey) passed as arg1 & arg2 to trigger reactive updates', async () => { const mutationKey = ref>(['foo2']) const mutationFn = ref((params: string) => sleep(0).then(() => params)) const queryClient = useQueryClient() const mutationCache = queryClient.getMutationCache() const mutation = useMutation({ mutationKey, mutationFn }) mutationKey.value = ['bar2'] let proof = false mutationFn.value = (params: string) => { proof = true return sleep(10).then(() => params) } await vi.advanceTimersByTimeAsync(10) mutation.mutate('xyz') await vi.advanceTimersByTimeAsync(10) const mutations = mutationCache.find({ mutationKey: ['bar2'] }) expect(mutations?.options.mutationKey).toEqual(['bar2']) expect(proof).toEqual(true) }) test('should reset state after invoking mutation.reset', async () => { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) mutation.mutate() await vi.advanceTimersByTimeAsync(10) mutation.reset() expect(mutation).toMatchObject({ isIdle: { value: true }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: false }, data: { value: undefined }, error: { value: null }, }) }) describe('side effects', () => { beforeEach(() => { vi.clearAllMocks() }) test('should call onMutate when passed as an option', async () => { const onMutate = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onMutate, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onMutate).toHaveBeenCalledTimes(1) }) test('should call onError when passed as an option', async () => { const onError = vi.fn() const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), onError, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onError).toHaveBeenCalledTimes(1) }) test('should call onSuccess when passed as an option', async () => { const onSuccess = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onSuccess, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call onSettled when passed as an option', async () => { const onSettled = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onSettled, }) mutation.mutate('') await vi.advanceTimersByTimeAsync(10) expect(onSettled).toHaveBeenCalledTimes(1) }) test('should call onError when passed as an argument of mutate function', async () => { const onError = vi.fn() const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) mutation.mutate(undefined, { onError }) await vi.advanceTimersByTimeAsync(10) expect(onError).toHaveBeenCalledTimes(1) }) test('should call onSuccess when passed as an argument of mutate function', async () => { const onSuccess = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) mutation.mutate('', { onSuccess }) await vi.advanceTimersByTimeAsync(10) expect(onSuccess).toHaveBeenCalledTimes(1) }) test('should call onSettled when passed as an argument of mutate function', async () => { const onSettled = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) mutation.mutate('', { onSettled }) await vi.advanceTimersByTimeAsync(10) expect(onSettled).toHaveBeenCalledTimes(1) }) test('should fire both onSettled functions', async () => { const onSettled = vi.fn() const onSettledOnFunction = vi.fn() const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), onSettled, }) mutation.mutate('', { onSettled: onSettledOnFunction }) await vi.advanceTimersByTimeAsync(10) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettledOnFunction).toHaveBeenCalledTimes(1) }) }) describe('async', () => { beforeEach(() => { vi.clearAllMocks() }) test('should resolve properly', async () => { const result = 'Mock data' const mutation = useMutation({ mutationFn: (params: string) => sleep(10).then(() => params), }) await vi.waitFor(() => expect(mutation.mutateAsync(result)).resolves.toBe(result), ) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: false }, isSuccess: { value: true }, data: { value: 'Mock data' }, error: { value: null }, }) }) test('should throw on error', async () => { const mutation = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), }) await vi.waitFor(() => expect(mutation.mutateAsync()).rejects.toThrowError('Some error'), ) expect(mutation).toMatchObject({ isIdle: { value: false }, isPending: { value: false }, isError: { value: true }, isSuccess: { value: false }, data: { value: undefined }, error: { value: Error('Some error') }, }) }) }) test('should warn when used outside of setup function in development mode', () => { vi.stubEnv('NODE_ENV', 'development') const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) try { useMutation({ mutationFn: (params: string) => sleep(0).then(() => params), }) expect(warnSpy).toHaveBeenCalledWith( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } finally { warnSpy.mockRestore() vi.unstubAllEnvs() } }) describe('throwOnError', () => { test('should evaluate throwOnError when mutation is expected to throw', async () => { const err = new Error('Expected mock error. All is well!') const boundaryFn = vi.fn() const { mutate } = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(err)), throwOnError: boundaryFn, }) mutate() await vi.advanceTimersByTimeAsync(10) expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith(err) }) test('should throw from error watcher when throwOnError returns true', async () => { const throwOnErrorFn = vi.fn().mockReturnValue(true) const { mutate } = useMutation({ mutationFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), throwOnError: throwOnErrorFn, }) mutate() // Suppress the Unhandled Rejection caused by watcher throw in Vue 3 const rejectionHandler = () => {} process.on('unhandledRejection', rejectionHandler) await vi.advanceTimersByTimeAsync(10) process.off('unhandledRejection', rejectionHandler) expect(throwOnErrorFn).toHaveBeenCalledTimes(1) expect(throwOnErrorFn).toHaveBeenCalledWith(Error('Some error')) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useMutationState.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useMutation } from '../useMutation' import { useMutationState } from '../useMutationState' import { useQueryClient } from '../useQueryClient' vi.mock('../useQueryClient') describe('useMutationState', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should return variables after calling mutate 1', () => { const mutationKey = ['mutation'] const variables = 'foo123' const { mutate } = useMutation({ mutationKey: mutationKey, mutationFn: (params: string) => sleep(0).then(() => params), }) mutate(variables) const mutationState = useMutationState({ filters: { mutationKey, status: 'pending' }, select: (mutation) => mutation.state.variables, }) expect(mutationState.value).toEqual([variables]) }) it('should return variables after calling mutate 2', () => { const queryClient = useQueryClient() queryClient.clear() const mutationKey = ['mutation'] const variables = 'bar234' const { mutate } = useMutation({ mutationKey: mutationKey, mutationFn: (params: string) => sleep(0).then(() => params), }) mutate(variables) const mutationState = useMutationState() expect(mutationState.value[0]?.variables).toEqual(variables) }) it('should work with options getter and be reactive', async () => { const keyRef = ref('useMutationStateGetter2') const variables = 'foo123' const { mutate } = useMutation({ mutationKey: ['useMutationStateGetter'], mutationFn: (params: string) => sleep(10).then(() => params), }) mutate(variables) const mutationState = useMutationState(() => ({ filters: { mutationKey: [keyRef.value], status: 'pending' }, select: (mutation) => mutation.state.variables, })) expect(mutationState.value).toEqual([]) keyRef.value = 'useMutationStateGetter' await vi.advanceTimersByTimeAsync(0) expect(mutationState.value).toEqual([variables]) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQueries.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { reactive } from 'vue' import { skipToken, useQueries } from '..' import { queryOptions } from '../queryOptions' import type { OmitKeyof, QueryObserverResult } from '..' import type { UseQueryOptions } from '../useQuery' describe('UseQueries config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const query1 = { queryKey: ['key1'], queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = queryOptions({ queryKey: ['key2'], queryFn: () => 'Query Data', initialData: 'initial data', }) const query3 = { queryKey: ['key2'], queryFn: () => 'Query Data', } const { value: queriesState } = useQueries({ queries: [query1, query2, query3], }) expectTypeOf(queriesState[0].data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(queriesState[1].data).toEqualTypeOf() expectTypeOf(queriesState[2].data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { value: queriesState } = useQueries({ queries: [options] }) expectTypeOf(queriesState[0].data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { const query1 = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: ['key'], queryFn: () => Promise.resolve(1), select: (data: any) => data > 1, } const queriesState = reactive(useQueries({ queries: [query1, query2] })) expectTypeOf(queriesState.value[0].data).toEqualTypeOf< boolean | undefined >() expectTypeOf(queriesState.value[1].data).toEqualTypeOf< boolean | undefined >() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const { value: queriesState } = useQueries({ queries: [ { queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) expectTypeOf(queriesState[0].data).toEqualTypeOf< { wow: boolean } | undefined >() }) it('TData should have correct type when conditional skipToken is passed', () => { const { value: queriesState } = useQueries({ queries: [ queryOptions({ queryKey: ['key'], queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }), ], }) const firstResult = queriesState[0] expectTypeOf(firstResult).toEqualTypeOf< QueryObserverResult >() expectTypeOf(firstResult.data).toEqualTypeOf() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { const useCustomQueries = ( options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn', 'safely' >, ) => useQueries({ queries: [ { ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }, ], }) const { value: queriesState } = useCustomQueries() expectTypeOf(queriesState[0].data).toEqualTypeOf() }) }) // Fix #7270 it('should have proper type inference with different options provided', () => { const numbers = [1, 2, 3] const queryKey = (n: number) => [n] const queryFn = (n: number) => () => Promise.resolve(n) const select = (data: number) => data.toString() const queries = numbers.map((n) => ({ queryKey: [n], queryFn: () => Promise.resolve(n), select: (data: number) => data.toString(), })) const queriesWithoutSelect = numbers.map((n) => ({ queryKey: queryKey(n), queryFn: queryFn(n), })) const queriesWithQueryOptions = numbers.map((n) => queryOptions({ queryKey: queryKey(n), queryFn: queryFn(n), select, }), ) const queriesWithQueryOptionsWithoutSelect = numbers.map((n) => queryOptions({ queryKey: queryKey(n), queryFn: queryFn(n), }), ) const query1 = useQueries({ queries: queries }) expectTypeOf(query1.value).toEqualTypeOf< Array> >() const query2 = useQueries({ queries: queriesWithoutSelect }) expectTypeOf(query2.value).toEqualTypeOf< Array> >() const query3 = useQueries({ queries: queriesWithQueryOptions }) expectTypeOf(query3.value).toEqualTypeOf< Array> >() const query4 = useQueries({ queries: queriesWithQueryOptionsWithoutSelect }) expectTypeOf(query4.value).toEqualTypeOf< Array> >() const queryCombine = useQueries({ queries: queries, combine: (data) => { return data.reduce((acc, i) => { acc.push(i.data ?? '') return acc }, [] as Array) }, }) expectTypeOf(queryCombine.value).toEqualTypeOf>() const queryCombineWithoutSelect = useQueries({ queries: queriesWithoutSelect, combine: (data) => { return data.reduce((acc, i) => { acc.push(i.data ?? 0) return acc }, [] as Array) }, }) expectTypeOf(queryCombineWithoutSelect.value).toEqualTypeOf>() }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: ['key1'], queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: ['key2'], queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const { value: queriesState } = useQueries({ queries: [...queries1List, { ...Queries2.get() }], }) expectTypeOf(queriesState).toEqualTypeOf< [ ...Array>, QueryObserverResult, ] >() expectTypeOf(queriesState[0].data).toEqualTypeOf< number | boolean | undefined >() }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQueries.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { onScopeDispose, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { useQueries } from '../useQueries' import { useQueryClient } from '../useQueryClient' import { QueryClient } from '../queryClient' import type { MockedFunction } from 'vitest' vi.mock('../useQueryClient') describe('useQueries', () => { beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should return result for each query', () => { const queries = [ { queryKey: ['key1'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key2'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) expect(queriesState.value).toMatchObject([ { status: 'pending', isPending: true, isFetching: true, isStale: true, }, { status: 'pending', isPending: true, isFetching: true, isStale: true, }, ]) }) test('should resolve to success and update reactive state', async () => { const queries = [ { queryKey: ['key11'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key12'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value).toMatchObject([ { status: 'success', isPending: false, isFetching: false, isStale: true, }, { status: 'success', isPending: false, isFetching: false, isStale: true, }, ]) }) test('should reject one of the queries and update reactive state', async () => { const queries = [ { queryKey: ['key21'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), }, { queryKey: ['key22'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value).toMatchObject([ { status: 'error', isPending: false, isFetching: false, isStale: true, }, { status: 'success', isPending: false, isFetching: false, isStale: true, }, ]) }) test('should return state for new queries', async () => { const queries = ref([ { queryKey: ['key31'], queryFn: () => sleep(0).then(() => 'value31'), }, { queryKey: ['key32'], queryFn: () => sleep(0).then(() => 'value32'), }, { queryKey: ['key33'], queryFn: () => sleep(0).then(() => 'value33'), }, ]) const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) queries.value.splice( 0, queries.value.length, { queryKey: ['key31'], queryFn: () => sleep(0).then(() => 'value31'), }, { queryKey: ['key34'], queryFn: () => sleep(0).then(() => 'value34'), }, ) await vi.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value.length).toEqual(2) expect(queriesState.value).toMatchObject([ { data: 'value31', status: 'success', isPending: false, isFetching: false, isStale: true, }, { data: 'value34', status: 'success', isPending: false, isFetching: false, isStale: true, }, ]) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementationOnce((fn) => fn()) const queries = [ { queryKey: ['key41'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key42'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] const queriesState = useQueries({ queries }) await vi.advanceTimersByTimeAsync(0) expect(queriesState.value).toMatchObject([ { status: 'pending', isPending: true, isFetching: true, isStale: true, }, { status: 'pending', isPending: true, isFetching: true, isStale: true, }, ]) }) test('should use queryClient provided via options', async () => { const queryClient = new QueryClient() const queries = [ { queryKey: ['key41'], queryFn: () => sleep(0).then(() => 'Some data'), }, { queryKey: ['key42'], queryFn: () => sleep(0).then(() => 'Some data'), }, ] useQueries({ queries }, queryClient) await vi.advanceTimersByTimeAsync(0) expect(useQueryClient).toHaveBeenCalledTimes(0) }) test('should combine queries', async () => { const firstResult = 'first result' const secondResult = 'second result' const queryClient = new QueryClient() const queries = [ { queryKey: ['key41'], queryFn: () => sleep(0).then(() => firstResult), }, { queryKey: ['key42'], queryFn: () => sleep(0).then(() => secondResult), }, ] const queriesResult = useQueries( { queries, combine: (results) => { return { combined: true, res: results.map((res) => res.data), } }, }, queryClient, ) await vi.advanceTimersByTimeAsync(0) expect(queriesResult.value).toMatchObject({ combined: true, res: [firstResult, secondResult], }) }) test('should be `enabled` to accept getter function', async () => { const fetchFn = vi.fn(() => 'foo') const checked = ref(false) useQueries({ queries: [ { queryKey: ['enabled'], queryFn: fetchFn, enabled: () => checked.value, }, ], }) expect(fetchFn).not.toHaveBeenCalled() checked.value = true await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalled() }) test('should allow getters for query keys', async () => { const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') useQueries({ queries: [ { queryKey: ['key', () => key1.value, () => key2.value], queryFn: fetchFn, }, ], }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key3' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key4' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) }) test('should allow arbitrarily nested getters for query keys', async () => { const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') const key4 = ref('key4') const key5 = ref('key5') useQueries({ queries: [ { queryKey: [ 'key', key1, () => key2.value, { key: () => key3.value }, [{ foo: { bar: () => key4.value } }], () => ({ foo: { bar: { baz: () => key5.value, }, }, }), ], queryFn: fetchFn, }, ], }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key1-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key2-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) key3.value = 'key3-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(4) key4.value = 'key4-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(5) key5.value = 'key5-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(6) }) test('should refetch only the specific query without affecting others', async () => { let userCount = 0 let postCount = 0 const queriesState = useQueries({ queries: [ { queryKey: ['users'], queryFn: () => sleep(10).then(() => `users-${++userCount}`), }, { queryKey: ['posts'], queryFn: () => sleep(20).then(() => `posts-${++postCount}`), }, ], }) await vi.advanceTimersByTimeAsync(20) expect(queriesState.value[0].data).toBe('users-1') expect(queriesState.value[1].data).toBe('posts-1') queriesState.value[0].refetch() await vi.advanceTimersByTimeAsync(10) expect(queriesState.value[0].data).toBe('users-2') expect(queriesState.value[1].data).toBe('posts-1') }) test('should warn when used outside of setup function in development mode', () => { vi.stubEnv('NODE_ENV', 'development') const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) try { useQueries({ queries: [ { queryKey: ['outsideScope'], queryFn: () => sleep(0).then(() => 'data'), }, ], }) expect(warnSpy).toHaveBeenCalledWith( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } finally { warnSpy.mockRestore() vi.unstubAllEnvs() } }) test('should work with options getter and be reactive', async () => { const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') const key4 = ref('key4') const key5 = ref('key5') useQueries({ queries: () => [ { queryKey: [ 'key', key1, key2.value, { key: key3.value }, [{ foo: { bar: key4.value } }], () => ({ foo: { bar: { baz: key5.value, }, }, }), ], queryFn: fetchFn, }, ], }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key1-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key2-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) key3.value = 'key3-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(4) key4.value = 'key4-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(5) key5.value = 'key5-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(6) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQuery.test-d.ts ================================================ import { describe, expectTypeOf, it } from 'vitest' import { computed, reactive, ref } from 'vue-demi' import { sleep } from '@tanstack/query-test-utils' import { queryOptions, useQuery } from '..' import type { OmitKeyof, UseQueryOptions } from '..' describe('useQuery', () => { describe('Config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const { data } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const { data } = reactive(useQuery(options)) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(1), }) const query = reactive( useQuery({ ...options, select: (data) => data > 1, }), ) expectTypeOf(query.data).toEqualTypeOf() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { const { data } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { 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 } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { 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 } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ) expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const { data, isSuccess } = reactive( useQuery({ queryKey: ['key'], queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }), ) if (isSuccess) { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() } }) it('data should not have undefined when initialData is provided', () => { const { data } = reactive( useQuery({ queryKey: ['query-key'], initialData: 42, }), ) expectTypeOf(data).toEqualTypeOf() }) }) describe('custom composable', () => { it('should allow custom composable using UseQueryOptions', () => { const useCustomQuery = ( options?: OmitKeyof< UseQueryOptions, 'queryKey' | 'queryFn', 'safely' >, ) => { return useQuery({ ...options, queryKey: ['todos-key'], queryFn: () => Promise.resolve('data'), }) } const { data } = reactive(useCustomQuery()) expectTypeOf(data).toEqualTypeOf() }) }) describe('structuralSharing', () => { it('should be able to use structuralSharing with unknown types', () => { // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 useQuery({ queryKey: ['key'], queryFn: () => 5, structuralSharing: (oldData, newData) => { expectTypeOf(oldData).toBeUnknown() expectTypeOf(newData).toBeUnknown() return newData }, }) }) }) describe('Discriminated union return type', () => { it('data should be possibly undefined by default', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) expectTypeOf(query.data).toEqualTypeOf() }) it('data should be defined when query is success', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) it('error should be null when query is success', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isSuccess) { expectTypeOf(query.error).toEqualTypeOf() } }) it('data should be undefined when query is pending', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isPending) { expectTypeOf(query.data).toEqualTypeOf() } }) it('error should be defined when query is error', () => { const query = reactive( useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) if (query.isError) { expectTypeOf(query.error).toEqualTypeOf() } }) }) describe('accept ref options', () => { it('should accept ref options', () => { const options = ref({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }) const query = reactive(useQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) it('should accept computed options', () => { const options = computed(() => ({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), })) const query = reactive(useQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) it('should accept computed query options', () => { const options = computed(() => queryOptions({ queryKey: ['key'], queryFn: () => sleep(0).then(() => 'Some data'), }), ) const query = reactive(useQuery(options)) if (query.isSuccess) { expectTypeOf(query.data).toEqualTypeOf() } }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQuery.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { computed, getCurrentInstance, onScopeDispose, reactive, ref, } from 'vue-demi' import { QueryObserver } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useBaseQuery } from '../useBaseQuery' import type { Mock, MockedFunction } from 'vitest' vi.mock('../useQueryClient') vi.mock('../useBaseQuery') describe('useQuery', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) test('should properly execute query', () => { const queryFn = () => sleep(0).then(() => 'Some data') useQuery({ queryKey: ['key0'], queryFn, staleTime: 1000, }) expect(useBaseQuery).toBeCalledWith( QueryObserver, { queryKey: ['key0'], queryFn, staleTime: 1000, }, undefined, ) }) test('should work with options getter', async () => { const query = useQuery(() => ({ queryKey: ['key01'], queryFn: () => sleep(0).then(() => 'result01'), })) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result01' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should work with options getter and be reactive', async () => { const keyRef = ref('key011') const resultRef = ref('result02') const query = useQuery(() => ({ queryKey: [keyRef.value], queryFn: () => sleep(0).then(() => resultRef.value), })) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result02' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) resultRef.value = 'result021' keyRef.value = 'key012' await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result021' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should return pending status initially', () => { const query = useQuery({ queryKey: ['key1'], queryFn: () => sleep(0).then(() => 'Some data'), }) expect(query).toMatchObject({ status: { value: 'pending' }, isPending: { value: true }, isFetching: { value: true }, isStale: { value: true }, }) }) test('should resolve to success and update reactive state: useQuery(key, dataFn)', async () => { const query = useQuery({ queryKey: ['key2'], queryFn: () => sleep(0).then(() => 'result2'), }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result2' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should resolve to success and update reactive state: useQuery(optionsObj)', async () => { const query = useQuery({ queryKey: ['key31'], queryFn: () => sleep(0).then(() => 'result31'), enabled: true, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result31' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should resolve to success and update reactive state: useQuery(key, optionsObj)', async () => { const query = useQuery({ queryKey: ['key32'], queryFn: () => sleep(0).then(() => 'result32'), enabled: true, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'success' }, data: { value: 'result32' }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isSuccess: { value: true }, }) }) test('should reject and update reactive state', async () => { const query = useQuery({ queryKey: ['key3'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'error' }, data: { value: undefined }, error: { value: { message: 'Some error' } }, isPending: { value: false }, isFetching: { value: false }, isFetched: { value: true }, isError: { value: true }, failureCount: { value: 1 }, failureReason: { value: { message: 'Some error' } }, }) }) test('should update query on reactive (Ref) key change', async () => { const secondKeyRef = ref('key7') const query = useQuery({ queryKey: ['key6', secondKeyRef], queryFn: () => sleep(10).then(() => 'Some data'), }) await vi.advanceTimersByTimeAsync(10) expect(query).toMatchObject({ status: { value: 'success' }, }) secondKeyRef.value = 'key8' await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'pending' }, data: { value: undefined }, }) await vi.advanceTimersByTimeAsync(10) expect(query).toMatchObject({ status: { value: 'success' }, }) }) test("should update query when an option is passed as Ref and it's changed", async () => { const enabled = ref(false) const query = useQuery({ queryKey: ['key9'], queryFn: () => sleep(10).then(() => 'Some data'), enabled, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ fetchStatus: { value: 'idle' }, data: { value: undefined }, }) enabled.value = true await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ fetchStatus: { value: 'fetching' }, data: { value: undefined }, }) await vi.advanceTimersByTimeAsync(10) expect(query).toMatchObject({ status: { value: 'success' }, }) }) test('should properly execute dependent queries', async () => { const { data } = useQuery({ queryKey: ['dependent1'], queryFn: () => sleep(0).then(() => 'Some data'), }) const enabled = computed(() => !!data.value) const dependentQueryFn = vi .fn() .mockImplementation(() => sleep(10).then(() => 'Some data')) const { fetchStatus, status } = useQuery( reactive({ queryKey: ['dependent2'], queryFn: dependentQueryFn, enabled, }), ) expect(data.value).toStrictEqual(undefined) expect(fetchStatus.value).toStrictEqual('idle') expect(dependentQueryFn).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(0) expect(data.value).toStrictEqual('Some data') expect(fetchStatus.value).toStrictEqual('fetching') await vi.advanceTimersByTimeAsync(10) expect(fetchStatus.value).toStrictEqual('idle') expect(status.value).toStrictEqual('success') expect(dependentQueryFn).toHaveBeenCalledTimes(1) expect(dependentQueryFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['dependent2'] }), ) }) test('should stop listening to changes on onScopeDispose', async () => { const onScopeDisposeMock = onScopeDispose as MockedFunction< typeof onScopeDispose > onScopeDisposeMock.mockImplementationOnce((fn) => fn()) const { status } = useQuery({ queryKey: ['onScopeDispose'], queryFn: () => sleep(0).then(() => 'Some data'), }) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') await vi.advanceTimersByTimeAsync(0) expect(status.value).toStrictEqual('pending') }) test('should use the current value for the queryKey when refetch is called', async () => { const fetchFn = vi.fn(() => 'foo') const keyRef = ref('key11') const query = useQuery({ queryKey: ['key10', keyRef], queryFn: fetchFn, enabled: false, }) expect(fetchFn).not.toHaveBeenCalled() await query.refetch() expect(fetchFn).toHaveBeenCalledTimes(1) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['key10', 'key11'], }), ) keyRef.value = 'key12' await query.refetch() expect(fetchFn).toHaveBeenCalledTimes(2) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['key10', 'key12'], }), ) }) test('should be `enabled` to accept getter function', async () => { const fetchFn = vi.fn(() => 'foo') const checked = ref(false) useQuery({ queryKey: ['enabled'], queryFn: fetchFn, enabled: () => checked.value, }) expect(fetchFn).not.toHaveBeenCalled() checked.value = true await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalled() }) test('should allow getters for query keys', async () => { const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') useQuery({ queryKey: ['key', () => key1.value, () => key2.value], queryFn: fetchFn, }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key3' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key4' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) }) test('should allow arbitrarily nested getters for query keys', async () => { const fetchFn = vi.fn(() => 'foo') const key1 = ref('key1') const key2 = ref('key2') const key3 = ref('key3') const key4 = ref('key4') const key5 = ref('key5') useQuery({ queryKey: [ 'key', key1, () => key2.value, { key: () => key3.value }, [{ foo: { bar: () => key4.value } }], () => ({ foo: { bar: { baz: () => key5.value, }, }, }), ], queryFn: fetchFn, }) expect(fetchFn).toHaveBeenCalledTimes(1) key1.value = 'key1-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(2) key2.value = 'key2-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(3) key3.value = 'key3-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(4) key4.value = 'key4-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(5) key5.value = 'key5-updated' await vi.advanceTimersByTimeAsync(0) expect(fetchFn).toHaveBeenCalledTimes(6) }) describe('throwOnError', () => { test('should evaluate throwOnError when query is expected to throw', async () => { const boundaryFn = vi.fn() useQuery({ queryKey: ['key'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), retry: false, throwOnError: boundaryFn, }) await vi.advanceTimersByTimeAsync(0) expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith( Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) }) }) describe('outside scope warning', () => { test('should warn when used outside of setup function in development mode', () => { vi.stubEnv('NODE_ENV', 'development') const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) try { useQuery({ queryKey: ['outsideScope'], queryFn: () => sleep(0).then(() => 'data'), }) expect(warnSpy).toHaveBeenCalledWith( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } finally { warnSpy.mockRestore() vi.unstubAllEnvs() } }) }) describe('suspense', () => { test('should return a Promise', () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const query = useQuery({ queryKey: ['suspense'], queryFn: () => sleep(0).then(() => 'Some data'), }) const result = query.suspense() expect(result).toBeInstanceOf(Promise) }) test('should resolve after being enabled', async () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) let afterTimeout = false const isEnabled = ref(false) const query = useQuery({ queryKey: ['suspense2'], queryFn: () => sleep(0).then(() => 'Some data'), enabled: isEnabled, }) setTimeout(() => { afterTimeout = true isEnabled.value = true }, 200) query.suspense() await vi.advanceTimersByTimeAsync(200) expect(afterTimeout).toBe(true) }) test('should resolve immediately when stale without refetching', () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const fetcherSpy = vi.fn(() => sleep(0).then(() => 'Some data')) // let afterTimeout = false; const query = useQuery({ queryKey: ['suspense3'], queryFn: () => sleep(0).then(() => 'Some data'), staleTime: 10000, initialData: 'foo', }) return query.suspense().then(() => { expect(fetcherSpy).toHaveBeenCalledTimes(0) }) }) test('should not throw from suspense by default', async () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const query = useQuery({ queryKey: ['suspense4'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), staleTime: 10000, }) await vi.advanceTimersByTimeAsync(0) expect(query).toMatchObject({ status: { value: 'error' }, isError: { value: true }, }) }) test('should throw from suspense when throwOnError is true', async () => { const getCurrentInstanceSpy = getCurrentInstance as Mock getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) const boundaryFn = vi.fn() const query = useQuery({ queryKey: ['suspense5'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), staleTime: 10000, throwOnError: boundaryFn, }) query.suspense() await vi.advanceTimersByTimeAsync(10000) expect(boundaryFn).toHaveBeenCalledTimes(2) expect(boundaryFn).toHaveBeenNthCalledWith( 1, Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) expect(boundaryFn).toHaveBeenNthCalledWith( 2, Error('Some error'), expect.objectContaining({ state: expect.objectContaining({ status: 'error' }), }), ) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/useQueryClient.test.ts ================================================ import { beforeEach, describe, expect, test, vi } from 'vitest' import * as VueDemi from 'vue-demi' import { useQueryClient } from '../useQueryClient' import { VUE_QUERY_CLIENT } from '../utils' vi.mock('vue-demi', async () => { const actual = await vi.importActual('vue-demi') return { ...actual, inject: vi.fn(), hasInjectionContext: vi.fn(() => true), } }) describe('useQueryClient', () => { const injectSpy = vi.mocked(VueDemi.inject) const hasInjectionContextSpy = vi.mocked(VueDemi.hasInjectionContext) beforeEach(() => { vi.clearAllMocks() }) test('should return queryClient when it is provided in the context', () => { const queryClientMock = { name: 'Mocked client' } injectSpy.mockReturnValueOnce(queryClientMock) const queryClient = useQueryClient() expect(queryClient).toStrictEqual(queryClientMock) expect(injectSpy).toHaveBeenCalledTimes(1) expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) }) test('should throw an error when queryClient does not exist in the context', () => { injectSpy.mockReturnValueOnce(undefined) expect(useQueryClient).toThrowError() expect(injectSpy).toHaveBeenCalledTimes(1) expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) }) test('should throw an error when used outside of setup function', () => { hasInjectionContextSpy.mockReturnValueOnce(false) expect(useQueryClient).toThrowError() expect(hasInjectionContextSpy).toHaveBeenCalledTimes(1) }) test('should call inject with a custom key as a suffix', () => { const queryClientKey = 'foo' const expectedKeyParameter = `${VUE_QUERY_CLIENT}:${queryClientKey}` const queryClientMock = { name: 'Mocked client' } injectSpy.mockReturnValueOnce(queryClientMock) useQueryClient(queryClientKey) expect(injectSpy).toHaveBeenCalledWith(expectedKeyParameter) }) }) ================================================ FILE: packages/vue-query/src/__tests__/utils.test.ts ================================================ import { describe, expect, test } from 'vitest' import { reactive, ref } from 'vue-demi' import { cloneDeep, cloneDeepUnref, updateState } from '../utils' describe('utils', () => { describe('updateState', () => { test('should update first object with values from the second one', () => { const origin = { option1: 'a', option2: 'b', option3: 'c' } const update = { option1: 'x', option2: 'y', option3: 'z' } const expected = { option1: 'x', option2: 'y', option3: 'z' } updateState(origin, update) expect(origin).toEqual(expected) }) test('should update only existing keys', () => { const origin = { option1: 'a', option2: 'b' } const update = { option1: 'x', option2: 'y', option3: 'z' } const expected = { option1: 'x', option2: 'y' } updateState(origin, update) expect(origin).toEqual(expected) }) test('should remove non existing keys', () => { const origin = { option1: 'a', option2: 'b', option3: 'c' } const update = { option1: 'x', option2: 'y' } const expected = { option1: 'x', option2: 'y' } updateState(origin, update) expect(origin).toEqual(expected) }) }) describe('cloneDeep', () => { test('should copy primitives and functions AS-IS', () => { expect(cloneDeep(3456)).toBe(3456) expect(cloneDeep('theString')).toBe('theString') expect(cloneDeep(null)).toBe(null) }) test('should copy Maps and Sets AS-IS', () => { const setVal = new Set([3, 4, 5]) const setValCopy = cloneDeep(setVal) expect(setValCopy).toBe(setVal) expect(setValCopy).toStrictEqual(new Set([3, 4, 5])) const mapVal = new Map([ ['a', 'aVal'], ['b', 'bVal'], ]) const mapValCopy = cloneDeep(mapVal) expect(mapValCopy).toBe(mapVal) expect(mapValCopy).toStrictEqual( new Map([ ['a', 'aVal'], ['b', 'bVal'], ]), ) }) test('should deeply copy arrays', () => { const val = [ 25, 'str', null, new Set([3, 4]), [5, 6, { a: 1 }], undefined, ] const cp = cloneDeep(val) expect(cp).toStrictEqual([ 25, 'str', null, new Set([3, 4]), [5, 6, { a: 1 }], undefined, ]) expect(cp).not.toBe(val) expect(cp[3]).toBe(val[3]) // Set([3, 4]) expect(cp[4]).not.toBe(val[4]) // [5, 6, { a: 1 }] expect((cp[4] as Array)[2]).not.toBe((val[4] as Array)[2]) // { a : 1 } }) test('should deeply copy object', () => { const val = reactive({ a: 25, b: 'str', c: null, d: undefined, e: new Set([5, 6]), f: [3, 4], g: { fa: 26 }, }) const cp = cloneDeep(val) expect(cp).toStrictEqual({ a: 25, b: 'str', c: null, d: undefined, e: new Set([5, 6]), f: [3, 4], g: { fa: 26 }, }) expect(cp.e).toBe(val.e) // Set expect(cp.f).not.toBe(val.f) // [] expect(cp.g).not.toBe(val.g) // {} }) }) describe('cloneDeepUnref', () => { test('should unref primitives', () => { expect(cloneDeepUnref(ref(34))).toBe(34) expect(cloneDeepUnref(ref('myStr'))).toBe('myStr') }) test('should deeply unref arrays', () => { const val = ref([2, 3, ref(4), ref('5'), { a: ref(6) }, [ref(7)]]) const cp = cloneDeepUnref(val) expect(cp).toStrictEqual([2, 3, 4, '5', { a: 6 }, [7]]) }) test('should deeply unref objects', () => { const val = ref({ a: 1, b: ref(2), c: [ref('c1'), ref(['c2'])], d: { e: ref('e'), }, }) const cp = cloneDeepUnref(val) expect(cp).toEqual({ a: 1, b: 2, c: ['c1', ['c2']], d: { e: 'e' }, }) }) test('should clone getters returning values in queryKey', () => { const val = ref({ queryKey: [1, 2, () => '3'] }) const cp = cloneDeepUnref(val) expect(cp).toStrictEqual({ queryKey: [1, 2, '3'] }) }) test('should unref undefined', () => { expect(cloneDeepUnref(ref(undefined))).toBe(undefined) }) }) }) ================================================ FILE: packages/vue-query/src/__tests__/vueQueryPlugin.test.ts ================================================ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { isVue2, isVue3, ref } from 'vue-demi' import { QueryClient } from '../queryClient' import { VueQueryPlugin } from '../vueQueryPlugin' import { VUE_QUERY_CLIENT } from '../utils' import { setupDevtools } from '../devtools/devtools' import { useQuery } from '../useQuery' import { useQueries } from '../useQueries' import type { App, ComponentOptions } from 'vue' import type { Mock } from 'vitest' vi.mock('../devtools/devtools') vi.mock('../useQueryClient') vi.mock('../useBaseQuery') type UnmountCallback = () => void interface TestApp extends App { onUnmount: UnmountCallback _unmount: UnmountCallback _mixin: ComponentOptions _provided: Record $root: TestApp } const testIf = (condition: boolean) => (condition ? test : test.skip) function getAppMock(withUnmountHook = false): TestApp { const mock = { provide: vi.fn(), unmount: vi.fn(), onUnmount: withUnmountHook ? vi.fn((u: UnmountCallback) => { mock._unmount = u }) : undefined, mixin: (m: ComponentOptions) => { mock._mixin = m }, } as unknown as TestApp return mock } describe('VueQueryPlugin', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('devtools', () => { test('should NOT setup devtools', () => { const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock) expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) }) testIf(isVue2)('should NOT setup devtools by default', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock) appMock.$root = appMock appMock._mixin.beforeCreate?.call(appMock) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) }) testIf(isVue2)('should setup devtools', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock, { enableDevtoolsV6Plugin: true }) appMock.$root = appMock appMock._mixin.beforeCreate?.call(appMock) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(1) }) testIf(isVue3)('should NOT setup devtools by default', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) }) testIf(isVue3)('should setup devtools', () => { const envCopy = process.env.NODE_ENV process.env.NODE_ENV = 'development' const setupDevtoolsMock = setupDevtools as Mock const appMock = getAppMock() VueQueryPlugin.install(appMock, { enableDevtoolsV6Plugin: true }) process.env.NODE_ENV = envCopy expect(setupDevtoolsMock).toHaveBeenCalledTimes(1) }) }) describe('when app unmounts', () => { test('should call unmount on each client when onUnmount is missing', () => { const appMock = getAppMock() const customClient = { mount: vi.fn(), unmount: vi.fn(), } as unknown as QueryClient const originalUnmount = appMock.unmount VueQueryPlugin.install(appMock, { queryClient: customClient, }) appMock.unmount() expect(appMock.unmount).not.toEqual(originalUnmount) expect(customClient.unmount).toHaveBeenCalledTimes(1) expect(originalUnmount).toHaveBeenCalledTimes(1) }) test('should call onUnmount if present', () => { const appMock = getAppMock(true) const customClient = { mount: vi.fn(), unmount: vi.fn(), } as unknown as QueryClient const originalUnmount = appMock.unmount VueQueryPlugin.install(appMock, { queryClient: customClient }) appMock._unmount() expect(appMock.unmount).toEqual(originalUnmount) expect(customClient.unmount).toHaveBeenCalledTimes(1) }) }) describe('when called without additional options', () => { testIf(isVue2)('should provide a client with default clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock) appMock._mixin.beforeCreate?.call(appMock) expect(appMock._provided).toMatchObject({ VUE_QUERY_CLIENT: expect.any(QueryClient), }) }) testIf(isVue3)('should provide a client with default clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock) expect(appMock.provide).toHaveBeenCalledWith( VUE_QUERY_CLIENT, expect.any(QueryClient), ) }) }) describe('when called with custom clientKey', () => { testIf(isVue2)('should provide a client with customized clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' }) appMock._mixin.beforeCreate?.call(appMock) expect(appMock._provided).toMatchObject({ [VUE_QUERY_CLIENT + ':CUSTOM']: expect.any(QueryClient), }) }) testIf(isVue3)('should provide a client with customized clientKey', () => { const appMock = getAppMock() VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' }) expect(appMock.provide).toHaveBeenCalledWith( VUE_QUERY_CLIENT + ':CUSTOM', expect.any(QueryClient), ) }) }) describe('when called with custom client', () => { testIf(isVue2)('should provide that custom client', () => { const appMock = getAppMock() const customClient = { mount: vi.fn() } as unknown as QueryClient VueQueryPlugin.install(appMock, { queryClient: customClient }) appMock._mixin.beforeCreate?.call(appMock) expect(customClient.mount).toHaveBeenCalled() expect(appMock._provided).toMatchObject({ VUE_QUERY_CLIENT: customClient, }) }) testIf(isVue3)('should provide that custom client', () => { const appMock = getAppMock() const customClient = { mount: vi.fn() } as unknown as QueryClient VueQueryPlugin.install(appMock, { queryClient: customClient }) expect(customClient.mount).toHaveBeenCalled() expect(appMock.provide).toHaveBeenCalledWith( VUE_QUERY_CLIENT, customClient, ) }) }) describe('when called with custom client config', () => { testIf(isVue2)( 'should instantiate a client with the provided config', () => { const appMock = getAppMock() const config = { defaultOptions: { queries: { enabled: true } }, } VueQueryPlugin.install(appMock, { queryClientConfig: config, }) appMock._mixin.beforeCreate?.call(appMock) const client = appMock._provided.VUE_QUERY_CLIENT as QueryClient const defaultOptions = client.getDefaultOptions() expect(defaultOptions).toEqual(config.defaultOptions) }, ) testIf(isVue3)( 'should instantiate a client with the provided config', () => { const appMock = getAppMock() const config = { defaultOptions: { queries: { enabled: true } }, } VueQueryPlugin.install(appMock, { queryClientConfig: config, }) const client = (appMock.provide as Mock).mock.calls[0]?.[1] const defaultOptions = client.getDefaultOptions() expect(defaultOptions).toEqual(config.defaultOptions) }, ) }) describe('when persister is provided', () => { test('should properly modify isRestoring flag on queryClient', async () => { const appMock = getAppMock() const customClient = { mount: vi.fn(), isRestoring: ref(false), } as unknown as QueryClient VueQueryPlugin.install(appMock, { queryClient: customClient, clientPersister: () => [ vi.fn(), new Promise((resolve) => { resolve() }), ], }) expect(customClient.isRestoring?.value).toBeTruthy() await vi.advanceTimersByTimeAsync(0) expect(customClient.isRestoring?.value).toBeFalsy() }) test('should delay useQuery subscription and not call fetcher if data is not stale', async () => { const appMock = getAppMock() const customClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 60, }, }, }) VueQueryPlugin.install(appMock, { queryClient: customClient, clientPersister: (client) => [ vi.fn(), new Promise((resolve) => { setTimeout(() => { client.setQueryData(['persist'], () => ({ foo: 'bar', })) resolve() }, 0) }), ], }) const fnSpy = vi.fn() const query = useQuery( { queryKey: ['persist'], queryFn: fnSpy, }, customClient, ) expect(customClient.isRestoring?.value).toBeTruthy() expect(query.isFetching.value).toBeFalsy() expect(query.data.value).toStrictEqual(undefined) expect(fnSpy).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(0) expect(customClient.isRestoring?.value).toBeFalsy() expect(query.data.value).toStrictEqual({ foo: 'bar' }) expect(fnSpy).toHaveBeenCalledTimes(0) }) test('should delay useQueries subscription and not call fetcher if data is not stale', async () => { const appMock = getAppMock() const customClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 60, }, }, }) VueQueryPlugin.install(appMock, { queryClient: customClient, clientPersister: (client) => [ vi.fn(), new Promise((resolve) => { setTimeout(() => { client.setQueryData(['persist1'], () => ({ foo1: 'bar1', })) client.setQueryData(['persist2'], () => ({ foo2: 'bar2', })) resolve() }, 0) }), ], }) const fnSpy = vi.fn() const query = useQuery( { queryKey: ['persist1'], queryFn: fnSpy, }, customClient, ) const queries = useQueries( { queries: [ { queryKey: ['persist2'], queryFn: fnSpy, }, ], }, customClient, ) expect(customClient.isRestoring?.value).toBeTruthy() expect(query.isFetching.value).toBeFalsy() expect(query.data.value).toStrictEqual(undefined) expect(queries.value[0].isFetching).toBeFalsy() expect(queries.value[0].data).toStrictEqual(undefined) expect(fnSpy).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(0) expect(customClient.isRestoring?.value).toBeFalsy() expect(query.data.value).toStrictEqual({ foo1: 'bar1' }) expect(queries.value[0].data).toStrictEqual({ foo2: 'bar2' }) expect(fnSpy).toHaveBeenCalledTimes(0) }) }) }) ================================================ FILE: packages/vue-query/src/devtools/devtools.ts ================================================ import { setupDevtoolsPlugin } from '@vue/devtools-api' import { rankItem } from '@tanstack/match-sorter-utils' import { onlineManager } from '@tanstack/query-core' import { getQueryStateLabel, getQueryStatusBg, getQueryStatusFg, sortFns, } from './utils' import type { CustomInspectorNode } from '@vue/devtools-api' import type { Query, QueryCacheNotifyEvent } from '@tanstack/query-core' import type { QueryClient } from '../queryClient' const pluginId = 'vue-query' const pluginName = 'Vue Query' export function setupDevtools(app: any, queryClient: QueryClient) { setupDevtoolsPlugin( { id: pluginId, label: pluginName, packageName: 'vue-query', homepage: 'https://tanstack.com/query/latest', logo: 'https://raw.githubusercontent.com/TanStack/query/main/packages/vue-query/media/vue-query.svg', app, settings: { baseSort: { type: 'choice', component: 'button-group', label: 'Sort Cache Entries', options: [ { label: 'ASC', value: 1, }, { label: 'DESC', value: -1, }, ], defaultValue: 1, }, sortFn: { type: 'choice', label: 'Sort Function', options: Object.keys(sortFns).map((key) => ({ label: key, value: key, })), defaultValue: Object.keys(sortFns)[0]!, }, onlineMode: { type: 'choice', component: 'button-group', label: 'Online mode', options: [ { label: 'Online', value: 1, }, { label: 'Offline', value: 0, }, ], defaultValue: 1, }, }, }, (api) => { const initialSettings = api.getSettings() onlineManager.setOnline(Boolean(initialSettings.onlineMode.valueOf())) const queryCache = queryClient.getQueryCache() api.addInspector({ id: pluginId, label: pluginName, icon: 'api', nodeActions: [ { icon: 'file_download', tooltip: 'Refetch', action: (queryHash: string) => { queryCache.get(queryHash)?.fetch() }, }, { icon: 'alarm', tooltip: 'Invalidate', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query queryClient.invalidateQueries(query) }, }, { icon: 'settings_backup_restore', tooltip: 'Reset', action: (queryHash: string) => { queryCache.get(queryHash)?.reset() }, }, { icon: 'delete', tooltip: 'Remove', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query queryCache.remove(query) }, }, { icon: 'hourglass_empty', tooltip: 'Force loading', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query query.setState({ data: undefined, status: 'pending', }) }, }, { icon: 'error_outline', tooltip: 'Force error', action: (queryHash: string) => { const query = queryCache.get(queryHash) as Query query.setState({ data: undefined, status: 'error', error: new Error('Unknown error from devtools'), }) }, }, ], }) api.addTimelineLayer({ id: pluginId, label: pluginName, color: 0xffd94c, }) queryCache.subscribe((event) => { api.sendInspectorTree(pluginId) api.sendInspectorState(pluginId) const queryEvents: Array = [ 'added', 'removed', 'updated', ] if (queryEvents.includes(event.type)) { api.addTimelineEvent({ layerId: pluginId, event: { title: event.type, subtitle: event.query.queryHash, time: api.now(), data: { queryHash: event.query.queryHash, ...event, }, }, }) } }) api.on.setPluginSettings((payload) => { if (payload.key === 'onlineMode') { onlineManager.setOnline(Boolean(payload.newValue)) } }) api.on.getInspectorTree((payload) => { if (payload.inspectorId === pluginId) { const queries = queryCache.getAll() const settings = api.getSettings() const filtered = payload.filter ? queries.filter( (item) => rankItem(item.queryHash, payload.filter).passed, ) : [...queries] const sorted = filtered.sort( (a, b) => sortFns[settings.sortFn]!(a, b) * settings.baseSort, ) const nodes: Array = sorted.map((query) => { const stateLabel = getQueryStateLabel(query) return { id: query.queryHash, label: query.queryHash, tags: [ { label: `${stateLabel} [${query.getObserversCount()}]`, textColor: getQueryStatusFg(query), backgroundColor: getQueryStatusBg(query), }, ], } }) payload.rootNodes = nodes } }) api.on.getInspectorState((payload) => { if (payload.inspectorId === pluginId) { const query = queryCache.get(payload.nodeId) if (!query) { return } payload.state = { ' Query Details': [ { key: 'Query key', value: query.queryHash, }, { key: 'Query status', value: getQueryStateLabel(query), }, { key: 'Observers', value: query.getObserversCount(), }, { key: 'Last Updated', value: new Date(query.state.dataUpdatedAt).toLocaleTimeString(), }, ], 'Data Explorer': [ { key: 'Data', value: query.state.data, }, ], 'Query Explorer': [ { key: 'Query', value: query, }, ], } } }) }, ) } ================================================ FILE: packages/vue-query/src/devtools/utils.ts ================================================ import type { Query } from '@tanstack/query-core' type SortFn = (a: Query, b: Query) => number enum QueryState { Fetching = 0, Fresh, Stale, Inactive, Paused, } export function getQueryState(query: Query): QueryState { if (query.state.fetchStatus === 'fetching') { return QueryState.Fetching } if (query.state.fetchStatus === 'paused') { return QueryState.Paused } if (!query.getObserversCount()) { return QueryState.Inactive } if (query.isStale()) { return QueryState.Stale } return QueryState.Fresh } export function getQueryStateLabel(query: Query): string { const queryState = getQueryState(query) if (queryState === QueryState.Fetching) { return 'fetching' } if (queryState === QueryState.Paused) { return 'paused' } if (queryState === QueryState.Stale) { return 'stale' } if (queryState === QueryState.Inactive) { return 'inactive' } return 'fresh' } export function getQueryStatusFg(query: Query): number { const queryState = getQueryState(query) if (queryState === QueryState.Stale) { return 0x000000 } return 0xffffff } export function getQueryStatusBg(query: Query): number { const queryState = getQueryState(query) if (queryState === QueryState.Fetching) { return 0x006bff } if (queryState === QueryState.Paused) { return 0x8c49eb } if (queryState === QueryState.Stale) { return 0xffb200 } if (queryState === QueryState.Inactive) { return 0x3f4e60 } return 0x008327 } const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash) const dateSort: SortFn = (a, b) => a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1 const statusAndDateSort: SortFn = (a, b) => { if (getQueryState(a) === getQueryState(b)) { return dateSort(a, b) } return getQueryState(a) > getQueryState(b) ? 1 : -1 } export const sortFns: Record = { 'Status > Last Updated': statusAndDateSort, 'Query Hash': queryHashSort, 'Last Updated': dateSort, } ================================================ FILE: packages/vue-query/src/index.ts ================================================ export * from '@tanstack/query-core' export { useQueryClient } from './useQueryClient' export { VueQueryPlugin } from './vueQueryPlugin' export { QueryClient } from './queryClient' export { QueryCache } from './queryCache' export { queryOptions } from './queryOptions' export { infiniteQueryOptions } from './infiniteQueryOptions' export type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' export { MutationCache } from './mutationCache' export { useQuery } from './useQuery' export { useQueries } from './useQueries' export { useInfiniteQuery } from './useInfiniteQuery' export { useMutation } from './useMutation' export { useIsFetching } from './useIsFetching' export { useIsMutating, useMutationState } from './useMutationState' export { VUE_QUERY_CLIENT } from './utils' export type { UseQueryOptions, UseQueryReturnType, UseQueryDefinedReturnType, UndefinedInitialQueryOptions, DefinedInitialQueryOptions, } from './useQuery' export type { UseInfiniteQueryOptions, UseInfiniteQueryReturnType, } from './useInfiniteQuery' export type { UseMutationOptions, UseMutationReturnType } from './useMutation' export type { UseQueriesOptions, UseQueriesResults } from './useQueries' export type { MutationFilters, MutationStateOptions } from './useMutationState' export type { QueryFilters } from './useIsFetching' export type { VueQueryPluginOptions } from './vueQueryPlugin' ================================================ FILE: packages/vue-query/src/infiniteQueryOptions.ts ================================================ import type { DataTag, DefaultError, InfiniteData, NonUndefinedGuard, QueryKey, } from '@tanstack/query-core' import type { UseInfiniteQueryOptions } from './useInfiniteQuery' export type UndefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData?: undefined } export type DefinedInitialDataInfiniteOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = UseInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { initialData: | NonUndefinedGuard> | (() => NonUndefinedGuard>) } export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { queryKey: DataTag, TError> } export function infiniteQueryOptions< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > & { queryKey: DataTag, TError> } export function infiniteQueryOptions(options: unknown) { return options } ================================================ FILE: packages/vue-query/src/mutationCache.ts ================================================ import { MutationCache as MC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import type { DefaultError, Mutation, MutationFilters, } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' export class MutationCache extends MC { find< TData = unknown, TError = DefaultError, TVariables = any, TOnMutateResult = unknown, >( filters: MaybeRefDeep, ): Mutation | undefined { return super.find(cloneDeepUnref(filters)) } findAll(filters: MaybeRefDeep = {}): Array { return super.findAll(cloneDeepUnref(filters)) } } ================================================ FILE: packages/vue-query/src/queryCache.ts ================================================ import { QueryCache as QC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import type { DefaultError, Query, QueryFilters, WithRequired, } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' export class QueryCache extends QC { find( filters: MaybeRefDeep>, ): Query | undefined { return super.find(cloneDeepUnref(filters)) } findAll(filters: MaybeRefDeep = {}): Array { return super.findAll(cloneDeepUnref(filters)) } } ================================================ FILE: packages/vue-query/src/queryClient.ts ================================================ import { nextTick, ref } from 'vue-demi' import { QueryClient as QC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import { QueryCache } from './queryCache' import { MutationCache } from './mutationCache' import type { UseQueryOptions } from './useQuery' import type { Ref } from 'vue-demi' import type { MaybeRefDeep, NoUnknown, QueryClientConfig } from './types' import type { CancelOptions, DefaultError, DefaultOptions, EnsureQueryDataOptions, FetchInfiniteQueryOptions, FetchQueryOptions, InferDataFromTag, InferErrorFromTag, InfiniteData, InvalidateOptions, InvalidateQueryFilters, MutationFilters, MutationKey, MutationObserverOptions, NoInfer, OmitKeyof, QueryFilters, QueryKey, QueryObserverOptions, QueryState, RefetchOptions, RefetchQueryFilters, ResetOptions, SetDataOptions, Updater, } from '@tanstack/query-core' export class QueryClient extends QC { constructor(config: QueryClientConfig = {}) { const vueQueryConfig = { defaultOptions: config.defaultOptions, queryCache: config.queryCache || new QueryCache(), mutationCache: config.mutationCache || new MutationCache(), } super(vueQueryConfig) } isRestoring?: Ref = ref(false) isFetching(filters: MaybeRefDeep = {}): number { return super.isFetching(cloneDeepUnref(filters)) } isMutating(filters: MaybeRefDeep = {}): number { return super.isMutating(cloneDeepUnref(filters)) } getQueryData( queryKey: TTaggedQueryKey, ): InferDataFromTag | undefined getQueryData( queryKey: MaybeRefDeep, ): TData | undefined getQueryData( queryKey: MaybeRefDeep, ): TData | undefined { return super.getQueryData(cloneDeepUnref(queryKey)) } ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: EnsureQueryDataOptions, ): Promise ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< EnsureQueryDataOptions >, ): Promise ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< EnsureQueryDataOptions >, ): Promise { return super.ensureQueryData(cloneDeepUnref(options)) } getQueriesData( filters: MaybeRefDeep, ): Array<[QueryKey, TData | undefined]> { return super.getQueriesData(cloneDeepUnref(filters)) } setQueryData< TQueryFnData = unknown, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, >( queryKey: TTaggedQueryKey, updater: Updater< NoInfer | undefined, NoInfer | undefined >, options?: MaybeRefDeep, ): NoInfer | undefined setQueryData>( queryKey: MaybeRefDeep, updater: Updater | undefined, NoInfer | undefined>, options?: MaybeRefDeep, ): NoInfer | undefined setQueryData( queryKey: MaybeRefDeep, updater: Updater, options: MaybeRefDeep = {}, ): NoInfer | undefined { return super.setQueryData( cloneDeepUnref(queryKey), updater, cloneDeepUnref(options), ) } setQueriesData( filters: MaybeRefDeep, updater: Updater, options: MaybeRefDeep = {}, ): Array<[QueryKey, TData | undefined]> { return super.setQueriesData( cloneDeepUnref(filters), updater, cloneDeepUnref(options), ) } getQueryState( queryKey: MaybeRefDeep, ): QueryState | undefined { return super.getQueryState(cloneDeepUnref(queryKey)) } removeQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >(filters?: QueryFilters): void removeQueries(filters: MaybeRefDeep = {}): void { return super.removeQueries(cloneDeepUnref(filters)) } resetQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: QueryFilters, options?: MaybeRefDeep, ): Promise resetQueries( filters: MaybeRefDeep = {}, options: MaybeRefDeep = {}, ): Promise { return super.resetQueries(cloneDeepUnref(filters), cloneDeepUnref(options)) } cancelQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: QueryFilters, options?: MaybeRefDeep, ): Promise cancelQueries( filters: MaybeRefDeep = {}, options: MaybeRefDeep = {}, ): Promise { return super.cancelQueries(cloneDeepUnref(filters), cloneDeepUnref(options)) } invalidateQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: InvalidateQueryFilters, options?: MaybeRefDeep, ): Promise invalidateQueries( filters: MaybeRefDeep> = {}, options: MaybeRefDeep = {}, ): Promise { const filtersCloned = cloneDeepUnref(filters) const optionsCloned = cloneDeepUnref(options) super.invalidateQueries( { ...filtersCloned, refetchType: 'none' }, optionsCloned, ) if (filtersCloned.refetchType === 'none') { return Promise.resolve() } const refetchFilters: RefetchQueryFilters = { ...filtersCloned, type: filtersCloned.refetchType ?? filtersCloned.type ?? 'active', } // (dosipiuk): We need to delay `refetchQueries` execution to next macro task for all reactive values to be updated. // This ensures that `context` in `queryFn` while `invalidating` along reactive variable change has correct return nextTick().then(() => { return super.refetchQueries(refetchFilters, optionsCloned) }) } refetchQueries< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( filters?: RefetchQueryFilters, options?: MaybeRefDeep, ): Promise refetchQueries( filters: MaybeRefDeep = {}, options: MaybeRefDeep = {}, ): Promise { return super.refetchQueries( cloneDeepUnref(filters), cloneDeepUnref(options), ) } fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: FetchQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise { return super.fetchQuery(cloneDeepUnref(options)) } prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: FetchQueryOptions, ): Promise prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefDeep< FetchQueryOptions >, ): Promise { return super.prefetchQuery(cloneDeepUnref(options)) } fetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise> fetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise> fetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise> { return super.fetchInfiniteQuery(cloneDeepUnref(options)) } prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefDeep< FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, ): Promise { return super.prefetchInfiniteQuery(cloneDeepUnref(options)) } setDefaultOptions(options: MaybeRefDeep): void { super.setDefaultOptions(cloneDeepUnref(options)) } setQueryDefaults< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, >( queryKey: MaybeRefDeep, options: MaybeRefDeep< Omit, 'queryKey'> >, ): void { super.setQueryDefaults(cloneDeepUnref(queryKey), cloneDeepUnref(options)) } getQueryDefaults( queryKey: MaybeRefDeep, ): OmitKeyof, 'queryKey'> { return super.getQueryDefaults(cloneDeepUnref(queryKey)) } setMutationDefaults< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( mutationKey: MaybeRefDeep, options: MaybeRefDeep< MutationObserverOptions >, ): void { super.setMutationDefaults( cloneDeepUnref(mutationKey), cloneDeepUnref(options), ) } getMutationDefaults( mutationKey: MaybeRefDeep, ): MutationObserverOptions { return super.getMutationDefaults(cloneDeepUnref(mutationKey)) } } ================================================ FILE: packages/vue-query/src/queryOptions.ts ================================================ import type { DataTag, DefaultError, QueryKey } from '@tanstack/query-core' import type { DefinedInitialQueryOptions, UndefinedInitialQueryOptions, } from './useQuery' export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialQueryOptions, ): DefinedInitialQueryOptions & { queryKey: DataTag } export function queryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialQueryOptions, ): UndefinedInitialQueryOptions & { queryKey: DataTag } export function queryOptions(options: unknown) { return options } ================================================ FILE: packages/vue-query/src/types.ts ================================================ import type { DefaultError, DehydrateOptions, HydrateOptions, MutationCache, MutationObserverOptions, OmitKeyof, QueryCache, QueryObserverOptions, } from '@tanstack/query-core' import type { ComputedRef, Ref, UnwrapRef } from 'vue-demi' type Primitive = string | number | boolean | bigint | symbol | undefined | null type UnwrapLeaf = | Primitive | Function | Date | Error | RegExp | Map | WeakMap | Set | WeakSet export type MaybeRef = Ref | ComputedRef | T export type MaybeRefOrGetter = MaybeRef | (() => T) export type MaybeRefDeep = MaybeRef< T extends Function ? T : T extends object ? { [Property in keyof T]: MaybeRefDeep } : T > export type NoUnknown = Equal extends true ? never : T export type Equal = (() => T extends TTargetA ? 1 : 2) extends () => T extends TTargetB ? 1 : 2 ? true : false export type DeepUnwrapRef = T extends UnwrapLeaf ? T : T extends Ref ? DeepUnwrapRef : T extends {} ? { [Property in keyof T]: DeepUnwrapRef } : UnwrapRef export type ShallowOption = { /** * Return data in a shallow ref object (it is `false` by default). It can be set to `true` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. */ shallow?: boolean } export interface DefaultOptions { queries?: OmitKeyof, 'queryKey'> & ShallowOption mutations?: MutationObserverOptions & ShallowOption hydrate?: HydrateOptions['defaultOptions'] dehydrate?: DehydrateOptions } export interface QueryClientConfig { queryCache?: QueryCache mutationCache?: MutationCache defaultOptions?: DefaultOptions } ================================================ FILE: packages/vue-query/src/useBaseQuery.ts ================================================ import { computed, getCurrentScope, onScopeDispose, reactive, readonly, shallowReactive, shallowReadonly, toRefs, watch, } from 'vue-demi' import { shouldThrowError } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref, updateState } from './utils' import type { Ref } from 'vue-demi' import type { DefaultedQueryObserverOptions, QueryKey, QueryObserver, QueryObserverResult, } from '@tanstack/query-core' import type { QueryClient } from './queryClient' import type { UseQueryOptions } from './useQuery' import type { UseInfiniteQueryOptions } from './useInfiniteQuery' import type { MaybeRefOrGetter } from './types' export type UseBaseQueryReturnType< TData, TError, TResult = QueryObserverResult, > = { [K in keyof TResult]: K extends | 'fetchNextPage' | 'fetchPreviousPage' | 'refetch' ? TResult[K] : Ref[K]> } & { suspense: () => Promise } type UseQueryOptionsGeneric< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = | UseQueryOptions | UseInfiniteQueryOptions export function useBaseQuery< TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey, TPageParam, >( Observer: typeof QueryObserver, options: MaybeRefOrGetter< UseQueryOptionsGeneric< TQueryFnData, TError, TData, TQueryData, TQueryKey, TPageParam > >, queryClient?: QueryClient, ): UseBaseQueryReturnType { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const defaultedOptions = computed(() => { let resolvedOptions = options if (typeof resolvedOptions === 'function') { resolvedOptions = resolvedOptions() } const clonedOptions = cloneDeepUnref(resolvedOptions as any) if (typeof clonedOptions.enabled === 'function') { clonedOptions.enabled = clonedOptions.enabled() } const defaulted: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > = client.defaultQueryOptions(clonedOptions) defaulted._optimisticResults = client.isRestoring?.value ? 'isRestoring' : 'optimistic' return defaulted }) const observer = new Observer(client, defaultedOptions.value) // @ts-expect-error const state = defaultedOptions.value.shallow ? shallowReactive(observer.getCurrentResult()) : reactive(observer.getCurrentResult()) let unsubscribe = () => { // noop } if (client.isRestoring) { watch( client.isRestoring, (isRestoring) => { if (!isRestoring) { unsubscribe() unsubscribe = observer.subscribe((result) => { updateState(state, result) }) } }, { immediate: true }, ) } const updater = () => { observer.setOptions(defaultedOptions.value) updateState(state, observer.getCurrentResult()) } watch(defaultedOptions, updater) onScopeDispose(() => { unsubscribe() }) // fix #5910 const refetch = (...args: Parameters<(typeof state)['refetch']>) => { updater() return state.refetch(...args) } const suspense = () => { return new Promise>( (resolve, reject) => { let stopWatch = () => { // noop } const run = () => { if (defaultedOptions.value.enabled !== false) { // fix #6133 observer.setOptions(defaultedOptions.value) const optimisticResult = observer.getOptimisticResult( defaultedOptions.value, ) if (optimisticResult.isStale) { stopWatch() observer .fetchOptimistic(defaultedOptions.value) .then(resolve, (error: TError) => { if ( shouldThrowError(defaultedOptions.value.throwOnError, [ error, observer.getCurrentQuery(), ]) ) { reject(error) } else { resolve(observer.getCurrentResult()) } }) } else { stopWatch() resolve(optimisticResult) } } } run() stopWatch = watch(defaultedOptions, run) }, ) } // Handle error boundary watch( () => state.error, (error) => { if ( state.isError && !state.isFetching && shouldThrowError(defaultedOptions.value.throwOnError, [ error as TError, observer.getCurrentQuery(), ]) ) { throw error } }, ) // @ts-expect-error const readonlyState = defaultedOptions.value.shallow ? shallowReadonly(state) : readonly(state) const object: any = toRefs(readonlyState) for (const key in state) { if (typeof state[key as keyof typeof state] === 'function') { object[key] = state[key as keyof typeof state] } } object.suspense = suspense object.refetch = refetch return object as UseBaseQueryReturnType } ================================================ FILE: packages/vue-query/src/useInfiniteQuery.ts ================================================ import { InfiniteQueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' import type { DefaultError, InfiniteData, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, QueryKey, QueryObserver, } from '@tanstack/query-core' import type { UseBaseQueryReturnType } from './useBaseQuery' import type { DeepUnwrapRef, MaybeRef, MaybeRefDeep, MaybeRefOrGetter, ShallowOption, } from './types' import type { QueryClient } from './queryClient' export type UseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = MaybeRef< { [Property in keyof InfiniteQueryObserverOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >]: Property extends 'enabled' ? MaybeRefOrGetter< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, DeepUnwrapRef, TPageParam >[Property] > : MaybeRefDeep< InfiniteQueryObserverOptions< TQueryFnData, TError, TData, DeepUnwrapRef, TPageParam >[Property] > } & ShallowOption > export type UseInfiniteQueryReturnType = UseBaseQueryReturnType< TData, TError, InfiniteQueryObserverResult > export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefOrGetter< DefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, queryClient?: QueryClient, ): UseInfiniteQueryReturnType export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefOrGetter< UndefinedInitialDataInfiniteOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam > >, queryClient?: QueryClient, ): UseInfiniteQueryReturnType export function useInfiniteQuery< TQueryFnData, TError = DefaultError, TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: MaybeRefOrGetter< UseInfiniteQueryOptions >, queryClient?: QueryClient, ): UseInfiniteQueryReturnType export function useInfiniteQuery( options: MaybeRefOrGetter, queryClient?: QueryClient, ) { return useBaseQuery( InfiniteQueryObserver as typeof QueryObserver, options, queryClient, ) } ================================================ FILE: packages/vue-query/src/useIsFetching.ts ================================================ import { getCurrentScope, onScopeDispose, ref, watchEffect } from 'vue-demi' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { Ref } from 'vue-demi' import type { QueryFilters as QF } from '@tanstack/query-core' import type { MaybeRefDeep } from './types' import type { QueryClient } from './queryClient' export type QueryFilters = MaybeRefDeep | (() => MaybeRefDeep) export function useIsFetching( fetchingFilters: QueryFilters = {}, queryClient?: QueryClient, ): Ref { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const isFetching = ref() const listener = () => { const resolvedFilters = typeof fetchingFilters === 'function' ? fetchingFilters() : fetchingFilters isFetching.value = client.isFetching(cloneDeepUnref(resolvedFilters)) } const unsubscribe = client.getQueryCache().subscribe(listener) watchEffect(listener) onScopeDispose(() => { unsubscribe() }) return isFetching } ================================================ FILE: packages/vue-query/src/useMutation.ts ================================================ import { computed, getCurrentScope, onScopeDispose, reactive, readonly, shallowReactive, shallowReadonly, toRefs, watch, } from 'vue-demi' import { MutationObserver, shouldThrowError } from '@tanstack/query-core' import { cloneDeepUnref, updateState } from './utils' import { useQueryClient } from './useQueryClient' import type { ToRefs } from 'vue-demi' import type { DefaultError, DistributiveOmit, MutateFunction, MutateOptions, MutationObserverOptions, MutationObserverResult, OmitKeyof, } from '@tanstack/query-core' import type { MaybeRefDeep, ShallowOption } from './types' import type { QueryClient } from './queryClient' type MutationResult = DistributiveOmit< MutationObserverResult, 'mutate' | 'reset' > type UseMutationOptionsBase = OmitKeyof< MutationObserverOptions, '_defaulted' > & ShallowOption export type UseMutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = | MaybeRefDeep< UseMutationOptionsBase > | (() => MaybeRefDeep< UseMutationOptionsBase >) type MutateSyncFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( ...options: Parameters< MutateFunction > ) => void export type UseMutationReturnType< TData, TError, TVariables, TOnMutateResult, TResult = MutationResult, > = ToRefs> & { mutate: MutateSyncFunction mutateAsync: MutateFunction reset: MutationObserverResult< TData, TError, TVariables, TOnMutateResult >['reset'] } export function useMutation< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( mutationOptions: UseMutationOptions< TData, TError, TVariables, TOnMutateResult >, queryClient?: QueryClient, ): UseMutationReturnType { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const options = computed(() => { const resolvedOptions = typeof mutationOptions === 'function' ? mutationOptions() : mutationOptions return client.defaultMutationOptions(cloneDeepUnref(resolvedOptions)) }) const observer = new MutationObserver(client, options.value) const state = options.value.shallow ? shallowReactive(observer.getCurrentResult()) : reactive(observer.getCurrentResult()) const unsubscribe = observer.subscribe((result) => { updateState(state, result) }) const mutate = ( variables: TVariables, mutateOptions?: MutateOptions, ) => { observer.mutate(variables, mutateOptions).catch(() => { // This is intentional }) } watch(options, () => { observer.setOptions(options.value) }) onScopeDispose(() => { unsubscribe() }) const readonlyState = options.value.shallow ? shallowReadonly(state) : readonly(state) const resultRefs = toRefs(readonlyState) as ToRefs< Readonly> > watch( () => state.error, (error) => { if ( error && shouldThrowError(options.value.throwOnError, [error as TError]) ) { throw error } }, ) return { ...resultRefs, mutate, mutateAsync: state.mutate, reset: state.reset, } } ================================================ FILE: packages/vue-query/src/useMutationState.ts ================================================ import { computed, getCurrentScope, onScopeDispose, shallowReadonly, shallowRef, watch, } from 'vue-demi' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { Ref } from 'vue-demi' import type { MutationFilters as MF, Mutation, MutationState, } from '@tanstack/query-core' import type { QueryClient } from './queryClient' import type { MaybeRefDeep } from './types' import type { MutationCache } from './mutationCache' export type MutationFilters = MaybeRefDeep export function useIsMutating( filters: MutationFilters | (() => MutationFilters) = {}, queryClient?: QueryClient, ): Ref { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const mutationState = useMutationState( { filters: computed(() => ({ ...cloneDeepUnref(typeof filters === 'function' ? filters() : filters), status: 'pending' as const, })), }, client, ) const length = computed(() => mutationState.value.length) return length } export type MutationStateOptions = { filters?: MutationFilters select?: (mutation: Mutation) => TResult } function getResult( mutationCache: MutationCache, options: MutationStateOptions, ): Array { return mutationCache .findAll(options.filters) .map( (mutation): TResult => (options.select ? options.select(mutation) : mutation.state) as TResult, ) } export function useMutationState( options: | MutationStateOptions | (() => MutationStateOptions) = {}, queryClient?: QueryClient, ): Readonly>> { const resolvedOptions = computed(() => { const newOptions = typeof options === 'function' ? options() : options return { filters: cloneDeepUnref(newOptions.filters), select: newOptions.select, } }) const mutationCache = (queryClient || useQueryClient()).getMutationCache() const state = shallowRef(getResult(mutationCache, resolvedOptions.value)) const unsubscribe = mutationCache.subscribe(() => { state.value = getResult(mutationCache, resolvedOptions.value) }) watch(resolvedOptions, () => { state.value = getResult(mutationCache, resolvedOptions.value) }) onScopeDispose(() => { unsubscribe() }) return shallowReadonly(state) } ================================================ FILE: packages/vue-query/src/useQueries.ts ================================================ import { QueriesObserver } from '@tanstack/query-core' import { computed, getCurrentScope, onScopeDispose, readonly, shallowReadonly, shallowRef, unref, watch, } from 'vue-demi' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { Ref } from 'vue-demi' import type { DefaultError, DefinedQueryObserverResult, QueriesObserverOptions, QueryFunction, QueryKey, QueryObserverResult, ThrowOnError, } from '@tanstack/query-core' import type { UseQueryOptions } from './useQuery' import type { QueryClient } from './queryClient' import type { DeepUnwrapRef, MaybeRefDeep, ShallowOption } from './types' // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function does not have a parameter type UseQueryOptionsForUseQueries< TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions // 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 GetUseQueryOptionsForUseQueries = // Part 1: if UseQueryOptions are already being sent through, then just return T T extends UseQueryOptions ? DeepUnwrapRef : // Part 2: 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 : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? UseQueryOptionsForUseQueries : T extends { data: infer TData; error?: infer TError } ? UseQueryOptionsForUseQueries : // Part 3: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData, infer TError] ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData] ? UseQueryOptionsForUseQueries : // Part 4: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? UseQueryOptionsForUseQueries< TQueryFnData, unknown extends TError ? DefaultError : TError, unknown extends TData ? TQueryFnData : TData, TQueryKey > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? UseQueryOptionsForUseQueries< TQueryFnData, TError, TQueryFnData, TQueryKey > : // Fallback UseQueryOptionsForUseQueries // A defined initialData setting should return a DefinedQueryObserverResult rather than QueryObserverResult type GetDefinedOrUndefinedQueryResult = T extends { initialData?: infer TInitialData } ? unknown extends TInitialData ? QueryObserverResult : TInitialData extends TData ? DefinedQueryObserverResult : TInitialData extends () => infer TInitialDataResult ? unknown extends TInitialDataResult ? QueryObserverResult : TInitialDataResult extends TData ? DefinedQueryObserverResult : QueryObserverResult : QueryObserverResult : QueryObserverResult type GetUseQueryResult = // Part 1: if using UseQueryOptions then the types are already set T extends UseQueryOptions< infer TQueryFnData, infer TError, infer TData, any, any > ? GetDefinedOrUndefinedQueryResult< T, undefined extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : // Part 2: responsible for mapping explicit type parameter to function result, if object T extends { queryFnData: any; error?: infer TError; data: infer TData } ? GetDefinedOrUndefinedQueryResult : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? GetDefinedOrUndefinedQueryResult : T extends { data: infer TData; error?: infer TError } ? GetDefinedOrUndefinedQueryResult : // Part 3: responsible for mapping explicit type parameter to function result, if tuple T extends [any, infer TError, infer TData] ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData, infer TError] ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData] ? GetDefinedOrUndefinedQueryResult : // Part 4: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > : T extends { queryFn?: | QueryFunction | SkipTokenForUseQueries throwOnError?: ThrowOnError } ? GetDefinedOrUndefinedQueryResult< T, TQueryFnData, unknown extends TError ? DefaultError : TError > : // Fallback QueryObserverResult /** * UseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type UseQueriesOptions< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueries] : T extends [infer Head, ...infer Tails] ? UseQueriesOptions< [...Tails], [...TResults, GetUseQueryOptionsForUseQueries], [...TDepth, 1] > : ReadonlyArray extends T ? T : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous 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 /** * UseQueriesResults reducer recursively maps type param to results */ export type UseQueriesResults< T extends Array, TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryResult] : T extends [infer Head, ...infer Tails] ? UseQueriesResults< [...Tails], [...TResults, GetUseQueryResult], [...TDepth, 1] > : { [K in keyof T]: GetUseQueryResult } type UseQueriesOptionsArg> = readonly [ ...UseQueriesOptions, ] export function useQueries< T extends Array, TCombinedResult = UseQueriesResults, >( { queries, ...options }: ShallowOption & { queries: | (() => MaybeRefDeep>) | MaybeRefDeep> | MaybeRefDeep< readonly [ ...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }, ] > combine?: (result: UseQueriesResults) => TCombinedResult }, queryClient?: QueryClient, ): Readonly> { if (process.env.NODE_ENV === 'development') { if (!getCurrentScope()) { console.warn( 'vue-query composable like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.', ) } } const client = queryClient || useQueryClient() const defaultedQueries = computed(() => { const resolvedQueries = typeof queries === 'function' ? (queries as () => MaybeRefDeep>)() : queries // Only unref the top level array. const queriesRaw = unref(resolvedQueries) as ReadonlyArray // Unref the rest for each element in the top level array. return queriesRaw.map((queryOptions) => { const clonedOptions = cloneDeepUnref(queryOptions) if (typeof clonedOptions.enabled === 'function') { clonedOptions.enabled = queryOptions.enabled() } const defaulted = client.defaultQueryOptions(clonedOptions) defaulted._optimisticResults = client.isRestoring?.value ? 'isRestoring' : 'optimistic' return defaulted }) }) const observer = new QueriesObserver( client, defaultedQueries.value, options as QueriesObserverOptions, ) const getOptimisticResult = () => { const [results, getCombinedResult] = observer.getOptimisticResult( defaultedQueries.value, (options as QueriesObserverOptions).combine, ) return getCombinedResult( results.map((result, index) => { return { ...result, refetch: async (...args: Array) => { const [{ [index]: query }] = observer.getOptimisticResult( defaultedQueries.value, (options as QueriesObserverOptions).combine, ) return query!.refetch(...args) }, } }), ) } const state = shallowRef(getOptimisticResult()) let unsubscribe = () => { // noop } if (client.isRestoring) { watch( client.isRestoring, (isRestoring) => { if (!isRestoring) { unsubscribe() unsubscribe = observer.subscribe(() => { state.value = getOptimisticResult() }) state.value = getOptimisticResult() } }, { immediate: true }, ) } watch(defaultedQueries, (queriesValue) => { observer.setQueries( queriesValue, options as QueriesObserverOptions, ) state.value = getOptimisticResult() }) onScopeDispose(() => { unsubscribe() }) return options.shallow ? shallowReadonly(state) : (readonly(state) as Readonly>) } ================================================ FILE: packages/vue-query/src/useQuery.ts ================================================ import { QueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { DefaultError, DefinedQueryObserverResult, Enabled, InitialDataFunction, NonUndefinedGuard, QueryKey, QueryObserverOptions, } from '@tanstack/query-core' import type { UseBaseQueryReturnType } from './useBaseQuery' import type { DeepUnwrapRef, MaybeRef, MaybeRefDeep, MaybeRefOrGetter, ShallowOption, } from './types' import type { QueryClient } from './queryClient' export type UseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = MaybeRef< { [Property in keyof QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >]: Property extends 'enabled' ? | MaybeRefOrGetter | (() => Enabled< TQueryFnData, TError, TQueryData, DeepUnwrapRef >) : MaybeRefDeep< QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, DeepUnwrapRef >[Property] > } & ShallowOption > export type UndefinedInitialQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions & { initialData?: | undefined | InitialDataFunction> | NonUndefinedGuard } export type DefinedInitialQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = UseQueryOptions & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) } export type UseQueryReturnType = UseBaseQueryReturnType< TData, TError > export type UseQueryDefinedReturnType = UseBaseQueryReturnType< TData, TError, DefinedQueryObserverResult > export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialQueryOptions, queryClient?: QueryClient, ): UseQueryDefinedReturnType export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialQueryOptions, queryClient?: QueryClient, ): UseQueryReturnType export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefOrGetter< UseQueryOptions >, queryClient?: QueryClient, ): UseQueryReturnType export function useQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefOrGetter< UseQueryOptions >, queryClient?: QueryClient, ): | UseQueryReturnType | UseQueryDefinedReturnType { return useBaseQuery(QueryObserver, options, queryClient) } ================================================ FILE: packages/vue-query/src/useQueryClient.ts ================================================ import { hasInjectionContext, inject } from 'vue-demi' import { getClientKey } from './utils' import type { QueryClient } from './queryClient' export function useQueryClient(id = ''): QueryClient { // ensures that `inject()` can be used if (!hasInjectionContext()) { throw new Error( 'vue-query hooks can only be used inside setup() function or functions that support injection context.', ) } const key = getClientKey(id) const queryClient = inject(key) if (!queryClient) { throw new Error( "No 'queryClient' found in Vue context, use 'VueQueryPlugin' to properly initialize the library.", ) } return queryClient } ================================================ FILE: packages/vue-query/src/utils.ts ================================================ import { isRef, unref } from 'vue-demi' import type { MaybeRefDeep } from './types' export const VUE_QUERY_CLIENT = 'VUE_QUERY_CLIENT' export function getClientKey(key?: string) { const suffix = key ? `:${key}` : '' return `${VUE_QUERY_CLIENT}${suffix}` } export function updateState( state: Record, update: Record, ): void { Object.keys(state).forEach((key) => { state[key] = update[key] }) } // Helper function for cloning deep objects where // the level and key is provided to the callback function. function _cloneDeep( value: MaybeRefDeep, customize?: ( val: MaybeRefDeep, key: string, level: number, ) => T | undefined, currentKey: string = '', currentLevel: number = 0, ): T { if (customize) { const result = customize(value, currentKey, currentLevel) if (result === undefined && isRef(value)) { return result as T } if (result !== undefined) { return result } } if (Array.isArray(value)) { return value.map((val, index) => _cloneDeep(val, customize, String(index), currentLevel + 1), ) as unknown as T } if (typeof value === 'object' && isPlainObject(value)) { const entries = Object.entries(value).map(([key, val]) => [ key, _cloneDeep(val, customize, key, currentLevel + 1), ]) return Object.fromEntries(entries) } return value as T } export function cloneDeep( value: MaybeRefDeep, customize?: ( val: MaybeRefDeep, key: string, level: number, ) => T | undefined, ): T { return _cloneDeep(value, customize) } export function cloneDeepUnref( obj: MaybeRefDeep, unrefGetters = false, ): T { return cloneDeep(obj, (val, key, level) => { // Check if we're at the top level and the key is 'queryKey' // // If so, take the recursive descent where we resolve // getters to values as well as refs. if (level === 1 && key === 'queryKey') { return cloneDeepUnref(val, true) } // Resolve getters to values if specified. if (unrefGetters && isFunction(val)) { // Cast due to older TS versions not allowing calling // on certain intersection types. return cloneDeepUnref((val as Function)(), unrefGetters) } // Unref refs and continue to recurse into the value. if (isRef(val)) { return cloneDeepUnref(unref(val), unrefGetters) } return undefined }) } // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types function isPlainObject(value: unknown): value is Object { if (Object.prototype.toString.call(value) !== '[object Object]') { return false } const prototype = Object.getPrototypeOf(value) return prototype === null || prototype === Object.prototype } function isFunction(value: unknown): value is Function { return typeof value === 'function' } ================================================ FILE: packages/vue-query/src/vueQueryPlugin.ts ================================================ import { isVue2 } from 'vue-demi' import { isServer } from '@tanstack/query-core' import { QueryClient } from './queryClient' import { getClientKey } from './utils' import { setupDevtools } from './devtools/devtools' import type { QueryClientConfig } from './types' type ClientPersister = (client: QueryClient) => [() => void, Promise] interface CommonOptions { enableDevtoolsV6Plugin?: boolean queryClientKey?: string clientPersister?: ClientPersister clientPersisterOnSuccess?: (client: QueryClient) => void } interface ConfigOptions extends CommonOptions { queryClientConfig?: QueryClientConfig } interface ClientOptions extends CommonOptions { queryClient?: QueryClient } export type VueQueryPluginOptions = ConfigOptions | ClientOptions export const VueQueryPlugin = { install: (app: any, options: VueQueryPluginOptions = {}) => { const clientKey = getClientKey(options.queryClientKey) let client: QueryClient if ('queryClient' in options && options.queryClient) { client = options.queryClient } else { const clientConfig = 'queryClientConfig' in options ? options.queryClientConfig : undefined client = new QueryClient(clientConfig) } if (!isServer) { client.mount() } let persisterUnmount = () => { // noop } if (options.clientPersister) { if (client.isRestoring) { client.isRestoring.value = true } const [unmount, promise] = options.clientPersister(client) persisterUnmount = unmount promise.then(() => { if (client.isRestoring) { client.isRestoring.value = false } options.clientPersisterOnSuccess?.(client) }) } const cleanup = () => { client.unmount() persisterUnmount() } if (app.onUnmount) { app.onUnmount(cleanup) } else { const originalUnmount = app.unmount app.unmount = function vueQueryUnmount() { cleanup() originalUnmount() } } if (isVue2) { app.mixin({ beforeCreate() { // HACK: taken from provide(): https://github.com/vuejs/composition-api/blob/master/src/apis/inject.ts#L30 if (!this._provided) { const provideCache = {} Object.defineProperty(this, '_provided', { get: () => provideCache, set: (v) => Object.assign(provideCache, v), }) } this._provided[clientKey] = client if (process.env.NODE_ENV === 'development') { if (this === this.$root && options.enableDevtoolsV6Plugin) { setupDevtools(this, client) } } }, }) } else { app.provide(clientKey, client) if (process.env.NODE_ENV === 'development') { if (options.enableDevtoolsV6Plugin) { setupDevtools(app, client) } } } }, } ================================================ FILE: packages/vue-query/test-setup.ts ================================================ import { vi } from 'vitest' vi.mock('vue-demi', async () => { const vue = await vi.importActual('vue-demi') return { ...(vue as any), inject: vi.fn(), provide: vi.fn(), onScopeDispose: vi.fn(), getCurrentInstance: vi.fn(() => ({ proxy: {} })), hasInjectionContext: vi.fn(() => true), } }) ================================================ FILE: packages/vue-query/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "test-setup.ts", "*.config.*", "package.json"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/vue-query/tsconfig.legacy.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist-ts/legacy" }, "include": ["src"], "exclude": ["src/__mocks__", "src/__tests__"], "references": [{ "path": "../query-core" }] } ================================================ FILE: packages/vue-query/tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "incremental": false, "composite": false, "rootDir": "../../" } } ================================================ FILE: packages/vue-query/tsup.config.ts ================================================ import { defineConfig } from 'tsup' import { legacyConfig, modernConfig } from './root.tsup.config.js' export default defineConfig([ modernConfig({ entry: ['src/*.ts', 'src/devtools/*.ts'] }), legacyConfig({ entry: ['src/*.ts', 'src/devtools/*.ts'] }), ]) ================================================ FILE: packages/vue-query/vite.config.ts ================================================ import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import packageJson from './package.json' export default defineConfig({ plugins: [vue()], // 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: true, provider: 'istanbul', include: ['src/**/*'], exclude: ['src/__tests__/**'], }, typecheck: { enabled: true }, onConsoleLog: function (log) { if (log.includes('Download the Vue Devtools extension')) { return false } return undefined }, }, }) ================================================ FILE: packages/vue-query-devtools/.attw.json ================================================ { "ignoreRules": [ "cjs-resolves-to-esm", "internal-resolution-error", "no-resolution" ] } ================================================ FILE: packages/vue-query-devtools/CHANGELOG.md ================================================ # @tanstack/vue-query-devtools ## 6.1.5 ### Patch Changes - fix: style prop type ([#10087](https://github.com/TanStack/query/pull/10087)) ## 6.1.4 ### Patch Changes - Updated dependencies [[`83366c4`](https://github.com/TanStack/query/commit/83366c46a6825b5c591399c705d8128743c527dd)]: - @tanstack/query-devtools@5.93.0 ## 6.1.3 ### Patch Changes - Updated dependencies [[`f9fc56a`](https://github.com/TanStack/query/commit/f9fc56a9b8724bcfae46f8f6cb229123478eb4db), [`0b29b6f`](https://github.com/TanStack/query/commit/0b29b6f877d4b3a6d05b1c85fb9cb1e6ea736291)]: - @tanstack/query-devtools@5.92.0 - @tanstack/vue-query@5.92.3 ## 6.1.2 ### Patch Changes - Updated dependencies [[`758414f`](https://github.com/TanStack/query/commit/758414fcecb9b56142014edf3ff455c283fb556e)]: - @tanstack/vue-query@5.92.0 ## 6.1.1 ### Patch Changes - Updated dependencies [[`b261b6f`](https://github.com/TanStack/query/commit/b261b6f29eee2a9bdbe1bc20035fe9b83b15376b)]: - @tanstack/query-devtools@5.91.1 ## 6.1.0 ### Minor Changes - feat(devtools): allow passing a theme via prop ([#9887](https://github.com/TanStack/query/pull/9887)) ### Patch Changes - Updated dependencies [[`0e9d5b5`](https://github.com/TanStack/query/commit/0e9d5b565276f0de2a1a14ffbb079b5988581c27)]: - @tanstack/query-devtools@5.91.0 ## 6.0.0 ### Patch Changes - Updated dependencies [[`b2bd79d`](https://github.com/TanStack/query/commit/b2bd79d0a6b2707461897c426b0d2275a3318e4b)]: - @tanstack/vue-query@5.91.0 ## 5.91.0 ### Minor Changes - feat(vue-query-devtools): Add embedded panel mode ([#9790](https://github.com/TanStack/query/pull/9790)) ================================================ FILE: packages/vue-query-devtools/eslint.config.js ================================================ // @ts-check import pluginVue from 'eslint-plugin-vue' import rootConfig from './root.eslint.config.js' export default [...rootConfig, ...pluginVue.configs['flat/base']] ================================================ FILE: packages/vue-query-devtools/package.json ================================================ { "name": "@tanstack/vue-query-devtools", "version": "6.1.5", "description": "Developer tools to interact with and visualize the TanStack/vue-query cache", "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/query.git", "directory": "packages/vue-query-devtools" }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "scripts": { "clean": "premove ./build ./coverage ./dist-ts", "compile": "vue-tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "vue-tsc --build", "test:build": "publint --strict && attw --pack", "build": "pnpm run compile && vite build" }, "type": "module", "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", "main": "dist/esm/index.js", "exports": { ".": { "@tanstack/custom-condition": "./src/index.ts", "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "./production": { "@tanstack/custom-condition": "./src/production.ts", "types": "./dist/esm/production.d.ts", "default": "./dist/esm/production.js" }, "./dist/production.js": { "types": "./dist/esm/production.d.ts", "default": "./dist/esm/production.js" }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src", "!src/__tests__" ], "dependencies": { "@tanstack/query-devtools": "workspace:*" }, "devDependencies": { "@tanstack/vue-query": "workspace:*", "@vitejs/plugin-vue": "^5.2.4", "eslint-plugin-vue": "^10.5.0", "typescript": "5.8.3", "vite": "^6.4.1", "vue": "^3.4.27", "vue-tsc": "^2.2.8" }, "peerDependencies": { "@tanstack/vue-query": "workspace:^", "vue": "^3.3.0" } } ================================================ FILE: packages/vue-query-devtools/src/devtools.vue ================================================ ================================================ FILE: packages/vue-query-devtools/src/devtoolsPanel.vue ================================================ ================================================ FILE: packages/vue-query-devtools/src/index.ts ================================================ import devtools from './devtools.vue' import devtoolsPanel from './devtoolsPanel.vue' import type { DefineComponent } from 'vue' import type { DevtoolsOptions, DevtoolsPanelOptions } from './types' export const VueQueryDevtools = ( process.env.NODE_ENV !== 'development' ? function () { return null } : devtools ) as DefineComponent export const VueQueryDevtoolsPanel = ( process.env.NODE_ENV !== 'development' ? function () { return null } : devtoolsPanel ) as DefineComponent ================================================ FILE: packages/vue-query-devtools/src/production.ts ================================================ import devtools from './devtools.vue' export default devtools ================================================ FILE: packages/vue-query-devtools/src/types.ts ================================================ import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, Theme, } from '@tanstack/query-devtools' import type { QueryClient } from '@tanstack/vue-query' export interface DevtoolsOptions { /** * Set this true if you want the dev tools to default to being open */ initialIsOpen?: boolean /** * The position of the React Query logo to open and close the devtools panel. * 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' * Defaults to 'bottom-right'. */ buttonPosition?: DevtoolsButtonPosition /** * The position of the React Query devtools panel. * 'top' | 'bottom' | 'left' | 'right' * Defaults to 'bottom'. */ position?: DevtoolsPosition /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean /** * Set this to 'light', 'dark', or 'system' to change the theme of the devtools panel. * Defaults to 'system'. */ theme?: Theme } export interface DevtoolsPanelOptions { /** * Custom instance of QueryClient */ client?: QueryClient /** * Use this so you can define custom errors that can be shown in the devtools. */ errorTypes?: Array /** * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. */ styleNonce?: string /** * Use this so you can attach the devtool's styles to specific element in the DOM. */ shadowDOMTarget?: ShadowRoot /** * Custom styles for the devtools panel * @default { height: '500px' } * @example { height: '100%' } * @example { height: '100%', width: '100%' } */ style?: Partial /** * Callback function that is called when the devtools panel is closed */ onClose?: () => unknown /** * Set this to true to hide disabled queries from the devtools panel. */ hideDisabledQueries?: boolean /** * Set this to 'light', 'dark', or 'system' to change the theme of the devtools panel. * Defaults to 'system'. */ theme?: Theme } ================================================ FILE: packages/vue-query-devtools/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist-ts", "rootDir": "." }, "include": ["src", "*.config.*", "package.json"], "references": [{ "path": "../vue-query" }, { "path": "../query-devtools" }] } ================================================ FILE: packages/vue-query-devtools/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { tanstackViteConfig } from '@tanstack/vite-config' const config = defineConfig({ plugins: [vue()], // fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660 resolve: { conditions: ['@tanstack/custom-condition'], }, environments: { ssr: { resolve: { conditions: ['@tanstack/custom-condition'], }, }, }, }) export default mergeConfig( config, tanstackViteConfig({ entry: ['src/index.ts', 'src/production.ts'], srcDir: 'src', }), ) ================================================ FILE: pnpm-workspace.yaml ================================================ cleanupUnusedCatalogs: true linkWorkspacePackages: true preferWorkspacePackages: true packages: - 'packages/*' - 'integrations/*' - 'examples/angular/*' - 'examples/react/*' - 'examples/preact/*' - 'examples/solid/*' - 'examples/svelte/*' - 'examples/vue/*' - '!examples/vue/2*' - '!examples/vue/nuxt*' ================================================ FILE: prettier.config.js ================================================ // @ts-check /** @type {import('prettier').Config} */ const config = { semi: false, singleQuote: true, trailingComma: 'all', plugins: ['prettier-plugin-svelte'], overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }], } export default config ================================================ FILE: scripts/create-github-release.mjs ================================================ // @ts-nocheck import fs from 'fs' import path from 'node:path' import { globSync } from 'node:fs' import { execSync, execFileSync } from 'node:child_process' import { tmpdir } from 'node:os' const rootDir = path.join(import.meta.dirname, '..') const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN // Resolve GitHub usernames from commit author emails const usernameCache = {} async function resolveUsername(email) { if (!ghToken || !email) return null if (usernameCache[email] !== undefined) return usernameCache[email] try { const res = await fetch(`https://api.github.com/search/users?q=${email}`, { headers: { Authorization: `token ${ghToken}` }, }) const data = await res.json() const login = data?.items?.[0]?.login || null usernameCache[email] = login return login } catch { usernameCache[email] = null return null } } // Resolve author from a PR number via GitHub API const prAuthorCache = {} async function resolveAuthorForPR(prNumber) { if (prAuthorCache[prNumber] !== undefined) return prAuthorCache[prNumber] if (!ghToken) { prAuthorCache[prNumber] = null return null } try { const res = await fetch( `https://api.github.com/repos/TanStack/query/pulls/${prNumber}`, { headers: { Authorization: `token ${ghToken}` } }, ) const data = await res.json() const login = data?.user?.login || null prAuthorCache[prNumber] = login return login } catch { prAuthorCache[prNumber] = null return null } } // Get the previous release commit to diff against. // This script runs right after the "ci: changeset release" commit is pushed, // so HEAD is the release commit. const releaseLogs = execSync( 'git log --oneline --grep="ci: changeset release" --format=%H', ) .toString() .trim() .split('\n') .filter(Boolean) const currentRelease = releaseLogs[0] || 'HEAD' const previousRelease = releaseLogs[1] // Find packages that were actually bumped by comparing versions const packagesDir = path.join(rootDir, 'packages') const allPkgJsonPaths = globSync('*/package.json', { cwd: packagesDir }) const bumpedPackages = [] for (const relPath of allPkgJsonPaths) { const fullPath = path.join(packagesDir, relPath) const currentPkg = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) if (currentPkg.private) continue // Get the version from the previous release commit if (previousRelease) { try { const prevContent = execFileSync( 'git', ['show', `${previousRelease}:packages/${relPath}`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }, ) const prevPkg = JSON.parse(prevContent) if (prevPkg.version !== currentPkg.version) { bumpedPackages.push({ name: currentPkg.name, version: currentPkg.version, prevVersion: prevPkg.version, dir: path.dirname(relPath), }) } } catch { // Package didn't exist in previous release — it's new bumpedPackages.push({ name: currentPkg.name, version: currentPkg.version, prevVersion: null, dir: path.dirname(relPath), }) } } else { // No previous release — include all non-private packages bumpedPackages.push({ name: currentPkg.name, version: currentPkg.version, prevVersion: null, dir: path.dirname(relPath), }) } } bumpedPackages.sort((a, b) => a.name.localeCompare(b.name)) // Build changelog from git log between releases (conventional commits) const rangeFrom = previousRelease || `${currentRelease}~1` const rawLog = execSync( `git log ${rangeFrom}..${currentRelease} --pretty=format:"%h %ae %s" --no-merges`, { encoding: 'utf-8' }, ).trim() const typeOrder = [ 'breaking', 'feat', 'fix', 'perf', 'refactor', 'docs', 'chore', 'test', 'ci', ] const typeLabels = { breaking: '⚠️ Breaking Changes', feat: 'Features', fix: 'Fix', perf: 'Performance', refactor: 'Refactor', docs: 'Documentation', chore: 'Chore', test: 'Tests', ci: 'CI', } const typeIndex = (t) => { const i = typeOrder.indexOf(t) return i === -1 ? 99 : i } const groups = {} const commits = rawLog ? rawLog.split('\n') : [] for (const line of commits) { const match = line.match(/^(\w+)\s+(\S+)\s+(.*)$/) if (!match) continue const [, hash, email, subject] = match // Skip release commits if (subject.startsWith('ci: changeset release')) continue // Parse conventional commit: type(scope)!: message const conventionalMatch = subject.match(/^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.*)$/) const type = conventionalMatch ? conventionalMatch[1] : 'other' const isBreaking = conventionalMatch ? !!conventionalMatch[3] : false const scope = conventionalMatch ? conventionalMatch[2] || '' : '' const message = conventionalMatch ? conventionalMatch[4] : subject // Only include user-facing change types if (!['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) continue // Extract PR number if present const prMatch = message.match(/\(#(\d+)\)/) const prNumber = prMatch ? prMatch[1] : null const bucket = isBreaking ? 'breaking' : type if (!groups[bucket]) groups[bucket] = [] groups[bucket].push({ hash, email, scope, message, prNumber }) } // Build markdown grouped by conventional commit type const sortedTypes = Object.keys(groups).sort( (a, b) => typeIndex(a) - typeIndex(b), ) let changelogMd = '' for (const type of sortedTypes) { const label = typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1) changelogMd += `### ${label}\n\n` for (const commit of groups[type]) { const scopePrefix = commit.scope ? `${commit.scope}: ` : '' const cleanMessage = commit.message.replace(/\s*\(#\d+\)/, '') const prRef = commit.prNumber ? ` (#${commit.prNumber})` : '' const username = commit.prNumber ? await resolveAuthorForPR(commit.prNumber) : await resolveUsername(commit.email) const authorSuffix = username ? ` by @${username}` : '' changelogMd += `- ${scopePrefix}${cleanMessage}${prRef} (${commit.hash})${authorSuffix}\n` } changelogMd += '\n' } if (!changelogMd.trim()) { changelogMd = '- No changelog entries\n\n' } const now = new Date() const date = now.toISOString().slice(0, 10) const time = now.toISOString().slice(11, 16).replace(':', '') const tagName = `release-${date}-${time}` const titleDate = `${date} ${now.toISOString().slice(11, 16)}` const isPrerelease = process.argv.includes('--prerelease') const isLatest = process.argv.includes('--latest') const body = `Release ${titleDate} ## Changes ${changelogMd} ## Packages ${bumpedPackages.map((p) => `- ${p.name}@${p.version}`).join('\n')} ` // Create the release // Check if tag already exists — if so, try to create the release for it // (handles retries where the tag was pushed but release creation failed) let tagExists = false try { execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' }) tagExists = true } catch { // Tag doesn't exist yet } if (!tagExists) { execSync(`git tag -a -m "${tagName}" ${tagName}`) execSync('git push --tags') } const prereleaseFlag = isPrerelease ? '--prerelease' : '' const latestFlag = isLatest ? ' --latest' : '' const tmpFile = path.join(tmpdir(), `release-notes-${tagName}.md`) fs.writeFileSync(tmpFile, body) try { execSync( `gh release create ${tagName} ${prereleaseFlag} --title "Release ${titleDate}" --notes-file ${tmpFile}${latestFlag}`, { stdio: 'inherit' }, ) console.info(`GitHub release ${tagName} created.`) } catch (err) { // Clean up the tag if we created it but release failed if (!tagExists) { console.info(`Release creation failed, cleaning up tag ${tagName}...`) try { execSync(`git push --delete origin ${tagName}`, { stdio: 'ignore' }) execSync(`git tag -d ${tagName}`, { stdio: 'ignore' }) } catch { // Best effort cleanup } } throw err } finally { fs.unlinkSync(tmpFile) } ================================================ FILE: scripts/generate-docs.ts ================================================ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { generateReferenceDocs } from '@tanstack/typedoc-config' const __dirname = fileURLToPath(new URL('.', import.meta.url)) await generateReferenceDocs({ packages: [ { name: 'angular-query-experimental', entryPoints: [ resolve( __dirname, '../packages/angular-query-experimental/src/index.ts', ), ], tsconfig: resolve( __dirname, '../packages/angular-query-experimental/tsconfig.json', ), outputDir: resolve(__dirname, '../docs/framework/angular/reference'), exclude: ['./packages/query-core/**/*'], }, { name: 'svelte-query', entryPoints: [ resolve(__dirname, '../packages/svelte-query/src/index.ts'), ], tsconfig: resolve(__dirname, '../packages/svelte-query/tsconfig.json'), outputDir: resolve(__dirname, '../docs/framework/svelte/reference'), exclude: ['./packages/query-core/**/*'], }, { name: 'preact-query', entryPoints: [ resolve(__dirname, '../packages/preact-query/src/index.ts'), ], tsconfig: resolve(__dirname, '../packages/preact-query/tsconfig.json'), outputDir: resolve(__dirname, '../docs/framework/preact/reference'), exclude: ['./packages/query-core/**/*'], }, ], }) console.log('\n✅ All markdown files have been processed!') process.exit(0) ================================================ FILE: scripts/generate-labeler-config.ts ================================================ import * as fs from 'node:fs' import * as path from 'node:path' import * as prettier from 'prettier' /** Pairs of package labels and their corresponding paths */ type LabelerPair = [string, string] function readPairsFromFs(): Array { const ignored = new Set(['.DS_Store']) const pairs: Array = [] // Add subfolders in the packages folder, i.e. packages/** fs.readdirSync(path.resolve('packages')) .filter((folder) => !ignored.has(folder)) .forEach((folder) => { // Check if package.json exists for the folder before adding it if ( fs.existsSync( path.resolve(path.join('packages', folder, 'package.json')), ) ) { pairs.push([`package: ${folder}`, `packages/${folder}/**/*`]) } else { console.info( `Skipping \`${folder}\` as it does not have a \`package.json\` file.`, ) } }) // Sort by package name in alphabetical order pairs.sort((a, b) => a[0].localeCompare(b[0])) return pairs } async function generateLabelerYaml(pairs: Array): Promise { function s(n = 1) { return ' '.repeat(n) } // Convert the pairs into valid yaml const formattedPairs = pairs .map(([packageLabel, packagePath]) => { const result = [ `'${packageLabel}':`, `${s(2)}-${s(1)}changed-files:`, `${s(4)}-${s(1)}any-glob-to-any-file:${s(1)}'${packagePath}'`, ].join('\n') return result }) .join('\n') // Get the location of the Prettier config file const prettierConfigPath = await prettier.resolveConfigFile() if (!prettierConfigPath) { throw new Error( 'No Prettier config file found. Please ensure you have a Prettier config file in your project.', ) } console.info('using prettier config file at:', prettierConfigPath) // Resolve the Prettier config const prettierConfig = await prettier.resolveConfig(prettierConfigPath) console.info('using resolved prettier config:', prettierConfig) // Format the YAML string using Prettier const formattedStr = await prettier.format(formattedPairs, { parser: 'yaml', ...prettierConfig, }) return formattedStr } async function run() { console.info('Generating labeler config...') // Generate the pairs of package labels and their corresponding paths const pairs = readPairsFromFs() // Always add the docs folder pairs.push(['documentation', 'docs/**/*']) // Convert the pairs into valid yaml const yamlStr = await generateLabelerYaml(pairs) // Write to 'labeler-config.yml' const configPath = path.resolve('labeler-config.yml') fs.writeFileSync(configPath, yamlStr, { encoding: 'utf-8', }) console.info(`Generated labeler config at \`${configPath}\`!`) return } try { run().then(() => { process.exit(0) }) } catch (error) { console.error('Error generating labeler config:', error) process.exit(1) } ================================================ FILE: scripts/getTsupConfig.js ================================================ // @ts-check import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' /** * @param {Object} opts - Options for building configurations. * @param {string[]} opts.entry - The entry array. * @returns {import('tsup').Options} */ export function modernConfig(opts) { return { entry: opts.entry, format: ['cjs', 'esm'], target: ['chrome91', 'firefox90', 'edge91', 'safari15', 'ios15', 'opera77'], outDir: 'build/modern', experimentalDts: true, sourcemap: true, clean: true, esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'js' })], } } /** * @param {Object} opts - Options for building configurations. * @param {string[]} opts.entry - The entry array. * @returns {import('tsup').Options} */ export function legacyConfig(opts) { return { entry: opts.entry, format: ['cjs', 'esm'], target: ['es2020', 'node16'], outDir: 'build/legacy', experimentalDts: true, sourcemap: true, clean: true, esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'js' })], } } ================================================ FILE: scripts/getViteAliases.js ================================================ // @ts-check import path from 'node:path' import ts from 'typescript' const tsconfig = ts.readConfigFile( path.resolve(__dirname, '..', 'tsconfig.json'), ts.sys.readFile, ).config export const dynamicAliases = Object.entries( tsconfig.compilerOptions.paths || {}, ).reduce((aliases, [key, [value]]) => { const aliasKey = key.replace('/*', '') aliases[aliasKey] = path.resolve(value.replace('/*', '')) return aliases }, /** @type {Record} */ ({})) ================================================ FILE: scripts/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["**/*"] } ================================================ FILE: scripts/verify-links.ts ================================================ import { existsSync, readFileSync, statSync } from 'node:fs' import { extname, resolve } from 'node:path' import { glob } from 'tinyglobby' // @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'. import markdownLinkExtractor from 'markdown-link-extractor' const errors: Array<{ file: string link: string resolvedPath: string reason: string }> = [] function isRelativeLink(link: string) { return ( !link.startsWith('/') && !link.startsWith('http://') && !link.startsWith('https://') && !link.startsWith('//') && !link.startsWith('#') && !link.startsWith('mailto:') ) } /** Remove any trailing .md */ function stripExtension(p: string): string { return p.replace(`${extname(p)}`, '') } function relativeLinkExists(link: string, file: string): boolean { // Remove hash if present const linkWithoutHash = link.split('#')[0] // If the link is empty after removing hash, it's not a file if (!linkWithoutHash) return false // Strip the file/link extensions const filePath = stripExtension(file) const linkPath = stripExtension(linkWithoutHash) // Resolve the path relative to the markdown file's directory // Nav up a level to simulate how links are resolved on the web let absPath = resolve(filePath, '..', linkPath) // Ensure the resolved path is within /docs const docsRoot = resolve('docs') if (!absPath.startsWith(docsRoot)) { errors.push({ link, file, resolvedPath: absPath, reason: 'Path outside /docs', }) return false } // Check if this is an example path const isExample = absPath.includes('/examples/') let exists = false if (isExample) { // Transform /docs/framework/{framework}/examples/ to /examples/{framework}/ absPath = absPath.replace( /\/docs\/framework\/([^/]+)\/examples\//, '/examples/$1/', ) // For examples, we want to check if the directory exists exists = existsSync(absPath) && statSync(absPath).isDirectory() } else { // For non-examples, we want to check if the .md file exists if (!absPath.endsWith('.md')) { absPath = `${absPath}.md` } exists = existsSync(absPath) } if (!exists) { errors.push({ link, file, resolvedPath: absPath, reason: 'Not found', }) } return exists } async function verifyMarkdownLinks() { // Find all markdown files in docs directory const markdownFiles = await glob('docs/**/*.md', { ignore: ['**/node_modules/**'], }) console.log(`Found ${markdownFiles.length} markdown files\n`) // Process each file for (const file of markdownFiles) { const content = readFileSync(file, 'utf-8') const links: Array = markdownLinkExtractor(content) const relativeLinks = links.filter((link: string) => { return isRelativeLink(link) }) if (relativeLinks.length > 0) { relativeLinks.forEach((link) => { relativeLinkExists(link, file) }) } } if (errors.length > 0) { console.log(`\n❌ Found ${errors.length} broken links:`) errors.forEach((err) => { console.log( `${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`, ) }) process.exit(1) } else { console.log('\n✅ No broken links found!') } } verifyMarkdownLinks().catch(console.error) ================================================ FILE: tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "checkJs": true, "composite": true, "customConditions": ["@tanstack/custom-condition"], "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "incremental": true, "isolatedModules": true, "lib": ["DOM", "DOM.Iterable", "ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "noEmit": false, "noImplicitReturns": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "ES2020", "types": ["node"] }, "include": ["*.config.*"] }