Last active
March 31, 2025 18:55
-
-
Save wolever/30a34443a9b2982615bbd2e84cd9529e to your computer and use it in GitHub Desktop.
Helper for TanStack's Query cache which will call `Symbol.dispose` on objects as they leave the query cache.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { QueryClient } from '@tanstack/react-query'; | |
export const SymbolDispose = Symbol('dispose'); | |
type Disposable = { [SymbolDispose]: () => void }; | |
/** | |
* Subscribe to ``client``'s query cache and dispose of any values that have a | |
* ``[Symbol.dispose]`` method. | |
* | |
* Notes: | |
* - The exported ``SymbolDispose`` can be used if ``Symbol.dispose`` is not | |
* available. | |
* - The ``createDisposingObjectUrl`` helper can be used to create a disposable | |
* object URL. | |
* | |
* For example:: | |
* | |
* const client = new QueryClient() | |
* queryClientDisposeOnCacheEvict(client) | |
* | |
* export const MyComponent = () => { | |
* const imageQuery = useQuery({ | |
* queryFn: () => { | |
* const blob = await fetch(...).then(r => r.blob()) | |
* // Note: see the ``createDisposingObjectUrl`` helper. | |
* const url = URL.createObjectURL(blob) | |
* return { url, [Symbol.dispose]: () => URL.revokeObjectURL(url) } | |
* }, | |
* }) | |
* ... | |
* } | |
*/ | |
export const queryClientDisposeOnCacheEvict = (client: QueryClient) => { | |
const cache = client.getQueryCache(); | |
const needsDispose = new Map<string, Disposable>(); | |
const add = (queryHash: string, newValue: Disposable) => { | |
const existing = needsDispose.get(queryHash); | |
if (existing === newValue) { | |
return; | |
} | |
if (existing) { | |
remove(queryHash); | |
} | |
if (newValue?.[SymbolDispose]) { | |
// console.info('queryClientDisposeOnCacheEvict: tracking', queryHash); | |
needsDispose.set(queryHash, newValue); | |
} | |
}; | |
const remove = (queryHash: string) => { | |
const oldValue = needsDispose.get(queryHash); | |
if (oldValue?.[SymbolDispose]) { | |
// console.info('queryClientDisposeOnCacheEvict: disposing', queryHash); | |
needsDispose.delete(queryHash); | |
try { | |
oldValue[SymbolDispose](); | |
} catch (err) { | |
console.error('queryClientDisposeOnCacheEvict: error disposing', queryHash, err); | |
} | |
} | |
}; | |
return cache.subscribe((event) => { | |
if (event.type == 'added' || event.type == 'updated') { | |
const value = cache.get(event.query.queryHash)?.state.data; | |
add(event.query.queryHash, value as any); | |
} | |
if (event.type == 'removed') { | |
remove(event.query.queryHash); | |
} | |
}); | |
}; | |
export const createDisposingObjectUrl = (blob: Blob) => { | |
const url = URL.createObjectURL(blob); | |
return { | |
url, | |
[SymbolDispose]: () => URL.revokeObjectURL(url), | |
}; | |
}; |
With these changes ESLint complained less about this code, and was able to be helpful in flagging in areas of my code where I used createDisposingObjectUrl
but did not correctly destructure the url
property. Thank you for this helpful Gist.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I adjusted the typing slightly to more accurately reflect what I think our understanding of disposable is: that it may or may not have a SymbolDispose field, but if it does, the field is a function returning void. Does this seem like an improvement?