Use when adding timers (`setInterval`, `setTimeout`), event listeners (`window.addEventListener`, `document.addEventListener`, `MediaQueryList.addEventListener`), or any other resource that needs cleanup inside a kea logic. Every logic has `cache.disposables.add(setup, key?, options?)` and `cache.disposables.dispose(key)` available via the globally registered `disposablesPlugin` (`frontend/src/kea-disposables.ts`). Replaces the bare `cache.foo = setInterval(...)` + `beforeUnmount: clearInterval(cache.foo)` pattern and auto-pauses background work when the tab is hidden.
npx skills add https://github.com/PostHog/posthog --skill using-kea-disposables
Every kea logic in this repo has cache.disposables injected by the local disposablesPlugin (frontend/src/kea-disposables.ts, registered globally in frontend/src/initKea.ts). Reach for it whenever you create a resource that needs explicit teardown — the plugin runs cleanup on unmount and automatically pauses background work when the tab is hidden.
Do not add a beforeUnmount for cleanup. The plugin runs the cleanup function you return from setup automatically when the logic unmounts (and re-runs setup/cleanup around tab visibility changes). If you find yourself writing a beforeUnmount whose only job is to clearInterval / clearTimeout / removeEventListener something registered earlier in the same logic, register that resource through cache.disposables.add(...) instead and delete the beforeUnmount. Reserve beforeUnmount for teardown that _isn't_ a resource you control (e.g. flushing state, persisting to localStorage, calling a third-party dispose()).
setInterval or setTimeout inside afterMount, a listener, or a subscriptionwindow.addEventListener, document.addEventListener, or MediaQueryList.addEventListenercache.<thing> plus a matching beforeUnmount cleanup — convert itcache.disposables.add(
setup, // () => () => void — runs immediately; MUST return a cleanup function
key?, // string — re-adding with the same key disposes the previous one first
options?, // { pauseOnPageHidden?: boolean } — default true: cleanup runs on hide, setup re-runs on show
)
Canonical example (frontend/src/layout/navigation/noEventsBannerLogic.ts:14-21):
afterMount(({ actions, cache }) => {
cache.disposables.add(() => {
const pollTimer = window.setInterval(() => {
actions.loadCurrentTeam()
}, POLL_INTERVAL_MS)
return () => clearInterval(pollTimer)
})
}),
afterMount.cache.disposables.dispose(key) later to stop it earlypauseOnPageHiddenThe default (true) is correct for almost everything — polling, animation tickers, hover timers. Background tabs stop doing work and resume on focus, which dramatically reduces CPU and network cost.
Opt out ({ pauseOnPageHidden: false }) only when the listener must keep firing while the page is hidden:
storage (writes from another tab), online / offline, message (from web workers, service workers, or other windows)visibilitychange listener itself — the whole point is to observe hide/showNote: popstate cannot fire on a hidden tab (it's user-driven), so pausing on hide is fine — see the toolbar example below.
dispose() to stop earlycache.disposables.dispose('key') tears down one specific resource without unmounting the logic. Use it when a state transition should end the resource — pause/resume a poller, stop a hover-only ticker on mouseleave, close a modal-scoped listener.
Unnamed setInterval poller — see the canonical example in The pattern (frontend/src/layout/navigation/noEventsBannerLogic.ts:14-21).
Keyed intervals with dispose() on hover-end / pause — frontend/src/lib/components/LiveUserCount/liveUserCountLogic.ts:94-118
setIsHovering: ({ isHovering }) => {
if (isHovering) {
actions.setNow(new Date())
cache.disposables.add(() => {
const intervalId = setInterval(() => actions.setNow(new Date()), 500)
return () => clearInterval(intervalId)
}, 'nowInterval')
} else {
cache.disposables.dispose('nowInterval')
}
},
pauseStream: () => {
cache.disposables.dispose('statsInterval')
},
resumeStream: () => {
actions.pollStats()
cache.disposables.add(() => {
const intervalId = setInterval(() => actions.pollStats(), props.pollIntervalMs ?? 30000)
return () => clearInterval(intervalId)
}, 'statsInterval')
},
setTimeout with key for spam-replacement — frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts:1837-1846
showSeekIndicator: () => {
// Same key auto-disposes the previous timer when spamming
cache.disposables.add(() => {
const timerId = setTimeout(() => actions.hideSeekIndicator(), 600)
return () => clearTimeout(timerId)
}, 'seekIndicatorTimer')
},
Multiple keyed window listeners in one afterMount — frontend/src/toolbar/bar/toolbarLogic.ts:655-688
cache.disposables.add(() => {
const clickListener = (e: MouseEvent): void => {
/* ... */
}
window.addEventListener('mousedown', clickListener)
return () => window.removeEventListener('mousedown', clickListener)
}, 'clickListener')
// popstate only fires on user-initiated back/forward, so a hidden tab won't
// generate events — pausing on hide (the default) is fine here. Opt out
// only if you must observe popstates while the tab is in the background.
cache.disposables.add(() => {
const popstateHandler = (): void => actions.maybeSendNavigationMessage()
window.addEventListener('popstate', popstateHandler)
return () => window.removeEventListener('popstate', popstateHandler)
}, 'popstateListener')
visibilitychange listener with pauseOnPageHidden: false — frontend/src/scenes/product-tours/productTourLogic.ts:647-663
openToolbarModal: () => {
cache.disposables.add(
() => {
const handler = (): void => {
if (document.visibilityState === 'hidden') {
actions.handleToolbarTabVisibility()
}
}
document.addEventListener('visibilitychange', handler)
return () => document.removeEventListener('visibilitychange', handler)
},
'toolbarModalVisibility',
{ pauseOnPageHidden: false }
)
},
closeToolbarModal: () => {
cache.disposables.dispose('toolbarModalVisibility')
},
MediaQueryList listener in events(afterMount) — frontend/src/layout/navigation-3000/themeLogic.ts:108-118
events(({ cache, actions }) => ({
afterMount() {
cache.disposables.add(() => {
const prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)')
const onPrefersColorSchemeChange = (e: MediaQueryListEvent): void =>
actions.syncDarkModePreference(e.matches)
prefersColorSchemeMedia.addEventListener('change', onPrefersColorSchemeChange)
return () => prefersColorSchemeMedia.removeEventListener('change', onPrefersColorSchemeChange)
}, 'prefersColorSchemeListener')
},
})),
Bare cache.<thing> + beforeUnmount cleanup is the pattern this plugin replaces. Convert these on sight.
Before (frontend/src/lib/components/HedgehogMode/hedgehogModeLogic.ts:205-215):
afterMount(({ actions, cache }) => {
cache.syncInterval = setInterval(() => actions.syncFromState(), 1000)
}),
beforeUnmount(({ cache }) => {
if (cache.syncInterval) {
clearInterval(cache.syncInterval)
cache.syncInterval = null
}
}),
After — note the beforeUnmount block is gone entirely; the cleanup function returned from setup is what the plugin runs on unmount:
afterMount(({ actions, cache }) => {
cache.disposables.add(() => {
const id = setInterval(() => actions.syncFromState(), 1000)
return () => clearInterval(id)
}, 'syncInterval')
}),
Other open conversion targets:
frontend/src/scenes/welcome/welcomeDialogLogic.ts:325-345 — bare window.addEventListener('storage', ...) with cache.storageHandler stashed manuallyfrontend/src/scenes/inbox/inboxSceneLogic.ts:260-267 — bare setInterval cleared by hand on every state changeComprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks
Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.
Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation.
Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill.
Create and edit Obsidian Flavored Markdown with wikilinks, embeds, callouts, properties, and other Obsidian-specific syntax. Use when working with .md files in Obsidian, or when the user mentions wikilinks, callouts, frontmatter, tags, embeds, or Obsidian notes.
Take posthog/using-kea-disposables from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.