跳到主要内容

useCommonImperativeHandle

import { useEffect, useRef, useCallback } from 'react';

export function useCommonImperativeHandle(ref, object) {
useEffect(() => {
if (ref) {
ref(object);
}
}, []);
}

众所周知,在 React 中我们可以通过 ref 配合 forwardRef 控制子组件,那么在 vue 中又该如何调用呢

useCommonImperativeHandle(commonRef, () => ({
handleSetVisibleUserAgreementPopup: (flag: boolean, cb?: any) => {
if (!flag) {
setIsChecked(false);
}

setVisibleUserAgreementPopup(flag);
cb && (popupConfirmCallbackRef.current = cb);
},
}));

useImperativeHandle(ref, () => ({
handleSetVisibleUserAgreementPopup: (flag: boolean, cb?: any) => {
if (!flag) {
setIsChecked(false);
}

setVisibleUserAgreementPopup(flag);
cb && (popupConfirmCallbackRef.current = cb);
},
}));
rule(options: any) {
const _dom: any = document.getElementById(options.appendDomId);
const root: ReactDOM.Root = ReactDOM.createRoot(_dom);
const key = Symbol();

root.render(
<Rule
commonRef={(ref: any) => {
this.maps.set(key, {root, ref});
}}
ref={options.ref}
value={options.isChecked}
onChange={(v: any) => {
options.onChange(v);
}}
text={options.text}
customLink={options.customLink}
className={options.className}
page={options.page}
/>,
);
return key;
}
this.op = window.SoybeanCommonMain.login.rule({
appendDomId: 'ruleDom',
onChange(v) {
self.hasChecked = v;
},
isChecked: false,

text: '阅读并同意',
});
if (!this.hasChecked) {
let ob = window.SoybeanCommonMain.login.maps.get(this.op);
ob.ref().handleSetVisibleUserAgreementPopup(true, () => {
this.handleCheckCode();
});
return;
}

补充说明

1. 这个 Hook 到底做了什么

useCommonImperativeHandle(ref, object) 可以理解为「回调版」的 useImperativeHandle

标准 useImperativeHandleuseCommonImperativeHandle
接收的 ref一个 ref 对象ref.current一个 回调函数 ref(...)
交出句柄的方式ref.current = factory()ref(factory)(把工厂函数交给回调)
触发时机每次渲染后(受依赖数组控制)useEffect 空依赖,仅首次挂载后执行一次
export function useCommonImperativeHandle(ref, object) {
useEffect(() => {
if (ref) {
ref(object); // 关键:把「工厂函数」交给回调,而不是写 ref.current
}
}, []);
}

注意一个细节:这里交给回调的是 object(工厂函数)本身,而不是执行后的结果。所以外部调用方取句柄时要先执行一次工厂再调方法:ref().method(...)

2. 为什么需要它(React 组件如何被 Vue / 原生宿主命令式调用)

标准的 forwardRef + useImperativeHandle 只适用于「父组件也是 React 组件」的场景——父组件用 useRef 拿一个 ref 对象传下去,子组件往里写句柄。

但当 React 组件是被另一套框架(如 Vue)或普通 JS 命令式地挂载时,并不存在「React 父组件」来持有 ref 对象。此时挂载方只能走另一条通道:

  1. 通过 createRoot(dom).render(<Comp />) 把 React 组件渲染进某个 DOM 容器;
  2. 通过一个回调 prop(示例中的 commonRef)把自己「接收句柄」的回调传给组件;
  3. 组件内部用 useCommonImperativeHandle(commonRef, factory),在挂载时把句柄推回给挂载方。

这也是示例代码里 useImperativeHandleuseCommonImperativeHandle 成对出现、方法体完全相同的原因:同一个组件既要能被 React 父组件通过 ref 控制,也要能被外部宿主通过 commonRef 控制,所以用两种方式各暴露一份同样的句柄。

3. 完整调用链(数据流)

结合示例代码,整条链路如下:

宿主(Vue / 原生 JS)
│ 1. 调用共享库的 rule({ appendDomId, ... }) 命令式挂载

rule()
│ createRoot(dom).render(<Rule commonRef={cb} ref={...} ... />)
│ 生成一个 Symbol 作为 key,并 return 给调用方

Rule 组件首次挂载
│ useCommonImperativeHandle(commonRef, factory)
│ → useEffect 触发 commonRef(factory)

commonRef 回调
│ maps.set(key, { root, ref: factory }) // 把「卸载句柄 root」+「方法工厂 ref」存进 Map

宿主后续需要控制组件时
│ maps.get(key).ref().method(...) // 取工厂 → 执行出句柄 → 调方法

三个要点:

  • key(Symbol)是连接凭据:由 rule() 返回,宿主自己保存,用来从 maps 里找回对应实例。
  • maps 存的是 { root, ref }root 负责卸载(root.unmount()),ref 负责取句柄。
  • 为什么存工厂而不是对象:因为 useCommonImperativeHandle 交给回调的是 object(工厂),所以调用方要 ref().method() 先执行一次工厂。

4. 几个容易踩的坑

  1. 空依赖会导致闭包过期useEffect 依赖是 [],回调只执行一次,工厂函数捕获的 state/props 都是「首次挂载时」的值。示例里的方法没有直接读 state,而是通过 setState(引用稳定)和 useRefpopupConfirmCallbackRef)来读写,从而避开了过期闭包。建议句柄内部只使用稳定的 setter 与 ref,不要直接引用会变化的 state。
  2. 命令式挂载要记得卸载:命令式渲染不会像声明式组件那样随父级自动销毁,宿主在不再需要时(弹窗关闭、页面卸载)应主动调用 root.unmount(),否则会造成内存泄漏、事件残留。
  3. 句柄只交出去一次useCommonImperativeHandle 不像 useImperativeHandle 那样随依赖更新重新赋值,它只在挂载后执行一次,适合「一次性把句柄交出去」的场景;若句柄需要随 state 变化而更新,应改用 useImperativeHandle 并传入依赖数组。