npx skills add ...
npx skills add posthog/posthog --skill writing-kea-logics
Guide for writing or reviewing PostHog kea logic files (`*Logic.ts` / `*Logic.tsx`). Use when creating a new logic, adding actions/reducers/selectors/listeners/loaders/forms/router bindings, choosing between reducer vs selector vs cache, deciding between listeners and `kea-subscriptions`, wiring React with `useValues`/`useActions`/`BindLogic`, or onboarding to kea conventions. Read keajs.org for upstream API; this skill captures PostHog-specific conventions and idioms.
npx skills add posthog/posthog --skill writing-kea-logics
PostHog uses kea as the state container for the frontend. Almost
all non-trivial business logic lives in a *Logic.ts / *Logic.tsx file, not in
React. We may be on a kea pre-release ahead of the version the keajs.org docs
cover — when in doubt, check pnpm-workspace.yaml for the pinned version.
This skill captures the PostHog-specific conventions on top of the upstream kea docs. When in doubt about a builder's signature, go upstream. When in doubt about whether to use it, read here.
*Logic.ts / *Logic.tsx filereducer vs selector vs cache vs loader for a piece of statesetInterval, addEventListener,
and any other resource that needs cleanup.If your work overlaps it, read the companion skill first.
Business logic lives in a logic, not in a component. CLAUDE.md is explicit: "If there is a kea logic file, write all business logic there, avoid React hooks at all costs." Hooks are for view concerns.
One concept, one source of truth. Pick exactly one of: action-driven reducer, derived selector, async loader. Don't mirror the same value into multiple places.
Prefer listeners over kea-subscriptions. Subscriptions install a redux
subscription that re-runs on every dispatch and is measurably slower. Listen to the
action that changed the value instead. See
references/reacting-to-changes.md.
Generated types are the contract. Every logic has an inline generated
MakeLogicType block above its kea() call. Import a logic type from the logic
source file, never from a separate *LogicType.ts file.
Resources that need cleanup go through cache.disposables. See the
using-kea-disposables skill.
Conventional block order: props → key → path → connect → actions → forms → loaders →
reducers → selectors → sharedListeners → listeners → subscriptions (rare) →
windowValues → urlToAction / actionToUrl → afterMount / propsChanged / beforeUnmount.
You almost never need all of those — half a dozen blocks is typical. Pick the ones the logic actually uses and leave the rest out.
Most kea bugs come from picking the wrong container for a piece of state. Work through this before reaching for any builder:
selector.reducer.cache.disposables
— see using-kea-disposables.cache.foo is an escape hatch for transient flags the UI never reads — reach for it
last, not first. See references/state-decision.md
for the full breakdown.
Each reference covers one job-to-be-done with the pattern shape, why it's the right shape, and the trade-offs. File citations inside references are "examples in the wild today" — they age, so the pattern itself is the source of truth.
| You want to... | Read |
|---|---|
| Decide between reducer / selector / cache / loader | references/state-decision.md |
| Load data from the API | references/loading-data.md |
| Poll an endpoint or refresh on an interval | references/polling.md |
| Build a form | references/forms.md |
| Sync state with the URL | references/routing.md |
| Persist state across reloads | references/persisting-state.md |
| React to a value change | references/reacting-to-changes.md |
| Have multiple instances of one logic | references/keyed-logics.md |
| Share state across logics or a component subtree | references/connecting-logics.md |
| Test the logic | references/testing.md |
| Recognise a pattern you should convert on sight | references/anti-patterns.md |
Inline type blocks are produced by kea-typegen 3.8.3. Commands:
pnpm --filter=@posthog/frontend typegen:watch — watch mode while writing logicspnpm --filter=@posthog/frontend typegen:write — one-shot writepnpm --filter=@posthog/frontend typegen:check — CI parity checkFull typegen over the whole codebase is slow. When you're iterating on a single logic, scope typegen to that file:
Use this loop while writing the logic. The current tsgo does not support combining
the project config with a file argument, so run the full typegen:check and
typescript:check once at the end to confirm nothing else broke.
Do not create or import a separate *LogicType.ts file. Change the logic and re-run
typegen so the inline generated block stays authoritative.
For keyed logics, annotate the export explicitly:
export const fooLogic: LogicWrapper<fooLogicType> = kea<fooLogicType>([...]).
*Logic.ts — there are hundreds of working
examples and the conventions are stable.