
Using Kea Disposables
FreeSimplify resource management in Kea logic.
Free · Opens the source repo
What Using Kea Disposables does
The Using Kea Disposables skill provides a streamlined approach to managing resources that require cleanup in Kea logic. It introduces the cache.disposables mechanism, which allows developers to easily register timers, event listeners, and other resources that need explicit teardown. By utilizing this skill, you can avoid the boilerplate of manually clearing intervals or removing event listeners in the beforeUnmount lifecycle method. Instead, the skill automatically handles cleanup when the logic unmounts or when the tab visibility changes, improving both code readability and performance.
This skill is particularly useful when adding resources like setInterval, setTimeout, or event listeners such as window.addEventListener. By using cache.disposables.add(setup, key?, options?), you can register these resources in a way that ensures they are properly disposed of without cluttering your logic with repetitive cleanup code. The skill also supports the option to pause background tasks when the tab is hidden, which can significantly reduce CPU and network usage.
Developers working with Kea will find this skill beneficial for maintaining cleaner code and avoiding common pitfalls associated with resource management. It encourages best practices by centralizing the management of lifecycle events and resource cleanup, making it easier to write and maintain complex logic. The skill is designed for those who want to enhance their Kea applications with more efficient resource handling, ensuring that background work is paused appropriately and resources are cleaned up as needed.
In summary, Using Kea Disposables is an essential skill for any developer working with Kea who wants to simplify resource management and improve the overall quality of their codebase.
When to use it
Use this skill when you need to manage timers, event listeners, or other resources that require cleanup in your Kea logic.
When not to use it
Avoid this skill if you do not use Kea or if your resource management needs are simple and do not require explicit teardown.
What you can build with it
Adding Timers
Use cache.disposables to manage polling intervals or timeouts without cluttering your logic.
Event Listener Management
Register event listeners with automatic cleanup to prevent memory leaks when components unmount.
Dynamic Resource Control
Easily dispose of resources based on state changes to optimize performance and resource usage.
How to install Using Kea Disposables
View source1. Install with the skills CLI
npx skills add posthog/posthog/using-kea-disposables --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by posthogUsing 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()).
Use this skill when
- Adding
setIntervalorsetTimeoutinsideafterMount, a listener, or a subscription - Adding
window.addEventListener,document.addEventListener, orMediaQueryList.addEventListener - Adding any subscription that needs explicit teardown (WebSocket, EventSource, ResizeObserver, IntersectionObserver, etc.)
- Reviewing or editing a logic with a bare
cache.<thing>plus a matchingbeforeUnmountcleanup — convert it - A state change should tear down a previously-registered timer or listener early
The pattern
cache.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)
})
}),
Choosing a key
- No key — fire-and-forget; cleaned up only on unmount. Fine for one-shot listeners registered in
afterMount. - Named key — needed when:
- You'll call
cache.disposables.dispose(key)later to stop it early - The same setup may be re-added and each call should replace the previous one (spam-replacement)
- You'll call
pauseOnPageHidden
The 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:
- Listeners for events that can genuinely fire while the tab is hidden — e.g.
storage(writes from another tab),online/offline,message(from web workers, service workers, or other windows) - A
visibilitychangelistener itself — the whole point is to observe hide/show - Anything the user expects to keep running while the tab is hidden
Note: popstate cannot fire on a hidden tab (it's user-driven), so pausing on hide is fine — see the toolbar example below.
Calling dispose() to stop early
cache.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.
Examples in the codebase
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')
},
})),
Anti-patterns to convert
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— barewindow.addEventListener('storage', ...)withcache.storageHandlerstashed manuallyproducts/signals/frontend/inbox/inboxSceneLogic.ts:260-267— baresetIntervalcleared by hand on every state change
Frequently asked questions about Using Kea Disposables
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
