Token导航 LogoToken导航TokenDH.com
前端设计需要联网github未标认证来源可访问许可证需确认审计异常

better-modal更好的模态

Agent Skill

better-modal 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

341

周安装

8

GitHub Stars

3

下载量

65
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:better-modal(更好的模态)
来源仓库:https://github.com/buiducnhat/better-modal
仓库路径:skills/better-modal
安装命令:
npx skills add https://github.com/buiducnhat/better-modal --skill better-modal
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/buiducnhat/better-modal --skill better-modal

简介

基于 Zustand 的状态管理器,为 React 19+ 提供 Promise 驱动的模态框控制方案。

  • 支持从非 React 上下文(如事件处理器)中打开模态框并等待返回结果。
  • 集中管理所有模态状态,提升应用内模态交互的一致性和可测试性。
  • 使用前需安装 zustand 作为 peer dependency,并正确挂载 ModalContainer 组件。
  • better-modal 属于前端设计类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

better-modal Usage Guide

@buiducnhat/better-modal is a headless, Promise-based modal state manager for React 19+. It centralises all modal state in a Zustand store and lets you open modals imperatively from anywhere in your app — including outside React components — then await the result.


Installation

# npm
npm install @buiducnhat/better-modal zustand

# bun
bun add @buiducnhat/better-modal zustand

Peer dependencies that must be installed separately:

  • react ^19
  • zustand ^5

Quick-Start (3 Steps)

Step 1 — Place <ModalContainer /> once at the app root

// app/layout.tsx (Next.js) or main.tsx (Vite)
import { ModalContainer } from "@buiducnhat/better-modal";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        <ModalContainer /> {/* ← required, exactly once */}
      </body>
    </html>
  );
}

Step 2 — Define a modal with createModal

// modals/confirm-modal.tsx
import { createModal } from "@buiducnhat/better-modal";
import type { useModal } from "@buiducnhat/better-modal";

type Props = { title: string; message: string };

export const ConfirmModal = createModal<Props>(
  "confirm",
  ({ title, message, modal }) => (
    <dialog open={modal.visible}>
      <h2>{title}</h2>
      <p>{message}</p>
      <button onClick={() => modal.resolve(true)}>Confirm</button>
      <button onClick={() => modal.resolve(false)}>Cancel</button>
    </dialog>
  ),
);

Step 3 — Open the modal from anywhere

// Any component, server action boundary, or plain module
import { ConfirmModal } from "@/modals/confirm-modal";

async function handleDelete() {
  const confirmed = await ConfirmModal.show({
    title: "Delete item",
    message: "This action cannot be undone.",
  });
  if (confirmed) {
    await deleteItem();
  }
}

Public API

createModal(id, Component)ModalController<TProps, TResult>

Registers a modal component and returns its typed control object.

ReturnTypeDescription
idstringThe ID used to register the modal
show(props?: TProps) => Promise<TResult>Opens the modal, returns an awaitable Promise
hide() => voidCloses without resolving the Promise
const MyModal = createModal<MyProps, MyResult>("my-modal", MyComponent);

const result = await MyModal.show({
  /* props */
});
MyModal.hide();

Rules:

  • Call createModal at module level (outside components) — it registers once.
  • The id string must be globally unique across your app.
  • TProps types the props passed to show(). TResult types the resolved value.
  • If TResult is omitted, it defaults to unknown.

useModal(id) — Hook used inside a modal component

Subscribe to a modal's state from within its own component.

const modal = useModal("my-modal");
PropertyTypeDescription
visiblebooleanWhether the modal is currently open
propsunknownProps passed to show()
show(props?: unknown) => Promise<unknown>Imperatively open this modal
hide() => voidClose without resolving
resolve(value: unknown) => voidClose + resolve the awaiting Promise
reject(reason: unknown) => voidClose + reject the awaiting Promise
remove() => voidRemove modal from store entirely (cleanup)
Note: modal is already injected as a prop by ModalContainer / createModal — you only need to call useModal manually for more advanced use-cases.

<ModalContainer />

Renders all active modals. Place exactly once at the app root (layout level). Uses useShallow internally — only re-renders when the set of modal IDs changes.

<ModalContainer />

registerModal(id, Component) — Low-level API

Direct registration without the show/hide control object. Use only when createModal is not suitable (e.g., dynamic registration).

import { registerModal } from "@buiducnhat/better-modal";

registerModal("alert", AlertComponent);

Exported types — ModalController, ModalComponentProps

import type {
  ModalController,
  ModalComponentProps,
} from "@buiducnhat/better-modal";

ModalController<TProps, TResult> — the type of the object returned by createModal:

interface ModalController<TProps = Record<string, unknown>, TResult = unknown> {
  id: string;
  show: (props?: TProps) => Promise<TResult>;
  hide: () => void;
}

Use it to type a variable that holds a modal reference:

let modal: ModalController<{ name: string }, boolean>;
modal = createModal<{ name: string }, boolean>("name-modal", NameComponent);

ModalComponentProps<TProps, TResult> — helper type for explicitly typing a modal component's props:

type ModalComponentProps<TProps, TResult> = TProps & {
  modal: { visible: boolean; props: TProps; show: ...; hide: ...; resolve: (val: TResult) => void; ... };
};
import type { ModalComponentProps } from "@buiducnhat/better-modal";

type MyProps = { title: string };
type MyResult = boolean;

function MyModalComponent({
  title,
  modal,
}: ModalComponentProps<MyProps, MyResult>) {
  return (
    <dialog open={modal.visible}>
      <h2>{title}</h2>
      <button onClick={() => modal.resolve(true)}>OK</button>
    </dialog>
  );
}

TypeScript Best Practices

Type props and result separately

type ConfirmProps = { title: string };
type ConfirmResult = boolean;

export const ConfirmModal = createModal<ConfirmProps, ConfirmResult>(
  "confirm",
  ({ title, modal }) => {
    const confirm = () => modal.resolve(true satisfies ConfirmResult);
    const cancel = () => modal.resolve(false satisfies ConfirmResult);

    return (
      <dialog open={modal.visible}>
        <p>{title}</p>
        <button onClick={confirm}>Yes</button>
        <button onClick={cancel}>No</button>
      </dialog>
    );
  },
);

// Caller gets: Promise<ConfirmResult>
const ok = await ConfirmModal.show({ title: "Continue?" });

Keep modal files co-located with their feature

features/
  orders/
    delete-order-modal.tsx   ← modal definition
    orders-list.tsx          ← caller

React / Vercel Performance Best Practices

1. Slice subscriptions — never subscribe to the whole store

useModal already subscribes to state.modals[id] only. Do not bypass this by calling useModalStore directly and reading the full modals object.

// ✅ Correct — slice subscription (built into useModal)
const modal = useModal("confirm");

// ❌ Wrong — subscribes to entire store, causes all modals to re-render together
const store = useModalStore();

2. Stable references with useCallback / useMemo

All action functions returned by useModal are already memoised with useCallback. When you pass them as event handlers, no extra wrapping is needed.

// ✅ Correct — modal.resolve is already stable
<button onClick={() => modal.resolve(true)}>Confirm</button>;

// ❌ Unnecessary — wrapping a stable ref in another useCallback adds noise
const handleConfirm = useCallback(() => modal.resolve(true), [modal.resolve]);

3. Avoid mounting modals unconditionally in JSX trees

ModalContainer renders only modals that have been show()-called. Do not place modal components directly in your JSX tree — it defeats the library's purpose and forces unnecessary renders.

// ✅ Correct — driven by ModalContainer at the root
export const ConfirmModal = createModal("confirm", ConfirmComponent);

// ❌ Anti-pattern — manual conditional render in component tree
function Page() {
  const [open, setOpen] = useState(false);
  return <>{open && <ConfirmDialog />}</>;
}

4. Register modals at module level, not inside components

createModal is a registration side-effect. Call it once per modal at module scope.

// ✅ Module level
export const AlertModal = createModal("alert", AlertComponent);

// ❌ Inside a component — re-registers on every render
function App() {
  const AlertModal = createModal("alert", AlertComponent); // BUG
}

5. Use await for confirmation flows, not prop-drilling callbacks

// ✅ Promise-based — no callback prop-drilling required
async function handleDelete(id: string) {
  const ok = await ConfirmModal.show({ message: "Delete this item?" });
  if (!ok) return;
  await deleteItem(id);
}

// ❌ Old pattern — callback hell, tight coupling
function Parent() {
  const [showModal, setShowModal] = useState(false);
  const handleConfirm = () => {
    deleteItem(id);
    setShowModal(false);
  };
  return <ConfirmModal open={showModal} onConfirm={handleConfirm} />;
}

6. Handle Promise rejection — always catch or use resolve(false) for cancellation

// ✅ Resolve-based cancellation (preferred)
<button onClick={() => modal.resolve(false)}>Cancel</button>;

// ✅ If you use reject(), always handle it at the call site
try {
  const result = await DeleteModal.show();
} catch {
  // user cancelled
}

// ❌ Unhandled rejection — will throw an unhandled Promise error
<button onClick={() => modal.reject("cancelled")}>Cancel</button>;
// ...without try/catch at the call site
Note: The library also rejects automatically in two edge cases: - Reopen before resolve — if show() is called while the modal is already open, the previous Promise is rejected with Error("Modal '...' was reopened before resolving"). - remove() before resolve — if modal.remove() is called on an unresolved modal, the Promise is rejected with Error("Modal '...' was removed before resolving"). Always handle rejections at the show() call site.

7. Clean up with modal.remove() for modals with heavy side-effects

hide() sets isOpen: false but keeps the state in the store. For modals with large props (e.g., image data) call remove() after close to free memory.

const MyModal = createModal("heavy", ({ data, modal }) => {
  const close = () => {
    modal.resolve(null);
    // modal.remove() is called automatically by resolve → hide,
    // but you can call remove() explicitly after animations complete
  };
  return <dialog open={modal.visible}>...</dialog>;
});

8. Vercel composition pattern — use children over render-props inside modals

When your modal component needs to accept arbitrary content, prefer children over render-prop patterns.

// ✅ children prop — clear, composable
type Props = { title: string; children: React.ReactNode };
const DialogModal = createModal<Props>(
  "dialog",
  ({ title, children, modal }) => (
    <dialog open={modal.visible}>
      <h2>{title}</h2>
      <div>{children}</div>
      <button onClick={() => modal.hide()}>Close</button>
    </dialog>
  ),
);

// Call site
DialogModal.show({ title: "Info", children: <p>Some content</p> });

9. Co-locate modal definitions with the feature that owns them

Avoid a single modals/ directory that becomes a dump of every modal in the app. Instead, keep modals adjacent to the feature components that use them.

features/
  payments/
    payment-error-modal.tsx
    payment-form.tsx
  settings/
    delete-account-modal.tsx
    account-settings.tsx

10. Avoid prop-spreading ...modal.props — be explicit

// ✅ Explicit props — type-safe and readable
const { title, message } = modal.props;
return (
  <dialog>
    <h2>{title}</h2>
    <p>{message}</p>
  </dialog>
);

// ❌ Spread loses type information
return <dialog {...modal.props} />;

Common Patterns

Confirmation dialog

// modals/confirm-modal.tsx
type ConfirmProps = { title: string; description?: string };

export const ConfirmModal = createModal<ConfirmProps>(
  "global-confirm",
  ({ title, description, modal }) => (
    <dialog open={modal.visible}>
      <h2>{title}</h2>
      {description && <p>{description}</p>}
      <footer>
        <button onClick={() => modal.resolve(true)}>Confirm</button>
        <button onClick={() => modal.resolve(false)}>Cancel</button>
      </footer>
    </dialog>
  ),
);

// Usage
const confirmed = await ConfirmModal.show({
  title: "Delete?",
  description: "Cannot undo.",
});

Form modal with typed result

// modals/edit-name-modal.tsx
type Props = { currentName: string };
type Result = { name: string } | null;

export const EditNameModal = createModal<Props>(
  "edit-name",
  ({ currentName, modal }) => {
    const [name, setName] = React.useState(currentName);

    return (
      <dialog open={modal.visible}>
        <input value={name} onChange={(e) => setName(e.target.value)} />
        <button onClick={() => modal.resolve({ name } satisfies Result)}>
          Save
        </button>
        <button onClick={() => modal.resolve(null)}>Cancel</button>
      </dialog>
    );
  },
);

// Usage
const result = await EditNameModal.show({ currentName: "Alice" });
if (result) {
  await updateName(result.name);
}

Alert / notification (fire-and-forget)

export const AlertModal = createModal<{ message: string }>(
  "alert",
  ({ message, modal }) => (
    <dialog open={modal.visible}>
      <p>{message}</p>
      <button onClick={modal.hide}>OK</button>
    </dialog>
  ),
);

// Fire-and-forget — no await needed
AlertModal.show({ message: "Saved successfully!" });

Using with shadcn/ui <Dialog>

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { createModal } from "@buiducnhat/better-modal";

type Props = { title: string };

export const InfoModal = createModal<Props>("info", ({ title, modal }) => (
  <Dialog open={modal.visible} onOpenChange={(open) => !open && modal.hide()}>
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{title}</DialogTitle>
      </DialogHeader>
      <button onClick={modal.hide}>Close</button>
    </DialogContent>
  </Dialog>
));

Using with Radix UI <Dialog>

import * as Dialog from "@radix-ui/react-dialog";
import { createModal } from "@buiducnhat/better-modal";

export const ConfirmModal = createModal<{ message: string }>(
  "radix-confirm",
  ({ message, modal }) => (
    <Dialog.Root
      open={modal.visible}
      onOpenChange={(open) => !open && modal.resolve(false)}
    >
      <Dialog.Portal>
        <Dialog.Overlay />
        <Dialog.Content>
          <p>{message}</p>
          <button onClick={() => modal.resolve(true)}>Yes</button>
          <button onClick={() => modal.resolve(false)}>No</button>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  ),
);

Anti-Patterns Checklist

Anti-patternWhy it's wrongFix
createModal inside a componentRe-registers on every renderMove to module scope
<ModalContainer /> placed more than onceRenders every modal twicePlace exactly once at root
Subscribing to full useModalStore()Triggers re-renders for all modal state changesUse useModal(id) or slice selectors
modal.reject() without try/catch at call siteUnhandled Promise rejectionUse resolve(false) for cancellation, or wrap in try/catch
Spreading modal.props onto JSXLoses TypeScript type narrowingDestructure explicitly
Storing modal IDs as magic strings across filesTypos cause silent failuresExport the ID from the modal definition file

Data Flow Reference

Module scope
  createModal(id, Comp)
    └─► modalRegistry.set(id, Comp)   (plain Map, not Zustand)
    └─► returns { id, show, hide }

Runtime
  Modal.show(props)
    └─► useModalStore.open(id, props) → stores Promise resolvers
    └─► returns Promise<result>

  <ModalContainer /> (root)
    └─► subscribes to Object.keys(store.modals)  [useShallow]
    └─► for each id → <ModalRenderer id={id} />
          └─► Comp = modalRegistry.get(id)
          └─► modal = useModal(id)                [slice subscription]
          └─► <Comp {...modal.props} modal={modal} />

  modal.resolve(value)
    └─► calls stored Promise resolver
    └─► sets isOpen = false
    └─► Promise resolves in original caller

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.69%
按下载量换算23

Claude

28.92%
按下载量换算19

Cursor

18.88%
按下载量换算12

Gemini CLI

8.35%
按下载量换算5

安全审计

Gen Agent Trust Hub

通过

Socket

未通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills