Metabase Embedded Analytics SDK 的 useAction Hook 完整指南:触发预置 Action、类型化结果与错误处理
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
useAction是 Metabase 嵌入式分析 SDK(Embedded Analytics SDK)中用于触发 Metabase 中已预置(pre-existing)Action的 React Hook。它把"执行 Action、拿到结构化结果、捕获执行错误"这一整套流程封装进一个 hook 状态机,让宿主应用可以在自己的按钮、表单和事件处理器中安全地调用 Metabase 的写操作(Create / Update / Delete / Bulk / SQL)。阅读本文后,你将掌握useAction的完整签名、五个ActionKind与判别联合结果类型、ActionExecuteError错误模型,以及"从事件处理器手动触发 execute"的正确使用范式。
一、useAction 是什么
在 Metabase 中,Action 是运行在模型(Model)之上的可写操作,包括隐式(basic / implicit)动作 Create、Update、Delete,以及自定义 SQL 动作与批量(bulk)动作。useAction让嵌入式宿主应用(即使用 SDK 的外部 React 应用)直接触发这些已存在的 Action:
function useAction<TParameters, TKind>( actionId: SdkActionId | null, ): UseActionResult<TParameters, TKind>;- 第一个参数
actionId是 Action 的数字 id,或者是它的entity_id字符串; - 泛型
TParameters用于类型化execute的参数对象; - 可选泛型
TKind用于类型化判别联合(discriminated union)形态的result。
SDK 文档对其语义的官方描述是:"Triggers a pre-existing Metabase action"——即该 hook只负责触发,不负责创建 Action。Action 本身需要在 Metabase 后台(模型详情的 Actions 选项卡)中预先配置好,包括每个参数的类型、必填与否与默认值。
二、函数签名与类型参数
完整签名如下(默认类型参数在 TypeScript 中可省略):
function useAction< TParameters extends Record<string, unknown> = Record<string, unknown>, TKind extends ActionKind | undefined = undefined, >( actionId: SdkActionId | null, ): UseActionResult<TParameters, TKind>;类型参数
| Type Parameter | 约束 | 说明 |
|---|---|---|
TParameters | Record<string, unknown> | 描述execute调用时传入的参数对象,例如{ name: string; email: string } |
TKind | ActionKind|undefined | 声明该 Action 的种类,用于把result收窄为对应的判别联合结果形状;省略时回退到AnyActionResult联合 |
TKind的可选值为五种字符串字面量,定义在ActionKind:
type ActionKind = "create" | "update" | "delete" | "bulk" | "sql";它是一层"扁平的公开种类联合":create/update/delete恒指单行操作,bulk覆盖任意批量变体,sql指自定义 SQL Action。SDK 文档明确指出,这五种值映射到后端命名空间的row/*与bulk/*implicitKind,以及query的type值——create/update/delete对应后端的row/create、row/update、row/delete隐式种类,sql对应后端的query类型 Action。
参数
| Parameter | Type | 说明 |
|---|---|---|
actionId | SdkActionId|null | 目标 Action 的标识符;传null表示"暂不绑定任何 Action" |
SdkActionId定义如下:
type SdkActionId = number | SdkEntityId;即它既可以是 Action 的数字主键 id,也可以是 Metabase 全局唯一的entity_id字符串。数字 id 适合在已知 Action 时直接硬编码或从接口读取;entity_id字符串则适合跨环境(开发 / 生产 / 多实例)稳定引用同一个 Action。
三、返回值:UseActionResult
useAction返回UseActionResult对象,它是一个完整的执行状态机,包含五个成员:
| Property | Type | 说明 |
|---|---|---|
error | ActionExecuteError|null | 最后一次抛出的错误,已归一化为公开的ActionExecuteError形状;没有错误时为null |
execute | (parameters:TParameters) =>Promise<ActionResultForKind<TKind> \| null> | 用给定参数触发 Action。成功时返回响应体,失败时同时抛错并写入error,供渲染期消费者读取 |
isExecuting | boolean | 当前是否正在执行中,可用于按钮 loading 态 |
reset | () =>void | 清空result与error |
result | ActionResultForKind<TKind> |null | 最后一次的响应;首次调用前以及调用reset()之后为null |
execute 的两个关键行为
- 成功与失败双通道:
execute成功时 resolve 出响应体;失败时throw,并且同一个错误会同步存入error状态。这意味着你可以选择两种消费方式——在事件处理器里try/catch做流程控制,或在渲染层读取error?.data?.message展示错误信息,二者拿到的都是同一个归一化错误对象。 - 空值短路:当
actionId为null,或 SDK 尚未初始化完成时,execute不会发起任何网络请求,而是直接 resolve 为null。SDK 文档建议:如果宿主应用中这些情况可达,调用方应在事件处理器里先用if (!actionId) return;自行防护,避免"点了按钮却没反应"的困惑。
四、不传 TKind:AnyActionResult 与类型收窄
如果不提供第二个泛型TKind(即保持默认undefined),result的类型会是AnyActionResult——所有可能响应体的联合:
type AnyActionResult = | ActionResultForCreate | ActionResultForUpdate | ActionResultForDelete | ActionResultForBulk | ActionResultForSql;SDK 文档特别强调:当作者在编写时并不知道 Action 的种类,使用默认联合类型依然能得到TS 可收窄的形状——通过"<key>" in result进行类型守卫(type guard),而不是从宽松的Record<string, unknown>强转。后者会吞掉错误的字段读取(mispelled reads 在编译期无法暴露),前者则能让 TypeScript 在in判断之后自动推导出具体的结果形状。
const { result } = useAction<{ id: number }>(actionId); // result 是 AnyActionResult | null if (result && "created-row" in result) { // 此处 TS 自动收窄为 ActionResultForCreate const inserted = result["created-row"]; } else if (result && "rows-affected" in result) { // 此处 TS 自动收窄为 ActionResultForSql console.log(result["rows-affected"]); }五、五种判别联合结果形状
TKind通过ActionResultForKind这个条件类型把result收窄到唯一确定的结果形状:
type ActionResultForKind<TKind> = TKind extends "create" ? ActionResultForCreate : TKind extends "update" ? ActionResultForUpdate : TKind extends "delete" ? ActionResultForDelete : TKind extends "bulk" ? ActionResultForBulk : TKind extends "sql" ? ActionResultForSql : AnyActionResult;各结果形状分别定义如下(均来自 SDK API 文档):
create —— 单行插入
ActionResultForCreate返回被插入的那一行:
type ActionResultForCreate = { created-row: Record<string, RowValue>; };update —— 单行更新
ActionResultForUpdate返回受影响行的主键:
type ActionResultForUpdate = { rows-updated: readonly RowValue[]; };delete —— 单行删除
ActionResultForDelete返回受影响行的主键:
type ActionResultForDelete = { rows-deleted: readonly RowValue[]; };bulk —— 批量变体
ActionResultForBulk返回一个成功标志加可选的行数统计:
type ActionResultForBulk = { rows-created?: number; rows-deleted?: number; rows-updated?: number; success: boolean; };sql —— 自定义 SQL Action
ActionResultForSql返回受影响行数:
type ActionResultForSql = { rows-affected: number; };其中RowValue是 Metabase 查询结果与 Action 响应中单个值的类型:
type RowValue = string | number | null | boolean | object;六、错误模型:ActionExecuteError
execute失败时(非 2xx 响应)抛出的错误会被归一化并捕获进 hook 的error状态,其形状定义在ActionExecuteError:
type ActionExecuteError = { data: { errors?: Record<string, string>; message?: string; }; isCancelled: boolean; status?: number; };| Property | Type | 说明 |
|---|---|---|
data.message? | string | 对终端用户可操作的诊断信息(actionable diagnostic) |
data.errors? | Record<string, string> | 后端报告参数级校验失败时的逐字段映射({ <slug>: <message> }) |
isCancelled | boolean | 是否被取消 |
status? | number | HTTP 状态码;传输层失败(离线、请求被中止)未收到 HTTP 响应时该字段不存在 |
hook 把error的类型声明为ActionExecuteError | null,因此消费者可以直接读取字段,无需任何类型断言:
const message = error?.data?.message;关于errors与message的关系,SDK 文档给出了关键区分:参数级校验失败时,error.data.errors是逐字段的映射;整体请求失败(例如外键约束违反)时,则是{ message: "...", errors: {} }这样的形态——即只有全局message而没有逐字段错误。这在表单场景中非常实用:你可以把errors渲染到对应字段下方,把message渲染为表单顶部的整体错误提示。
七、与查询 hooks 的本质区别:手动触发 execute
useAction与 SDK 中的查询类 hooks(如useMetabot、查询结果类 hooks)有一个关键差异:
Unlike the query hooks, this does NOT run on mount——
useAction不会在组件挂载时自动执行。
它完全由调用方从事件处理器(如按钮onClick、表单onSubmit)中显式调用execute来触发。这样设计是合理的:Action 是写操作,绝不能在挂载时无意识地执行(否则每次打开页面都会插入 / 更新 / 删除数据)。
由此也引出一个重要的使用范式——条件执行的判断放在事件处理器中,而不是依赖 hook 内部的状态:
function handleClick() { if (!user.canEdit) return; // 权限门控 if (!actionId) return; // Action 未就绪 execute({ name, email }); }SDK 文档给出的示例即是如此(if (!user.canEdit) return;之后再调用execute)。
八、完整实战示例:类型化创建用户
综合以上全部知识,一个完整的"表单提交触发 create 类型 Action"的宿主端组件可以这样写:
import { useAction } from "@metabase/embedding-sdk-react"; type CreateUserParams = { name: string; email: string }; function CreateUserForm({ actionId }: { actionId: number | null }) { const { execute, isExecuting, error, result, reset } = useAction< CreateUserParams, "create" >(actionId); const handleSubmit = async (params: CreateUserParams) => { if (!actionId) return; // actionId 为空则不发起请求 if (!params.email.includes("@")) return; try { const res = await execute(params); // 因为 TKind = "create",res 被收窄为 // ActionResultForCreate | null if (res) { console.log("inserted row:", res["created-row"]); } } catch (e) { // 失败时 execute 抛错;同一错误也已在 error 状态中 console.error(e); } }; return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name, email }); }}> {/* 字段编辑区 */} <button type="submit" disabled={isExecuting || !actionId}> {isExecuting ? "Saving…" : "Create"} </button> {/* 渲染期读取归一化错误 */} {error && <p role="alert">{error.data?.message}</p>} {result && <p>Created row: {JSON.stringify(result["created-row"])}</p>} <button type="button" onClick={reset}>Reset</button> </form> ); }要点回顾:
TParameters用CreateUserParams声明,execute(params)的参数因此获得完整类型检查;TKind = "create"让result与execute的 resolve 值被收窄为ActionResultForCreate,可直接读取created-row;isExecuting驱动按钮 loading 态,避免重复提交;- 渲染层直接读
error.data?.message,无需类型断言; reset()用于表单重新打开或提交成功后清空上次的result与error。
九、后端执行链路印证
SDK 侧的类型与语义设计,与后端 Action 执行实现是严格对应的。在 src/metabase/actions/execution.clj 中:
execute-custom-action!按action-type分发::query类型走execute-query-action!执行写查询,:http类型走http-action/execute-http-action!。这印证了ActionKind中"sql"(后端query类型)与其它种类的划分依据。execute-query-action!会把请求参数按参数 id 装配进查询的:parameters,再通过qp/execute-write-query!在写连接上执行——这正是execute(parameters)中参数对象被发送到后端并被逐个匹配到 Action 参数槽的底层链路。- 隐式 Action 的种类映射定义在 legacy->current:
:row/create、:row/update、:row/delete对应到:model.row/create、:model.row/update、:model.row/delete——这正是ActionKind文档中"create/update/delete对应后端命名空间row/*implicitKind"这一说法的代码出处。 - 后端还实现了
check-no-extra-parameters(src/metabase/actions/execution.clj#L98-L111):当请求参数中存在没有对应目标参数槽的键时,返回 400 并携带:type :invalid-parameter与:parameters——对应前端ActionExecuteError.data中参数相关错误的来源。
此外,关于隐式(basic)动作本身的创建与限制(只能基于"包装单一原始表"的模型、只支持 Create/Update/Delete 三种、不可归档只能开关),可进一步参阅 docs/actions/basic.md 与 docs/actions/introduction.md;自定义 SQL Action 的编写方式见 docs/actions/custom.md。
十、相关 API 速查
| API | 类型 | 说明 |
|---|---|---|
useAction | Hook | 触发已存在的 Metabase Action |
UseActionResult | 接口 | hook 返回值:error/execute/isExecuting/reset/result |
SdkActionId | 类型别名 | number \| SdkEntityId,Action 的数字 id 或entity_id |
ActionKind | 类型别名 | "create" \| "update" \| "delete" \| "bulk" \| "sql" |
ActionResultForKind | 类型别名 | 按TKind条件映射到具体结果形状 |
AnyActionResult | 类型别名 | 所有结果形状的联合,TKind缺省时的result类型 |
ActionExecuteError | 类型别名 | 归一化执行错误:data.message、data.errors、status、isCancelled |
ActionResultForCreate | 类型别名 | { created-row },插入的行 |
ActionResultForUpdate | 类型别名 | { rows-updated },受影响主键 |
ActionResultForDelete | 类型别名 | { rows-deleted },受影响主键 |
ActionResultForBulk | 类型别名 | { success, rows-created?/deleted?/updated? } |
ActionResultForSql | 类型别名 | { rows-affected },受影响行数 |
RowValue | 类型别名 | string \| number \| null \| boolean \| object |
总结:useAction是嵌入式宿主应用与 Metabase 写能力之间的桥梁。用好它只需记住三条核心纪律:一是始终从事件处理器手动调用execute,绝不在挂载期自动执行写操作;二是尽量在调用时声明TKind以获得判别联合的完整类型收窄,无法预先确定时则用"<key>" in result做类型守卫;三是错误处理走error.data.message(全局)与error.data.errors(逐字段)双通道,配合isExecuting与reset()完成完整的执行状态管理。
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考