mcpField
W
writing-kea-logics

writing-kea-logics

github.com/posthog开发40.5k137 次安装今天更新
代码仓库

关于 writing-kea-logics

为 PostHog 前端开发者提供的 kea 状态逻辑编写规范,涵盖状态选择、React 组件连接和类型生成,帮助按项目约定组织业务逻辑。

主要功能

  1. 01kea 逻辑编写约定
  2. 02状态容器选择指导
  3. 03React 组件连接模式
  4. 04内联类型生成流程
  5. 05常见模式参考索引

适用场景

  1. 01创建或修改 kea 逻辑文件
  2. 02评审使用 React hooks 的代码
  3. 03为单个逻辑更新类型
  4. 04为状态变化选择合适构建器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/posthog/posthog --skill writing-kea-logics

安装后在 Claude Code 中输入/writing-kea-logics确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
connecting-logics.md
forms.md
keyed-logics.md
loading-data.md
persisting-state.md
polling.md
reacting-to-changes.md
routing.md
state-decision.md
testing.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 会更符合习惯的代码

配套技能(请勿重复)

如果你的工作与之重叠,请先阅读配套技能。

核心原则

  1. 业务逻辑存在于 logic 中,而不是组件中。 CLAUDE.md 明确指出:“如果存在 kea logic 文件,就把所有 业务逻辑写在那里,不惜一切代价避免使用 React hooks。”Hooks 用于视图层面的关注点。

  2. 一个概念,一个事实来源。 从以下三者中恰好选择一个:由 action 驱动的 reducer、 派生 selector、异步 loader。不要将同一个值镜像到多个地方。

  3. 优先使用 listeners 而非 kea-subscriptions。 Subscriptions 会安装一个 redux 订阅,它在每次 dispatch 时都会重新运行,并且明显更慢。改为监听改变了该值的 action。参见 references/reacting-to-changes.md。

  4. 生成的类型就是契约。 每个 logic 在其 kea() 调用上方都有一个内联生成的 MakeLogicType 块。请从 logic 源文件导入 logic 类型,绝不要从单独的 *LogicType.ts 文件导入。

  5. 需要清理的资源通过 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 之前,先按以下 流程走一遍:

  1. 它来自 HTTP 调用吗? 使用 loader。
  2. 它可以由其他状态计算得出吗? 使用 selector。
  3. 有 action 会改变它吗?当它改变时 UI 需要重新渲染吗? 使用 reducer。
  4. 它是定时器、监听器或其他可释放资源吗? 使用 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 名称——其中有数百个可用示例,而且约定是稳定的。
  • 对于状态管理决策,优先选择能让你在其他地方删除代码的选项。

评论

支持 Markdown