1- import { hashKey , type QueryKey , useQueryClient } from "@tanstack/react-query"
1+ import {
2+ hashKey ,
3+ type QueryClient ,
4+ type QueryKey ,
5+ useQueryClient ,
6+ } from "@tanstack/react-query"
27import { createContext , memo , useContext , useEffect , useState } from "react"
38import {
49 fromEvent ,
@@ -9,6 +14,7 @@ import {
914} from "rxjs"
1015
1116type CacheEntry = {
17+ queryKey : QueryKey
1218 query$ : Observable < unknown >
1319 signal : AbortSignal
1420 sub : Subscription | undefined
@@ -19,6 +25,8 @@ type CacheEntry = {
1925export class QueryClient$ {
2026 public readonly queryMap : Map < string , CacheEntry > = new Map ( )
2127
28+ constructor ( public readonly queryClient : QueryClient ) { }
29+
2230 getQuery ( queryHash : string ) {
2331 return this . queryMap . get ( queryHash )
2432 }
@@ -31,17 +39,12 @@ export class QueryClient$ {
3139 const queryHash = hashKey ( queryKey )
3240
3341 const sharedQuery$ = query$ . pipe (
34- /**
35- * abort signal is triggered on:
36- * - manual cancellation from user
37- * - unmounting the component
38- * @see https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation
39- */
4042 takeUntil ( fromEvent ( signal , "abort" ) ) ,
4143 share ( ) ,
4244 )
4345
4446 const cacheEntry : CacheEntry = {
47+ queryKey,
4548 query$ : sharedQuery$ ,
4649 signal,
4750 sub : undefined ,
@@ -60,7 +63,9 @@ export class QueryClient$ {
6063 }
6164 } ,
6265 complete : ( ) => {
63- this . deleteQuery ( queryHash )
66+ if ( this . queryMap . get ( queryHash ) === cacheEntry ) {
67+ this . deleteQuery ( queryHash )
68+ }
6469 } ,
6570 } )
6671
@@ -74,14 +79,29 @@ export class QueryClient$ {
7479
7580 if ( ! entry ) return
7681
82+ entry . isCompleted = true
83+
84+ this . queryMap . delete ( queryHash )
85+
7786 if ( entry . sub ) {
7887 entry . sub . unsubscribe ( )
7988 entry . sub = undefined
8089 }
8190
82- entry . isCompleted = true
83-
84- this . queryMap . delete ( queryHash )
91+ /**
92+ * Only cancel queries whose stream already emitted at least once.
93+ * Those are "live" streams stuck in the take(1) refetch loop and
94+ * need an explicit cancel so the pending queryFnAsync promise
95+ * settles. One-shot observables (used like promises) that haven't
96+ * resolved yet should keep react-query's default behavior: stay
97+ * pending and resolve naturally even after unmount.
98+ */
99+ if ( ! entry . signal . aborted && entry . lastData !== undefined ) {
100+ this . queryClient ?. cancelQueries ( {
101+ queryKey : entry . queryKey ,
102+ exact : true ,
103+ } )
104+ }
85105 }
86106
87107 destroy ( ) {
@@ -99,44 +119,42 @@ export const Context = createContext<QueryClient$ | undefined>(undefined)
99119 * (including via `queryClient.clear()`), matching `useQuery$` observable
100120 * entries are dropped.
101121 */
102- export const QueryClientProvider$ = memo (
103- ( {
104- children,
105- client : _client ,
106- } : {
107- children : React . ReactNode
108- client ?: QueryClient$
109- } ) => {
110- const [ client ] = useState ( ( ) => _client ?? new QueryClient$ ( ) )
111- const queryClient = useQueryClient ( )
112-
113- useEffect (
114- function subscribeToQueryCache ( ) {
115- return queryClient . getQueryCache ( ) . subscribe ( ( event ) => {
116- const activeObservers = event . query . getObserversCount ( )
117-
118- /**
119- * When observers unmount, we need to delete the query from the cache
120- * because if the query contains a stream, it will continue. Nothing stops it.
121- * However this is only valid when there is no more observers.
122- */
123- if ( event . type === "observerRemoved" && activeObservers === 0 ) {
124- client . deleteQuery ( event . query . queryHash )
125- }
126- } )
127- } ,
128- [ queryClient , client ] ,
129- )
130-
131- useEffect ( ( ) => {
132- return ( ) => {
133- client . destroy ( )
134- }
135- } , [ client ] )
122+ export const QueryClientProvider$ = memo ( function QueryClientProvider$ ( {
123+ children,
124+ client : _client ,
125+ } : {
126+ children : React . ReactNode
127+ client ?: QueryClient$
128+ } ) {
129+ const queryClient = useQueryClient ( )
130+ const [ client ] = useState ( ( ) => _client ?? new QueryClient$ ( queryClient ) )
131+
132+ useEffect (
133+ function subscribeToQueryCache ( ) {
134+ return queryClient . getQueryCache ( ) . subscribe ( ( event ) => {
135+ const activeObservers = event . query . getObserversCount ( )
136+
137+ /**
138+ * When all observers unmount we need to delete the query from the cache
139+ * because if the query contains a stream, it will continue. Nothing stops it.
140+ * However this is only valid when there is no more observers.
141+ */
142+ if ( event . type === "observerRemoved" && activeObservers === 0 ) {
143+ client . deleteQuery ( event . query . queryHash )
144+ }
145+ } )
146+ } ,
147+ [ queryClient , client ] ,
148+ )
149+
150+ useEffect ( ( ) => {
151+ return ( ) => {
152+ client . destroy ( )
153+ }
154+ } , [ client ] )
136155
137- return < Context . Provider value = { client } > { children } </ Context . Provider >
138- } ,
139- )
156+ return < Context . Provider value = { client } > { children } </ Context . Provider >
157+ } )
140158
141159export const useQueryClient$ = ( ) => {
142160 const client = useContext ( Context )
0 commit comments