关于 writing-kea-logics
为 PostHog 前端开发者提供的 kea 状态逻辑编写规范,涵盖状态选择、React 组件连接和类型生成,帮助按项目约定组织业务逻辑。
主要功能
- 01kea 逻辑编写约定
- 02状态容器选择指导
- 03React 组件连接模式
- 04内联类型生成流程
- 05常见模式参考索引
适用场景
- 01创建或修改 kea 逻辑文件
- 02评审使用 React hooks 的代码
- 03为单个逻辑更新类型
- 04为状态变化选择合适构建器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ posthog/ posthog --skill writing-kea-logics安装后在 Claude Code 中输入/writing-kea-logics确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdconnecting-logics.mdforms.mdkeyed-logics.mdloading-data.mdpersisting-state.mdpolling.mdreacting-to-changes.mdrouting.mdstate-decision.mdtesting.md详细介绍
编写 kea logic
PostHog 使用 kea 作为前端的状态容器。几乎所有非平凡的
业务逻辑都位于 *Logic.ts / *Logic.tsx 文件中,而不是 React 中。我们可能
使用着领先于 keajs.org 文档所涵盖版本的 kea 预发布版——如有疑问,请查看
pnpm-workspace.yaml 中锁定的版本。
本技能记录了 PostHog 在上游 kea 文档之外特有的约定。 如果对某个 builder 的签名有疑问,请查阅上游文档。如果对是否应该使用它有疑问, 请阅读本文。
在以下情况下使用本技能
- 创建新的
*Logic.ts/*Logic.tsx文件 - 向现有 logic 添加 builder(actions、reducers、selectors、listeners、loaders、forms)
- 为某一状态在
reducer、selector、cache和loader之间做选择 - 将 React 组件与 logic 连接起来
- 审查引入或修改 kea logic 的 PR
- 审查使用了 React hooks、而 logic 会更符合习惯的代码
配套技能(请勿重复)
- using-kea-disposables —
setInterval、addEventListener以及任何其他需要清理的资源。
如果你的工作与之重叠,请先阅读配套技能。
核心原则
-
业务逻辑存在于 logic 中,而不是组件中。 CLAUDE.md 明确指出:“如果存在 kea logic 文件,就把所有 业务逻辑写在那里,不惜一切代价避免使用 React hooks。”Hooks 用于视图层面的关注点。
-
一个概念,一个事实来源。 从以下三者中恰好选择一个:由 action 驱动的 reducer、 派生 selector、异步 loader。不要将同一个值镜像到多个地方。
-
优先使用 listeners 而非
kea-subscriptions。 Subscriptions 会安装一个 redux 订阅,它在每次 dispatch 时都会重新运行,并且明显更慢。改为监听改变了该值的 action。参见 references/reacting-to-changes.md。 -
生成的类型就是契约。 每个 logic 在其
kea()调用上方都有一个内联生成的MakeLogicType块。请从 logic 源文件导入 logic 类型,绝不要从单独的*LogicType.ts文件导入。 -
需要清理的资源通过
cache.disposables处理。 参见 using-kea-disposables 技能。
结构一览
import { MakeLogicType, actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import * as api from 'products/foo/frontend/generated/api'
export interface FooLogicProps {
fooId: string
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface fooLogicValues {
name: string
nameUpper: string
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface fooLogicActions {
setName: (name: string) => { name: string }
}
export type fooLogicType = MakeLogicType<fooLogicValues, fooLogicActions, FooLogicProps>
export const fooLogic = kea<fooLogicType>([
props({} as FooLogicProps),
key((props) => props.fooId),
path((key) => ['scenes', 'foo', 'fooLogic', key]),
connect(() => ({ values: [teamLogic, ['currentTeamId']] })),
actions({ setName: (name: string) => ({ name }) }),
loaders(({ props }) => ({
foo: [null as Foo | null, { loadFoo: async () => api.foosRetrieve(props.fooId) }],
})),
reducers({ name: ['', { setName: (_, { name }) => name }] }),
selectors({ nameUpper: [(s) => [s.name], (name): string => name.toUpperCase()] }),
listeners(({ actions }) => ({
loadFooSuccess: () => {
/* ... */
},
})),
afterMount(({ actions }) => {
actions.loadFoo()
}),
])
约定的块顺序:props → key → path → connect → actions → forms → loaders →
reducers → selectors → sharedListeners → listeners → subscriptions(少见)→
windowValues → urlToAction / actionToUrl → afterMount / propsChanged / beforeUnmount。
你几乎从来不需要用到所有这些——通常半打块就够了。挑选该 logic 实际使用的块, 其余的省略。
决策流程——在开始之前选择正确的容器
大多数 kea bug 都源于为某一状态选错了容器。在选择任何 builder 之前,先按以下 流程走一遍:
- 它来自 HTTP 调用吗? 使用 loader。
- 它可以由其他状态计算得出吗? 使用
selector。 - 有 action 会改变它吗?当它改变时 UI 需要重新渲染吗?
使用
reducer。 - 它是定时器、监听器或其他可释放资源吗? 使用
cache.disposables——参见 using-kea-disposables。
cache.foo 是用于 UI 从不读取的临时标志的逃生通道——最后才考虑它,
而不是一开始就用。完整分类参见 references/state-decision.md。
模式索引
每个参考文档都围绕一个待完成的任务,说明该模式形态、为何这是正确的形态,以及其中的取舍。参考文档中的文件引用是“当下真实存在的示例”——它们会过时,因此模式本身才是权威依据。
| 你想要…… | 阅读 |
|---|---|
| 在 reducer / selector / cache / loader 之间做决定 | references/state-decision.md |
| 从 API 加载数据 | references/loading-data.md |
| 轮询某个端点或按间隔刷新 | references/polling.md |
| 构建表单 | references/forms.md |
| 让状态与 URL 同步 | references/routing.md |
| 在多次重新加载之间持久化状态 | references/persisting-state.md |
| 对值的变化作出反应 | references/reacting-to-changes.md |
| 让同一逻辑拥有多个实例 | references/keyed-logics.md |
| 在多个逻辑或某个组件子树之间共享状态 | references/connecting-logics.md |
| 测试逻辑 | references/testing.md |
| 识别出应当一看到就转换的模式 | references/anti-patterns.md |
类型与 typegen
import { MakeLogicType, kea } from 'kea'
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface fooLogicValues {
name: string
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface fooLogicActions {
setName: (name: string) => { name: string }
}
export type fooLogicType = MakeLogicType<fooLogicValues, fooLogicActions>
export const fooLogic = kea<fooLogicType>([...])
内联类型块由 kea-typegen 3.8.3 生成。命令:
pnpm --filter=@posthog/frontend typegen:watch— 在编写逻辑时进入监听模式pnpm --filter=@posthog/frontend typegen:write— 一次性写入pnpm --filter=@posthog/frontend typegen:check— CI 一致性检查
针对单个逻辑进行迭代
对整个代码库运行完整的 typegen 很慢。当你只针对单个逻辑进行迭代时,将 typegen 限定到该文件:
# Regenerate the type for one logic
pnpm --filter=@posthog/frontend typegen:file frontend/src/scenes/foo/fooLogic.ts
在编写逻辑时使用这个循环。当前的 tsgo 不支持将项目配置与文件参数组合使用,因此在最后运行一次完整的 typegen:check 和 typescript:check,以确认没有其他东西被破坏。
不要创建或导入单独的 *LogicType.ts 文件。修改逻辑并重新运行 typegen,使内联生成块保持权威性。
对于带键逻辑,请显式标注导出:
export const fooLogic: LogicWrapper<fooLogicType> = kea<fooLogicType>([...])。
如有疑问
- 在发明新模式之前,先阅读上面的相关参考文档。
- 阅读上游 keajs.org 文档以了解 builder 签名。
- 在仓库中的
*Logic.ts里搜索 builder 名称——其中有数百个可用示例,而且约定是稳定的。 - 对于状态管理决策,优先选择能让你在其他地方删除代码的选项。