Agent SkillsUsefulSoftwareCo/executor › self-contained-modals

self-contained-modals

GitHub

指导构建自包含模态框,确保表单状态和异步工作(如OAuth、定时器)位于组件内部。关闭时通过卸载而非手动重置来清理状态,防止生命周期泄漏导致的卡顿Bug。

.claude/skills/self-contained-modals/SKILL.md UsefulSoftwareCo/executor

触发场景

编写或审查模态框/对话框代码 处理包含异步工作的模态框(如OAuth弹窗、计时器、订阅) 排查模态框关闭后状态未清理的问题

安装

npx skills add UsefulSoftwareCo/executor --skill self-contained-modals -g -y
更多选项

非标准路径

npx skills add https://github.com/UsefulSoftwareCo/executor/tree/main/.claude/skills/self-contained-modals -g -y

不安装直接使用

npx skills use UsefulSoftwareCo/executor@self-contained-modals

指定 Agent (Claude Code)

npx skills add UsefulSoftwareCo/executor --skill self-contained-modals -a claude-code -g -y

安装 repo 全部 skill

npx skills add UsefulSoftwareCo/executor --all -g -y

预览 repo 内 skill

npx skills add UsefulSoftwareCo/executor --list

SKILL.md

Frontmatter
{
    "name": "self-contained-modals",
    "description": "Build modals\/dialogs self-contained: form and in-flight state lives inside, closing unmounts it. Use when writing or reviewing a modal\/dialog, especially one that owns async work (OAuth popups, timers, subscriptions, AbortControllers). Catches the stuck-on-Connecting class of lifecycle-leak bugs."
}

Self-contained modals

A modal's form state and in-flight work live INSIDE the modal, and closing the modal UNMOUNTS that state so it is destroyed, not hand-reset. Only the open/route intent belongs to the parent (deep links, programmatic open, reconnect handoffs need it).

Why

A hand-written reset() has to enumerate every field, and it silently drifts out of sync with state owned by child hooks the parent can't see. Unmounting resets everything for free, including child-hook cleanup effects (cancelling a dangling server session, clearing a timer, aborting a fetch).

Concrete bug this prevents (executor, add-account-modal): the modal was mounted unconditionally, so useOAuthPopupFlow's busy survived close. reset() zeroed its own booleans but never called oauthPopup.cancel(). Abandon the OAuth popup, close, reopen, and oauthBusy = false || busy(true) = true, so the footer is wedged on "Connecting…" with Close disabled. Unmounting would have cleared busy AND run the hook's cleanup that cancels the server OAuth session.

How to apply

Default to genuine conditional unmount, state inside. When closed the component returns null, so React destroys all of it and runs every child hook's cleanup. This is the cleanest fix and the one to reach for first:

function Parent() {
  const [open, setOpen] = useState(false);
  return open ? <Modal onClose={() => setOpen(false)} /> : null;
}

A key bump (<Body key={openCount} />) is still a manual reset, just spelled as a remount. Prefer real unmount; only reach for keyed remount in the one case below.

That case: Radix Dialog (this repo's components/dialog.tsx) Content/Overlay use data-[state=closed]:animate-out exit animations, so unmounting the whole Dialog drops the close animation. If you must keep that animation, keep the Dialog + DialogContent shell mounted and remount only the state-bearing body per open:

function Parent() {
  const [open, setOpen] = useState(false);
  const [openCount, setOpenCount] = useState(0);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogContent>{open ? <ModalBody key={openCount} /> : null}</DialogContent>
    </Dialog>
  );
}

This only works when DialogContent is independent of body state. If the shell depends on the body (e.g. a width className driven by the body's current sub-view), the body must own DialogContent, so there is no stable shell to keep, and genuine unmount (losing the exit animation) is the right call. That is exactly the executor add-account-modal: it genuinely unmounts and accepts the lost animation rather than plumbing body state up to a shell.

Reviewing: flag these smells

  1. A hand-written reset() exists. Its presence means state outlives the modal; ask why the modal isn't just unmounted.
  2. An always-mounted dialog (rendered unconditionally with an open prop) that owns async/in-flight state: popups, timers, subscriptions, AbortControllers, server sessions.
  3. A busy/loading flag composed from a child hook (e.g. ccBusy || someHook.busy) where reset() clears only part of it.

版本历史

  • fd4fb02 当前 2026-07-05 10:55

同 Skill 集合

.agents/skills/stack/SKILL.md
.agents/skills/wrdn-effect-atom-optimistic/SKILL.md
.agents/skills/wrdn-effect-atom-reactivity-keys/SKILL.md
.agents/skills/wrdn-effect-promise-exit/SKILL.md
.agents/skills/wrdn-effect-raw-fetch-boundary/SKILL.md
.agents/skills/wrdn-effect-schema-boundaries/SKILL.md
.agents/skills/wrdn-effect-schema-inferred-types/SKILL.md
.agents/skills/wrdn-effect-typed-errors/SKILL.md
.agents/skills/wrdn-effect-value-inferred-types/SKILL.md
.agents/skills/wrdn-effect-vitest-tests/SKILL.md
.agents/skills/wrdn-package-boundaries/SKILL.md
.agents/skills/wrdn-typescript-type-safety/SKILL.md
.claude/skills/emulate/SKILL.md
.claude/skills/prod-telemetry/SKILL.md
.skills/cli-release/SKILL.md
.skills/effect-atom-optimistic-updates/SKILL.md
.skills/effect-http-testing/SKILL.md
.skills/effect-use-pattern/SKILL.md
.skills/graphite/SKILL.md
.skills/warden-security-review/SKILL.md

元信息

文件数
0
版本
8bc037d
Hash
8704e420
收录时间
2026-07-05 10:55

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-18 01:55
浙ICP备14020137号-1 $访客地图$