React 基础体系 · 第 30/70 篇。示例以 React 19、现代 TypeScript 和主流框架能力为基础;客户端与服务端边界会明确说明。

React Ref 与 useImperativeHandle:焦点、测量和命令式 API

ref 是 React 提供的一条“保留引用通道”:组件提交(commit)到界面后,React 可以把某个 DOM 节点或组件实例写入 ref。它不同于 propsstate

  • props 描述父组件传给子组件的输入;
  • state 参与渲染,更新后通常会触发重新渲染;
  • ref 保存一个可变值,React 不会因为 ref.current 改变而重新渲染组件。

因此,ref 适合表示“已经提交到界面的对象”或“跨渲染保存但不驱动渲染的值”,典型场景包括:

  1. 让输入框获得焦点、选中文本;
  2. 读取 DOM 的尺寸和位置;
  3. 保存计时器、第三方实例等非渲染资源;
  4. 子组件向父组件暴露少量命令式操作。

useImperativeHandle 则用于控制父组件通过 ref 能看到什么。它不是让 React 变成命令式框架,而是把一个明确、受控的命令式接口接到声明式组件上。

一、先区分三种“引用”

1. DOM 引用

import { useRef } from 'react';

export function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <input
      ref={inputRef}
      placeholder="搜索"
      onFocus={() => {
        console.log(inputRef.current);
      }}
    />
  );
}

在首次渲染期间,inputRef.current 通常仍然是 null。React 完成 DOM 提交后,才会把对应的 <input> 写入其中;组件卸载时又会将其设回 null

可以把它抽象成以下时序:

render
  │
  ├─ inputRef.current 仍可能是 null
  │
commit:创建或更新 DOM
  │
  ├─ inputRef.current = HTMLInputElement
  │
浏览器绘制
  │
卸载或节点解绑
  │
  └─ inputRef.current = null

这意味着以下代码是不安全的:

function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  // 错误:render 阶段不能假设 DOM 已经存在
  inputRef.current?.focus();

  return <input ref={inputRef} />;
}

它可能不会立即抛错,因为可选链会跳过 null,但也不会在输入框真正挂载后自动执行聚焦。应该把操作放入事件处理器、Effect,或者由父组件在合适的时机调用。

2. 保存普通可变值的引用

const timerRef = useRef<number | null>(null);

function start() {
  timerRef.current = window.setTimeout(() => {
    console.log('done');
  }, 1000);
}

timerRef.current 的改变不会触发重新渲染。如果界面需要显示“计时器是否运行”,就不能只修改这个 ref,还需要使用 useState

这构成一个重要条件:

只有当某个值的变化不需要重新计算 JSX 时,才适合仅保存在 ref 中。

3. 暴露给父组件的命令式句柄

DOM 引用是“把节点交给使用者”;命令式句柄(imperative handle)是“只把预先定义的操作交给使用者”。

例如,父组件不必获得整个 <input>,而只获得:

type SearchBoxHandle = {
  focus: () => void;
  select: () => void;
  measure: () => DOMRect;
};

这样,子组件可以更换内部结构,而父组件仍然只依赖这个接口。

二、React 19 中的 ref 传递方式

在 React 19 中,函数组件可以像接收普通 prop 一样接收 ref

import {
  useImperativeHandle,
  useRef,
  type Ref,
} from 'react';

type SearchBoxHandle = {
  focus: () => void;
};

type SearchBoxProps = {
  ref?: Ref<SearchBoxHandle>;
};

function SearchBox({ ref }: SearchBoxProps) {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
  }), []);

  return <input ref={inputRef} />;
}

使用方:

function Page() {
  const searchBoxRef = useRef<SearchBoxHandle>(null);

  return (
    <>
      <SearchBox ref={searchBoxRef} />
      <button onClick={() => searchBoxRef.current?.focus()}>
        聚焦搜索框
      </button>
    </>
  );
}

这里有两个不同的 ref

  • inputRef:子组件内部指向真实 DOM 节点;
  • searchBoxRef:父组件持有子组件暴露的 SearchBoxHandle

useImperativeHandle 把前者包装成后者。

在 React 18 及更早的常见写法中,函数组件不能直接通过普通参数接收 ref,通常使用 forwardRef

import {
  forwardRef,
  useImperativeHandle,
  useRef,
} from 'react';

const SearchBox = forwardRef<SearchBoxHandle, {}>((_props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
  }), []);

  return <input ref={inputRef} />;
});

如果组件库需要同时兼容 React 18 和 React 19,应根据支持范围保留 forwardRef 方案;只面向 React 19 的代码可以直接声明 ref prop。TypeScript 类型还需要与相应版本的 @types/react 保持一致。

三、useImperativeHandle 的精确定义

API 形式是:

useImperativeHandle(ref, createHandle, dependencies?)

三个参数分别表示:

  • ref:父组件传入的 ref,可以是对象 ref、回调 ref,或者 null
  • createHandle:返回要暴露给父组件的值的函数;
  • dependencies:创建句柄所依赖的响应式值列表。

例如:

useImperativeHandle(ref, () => ({
  focus() {
    inputRef.current?.focus();
  },
}), []);

执行后,父组件的 ref.current 会指向返回的对象,而不是子组件函数本身:

父组件 ref.current
        │
        ▼
{
  focus: Function
}

如果没有 useImperativeHandle,把 ref 直接放到某个 DOM 元素上:

<input ref={ref} />

则父组件获得的是 HTMLInputElement。使用 useImperativeHandle 后,父组件获得的是子组件指定的句柄。

依赖数组决定句柄何时重建

假设句柄使用了 disabled

useImperativeHandle(ref, () => ({
  focus() {
    if (!disabled) {
      inputRef.current?.focus();
    }
  },
}), [disabled]);

disabled 发生变化时,React 会重新创建句柄。

React 使用类似 Object.is(previous, next) 的比较判断依赖是否变化。因此:

const options = { preventScroll: true };

useImperativeHandle(ref, () => ({
  focus() {
    inputRef.current?.focus(options);
  },
}), [options]);

如果 options 在每次渲染时都是新对象,句柄也会在每次渲染时重建。可以把稳定值移出组件、使用 useMemo,或者直接写成字面量调用。

如果句柄方法只读取 inputRef.current,不读取会变化的 props 或 state,则可以使用空依赖数组:

useImperativeHandle(ref, () => ({
  focus() {
    inputRef.current?.focus();
  },
}), []);

这里不会产生旧闭包问题,因为方法每次执行时都读取当前的 inputRef.current。相反,下面的代码会捕获创建句柄时的 value

function CounterInput({
  value,
  ref,
}: {
  value: string;
  ref?: React.Ref<{ logValue: () => void }>;
}) {
  useImperativeHandle(ref, () => ({
    logValue() {
      console.log(value);
    },
  }), []); // 错误:value 不在依赖中
}

父组件之后调用 logValue() 时,可能得到旧的 value。修复方式是声明依赖:

useImperativeHandle(ref, () => ({
  logValue() {
    console.log(value);
  },
}), [value]);

或者让方法读取一个始终同步的 ref。通常前者更直接,也更容易让代码和 React 的响应式数据流保持一致。

四、焦点控制:完整的可运行示例

下面的组件实现三个命令:

  • focus():让输入框获得焦点;
  • select():选中输入框中的全部文本;
  • measure():读取输入框当前的几何信息。
import {
  useImperativeHandle,
  useRef,
  type Ref,
} from 'react';

export type SearchBoxHandle = {
  focus: () => void;
  select: () => void;
  measure: () => DOMRect | null;
};

type SearchBoxProps = {
  initialValue?: string;
  ref?: Ref<SearchBoxHandle>;
};

export function SearchBox({
  initialValue = '',
  ref,
}: SearchBoxProps) {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus({
        preventScroll: true,
      });
    },

    select() {
      inputRef.current?.select();
    },

    measure() {
      return inputRef.current?.getBoundingClientRect() ?? null;
    },
  }), []);

  return (
    <input
      ref={inputRef}
      defaultValue={initialValue}
      aria-label="搜索"
    />
  );
}

父组件:

import { useRef } from 'react';
import {
  SearchBox,
  type SearchBoxHandle,
} from './SearchBox';

export function SearchPage() {
  const searchBoxRef = useRef<SearchBoxHandle>(null);

  function handleMeasure() {
    const rect = searchBoxRef.current?.measure();

    if (!rect) {
      console.log('输入框尚未挂载,或已经卸载');
      return;
    }

    console.log({
      left: rect.left,
      top: rect.top,
      width: rect.width,
      height: rect.height,
    });
  }

  return (
    <main>
      <SearchBox
        ref={searchBoxRef}
        initialValue="React Ref"
      />

      <div>
        <button onClick={() => searchBoxRef.current?.focus()}>
          聚焦
        </button>

        <button onClick={() => searchBoxRef.current?.select()}>
          全选
        </button>

        <button onClick={handleMeasure}>
          测量
        </button>
      </div>
    </main>
  );
}

调用路径是:

点击“聚焦”
  │
  ├─ 父组件读取 searchBoxRef.current
  ├─ 调用 SearchBoxHandle.focus()
  ├─ 子组件读取 inputRef.current
  └─ 浏览器将焦点设置到 input 元素

点击“测量”
  │
  ├─ 父组件调用 SearchBoxHandle.measure()
  ├─ 子组件执行 input.getBoundingClientRect()
  └─ 返回当前布局对应的 DOMRect

?. 不是多余的防御代码。父组件可能在以下时刻调用命令:

  • 子组件尚未完成挂载;
  • 条件渲染刚刚隐藏了子组件;
  • 页面正在切换路由;
  • 某个异步回调晚于组件卸载执行。

因此,句柄应当定义清楚“目标不存在时怎么办”。上面的实现选择返回 null 或静默不执行;另一种设计是抛出错误,但这只有在“不存在一定表示程序错误”时才合适。

五、测量 DOM:什么时候读才可靠

getBoundingClientRect() 返回元素相对于视口的矩形:

const rect = element.getBoundingClientRect();

rect.left;   // 视口左边缘到元素左边缘的距离
rect.top;    // 视口上边缘到元素上边缘的距离
rect.width;  // 元素布局宽度
rect.height; // 元素布局高度

这些值通常是 CSS 像素,可以是小数;它们受布局、滚动、缩放和 CSS transform 影响。它不是文档坐标。如果需要文档坐标,常见计算是:

const documentLeft = rect.left + window.scrollX;
const documentTop = rect.top + window.scrollY;

在事件中测量

用户点击按钮时,DOM 已经存在,因此直接测量通常成立:

function handleClick() {
  const rect = inputRef.current?.getBoundingClientRect();
  console.log(rect?.width);
}

在 DOM 更新后测量

如果某个 state 改变会影响尺寸,需要在 DOM 提交之后读取:

import {
  useLayoutEffect,
  useRef,
  useState,
} from 'react';

export function ResizablePanel() {
  const [large, setLarge] = useState(false);
  const panelRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const element = panelRef.current;
    if (!element) return;

    const rect = element.getBoundingClientRect();
    console.log('提交后的尺寸:', rect.width, rect.height);
  }, [large]);

  return (
    <>
      <button onClick={() => setLarge((value) => !value)}>
        切换尺寸
      </button>

      <div
        ref={panelRef}
        style={{
          width: large ? 400 : 200,
          height: 100,
        }}
      />
    </>
  );
}

useLayoutEffect 在 DOM 更新后、浏览器绘制前运行,适合“先测量,再决定同步布局”的场景,例如定位浮层。它可能阻塞绘制,因此不能把所有副作用都放进去;如果不需要避免首帧闪烁,普通 useEffect 通常更合适。

测量的因果链是:

setLarge(true)
  │
  ├─ React 重新渲染 JSX
  ├─ React 提交 style.width = 400px
  ├─ useLayoutEffect 读取 getBoundingClientRect()
  └─ 得到新布局的尺寸

如果在 setLarge(true) 之后、React 提交之前立即读取 DOM:

setLarge(true);

const width = panelRef.current?.getBoundingClientRect().width;

这里读到的可能仍是旧尺寸,因为 state 更新不会在这行代码中同步完成一次完整的渲染和提交。不要把“发起更新”和“DOM 已更新”当成同一时刻。

持续观察尺寸

如果尺寸会因窗口变化、字体加载或父布局变化而改变,仅在某次点击时测量不够。可以使用 ResizeObserver

import { useLayoutEffect, useRef, useState } from 'react';

export function MeasuredBox() {
  const boxRef = useRef<HTMLDivElement>(null);
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    const element = boxRef.current;
    if (!element) return;

    const observer = new ResizeObserver(([entry]) => {
      setWidth(entry.contentRect.width);
    });

    observer.observe(element);

    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <>
      <div ref={boxRef} style={{ resize: 'horizontal', overflow: 'auto' }}>
        拖动边缘改变尺寸
      </div>
      <p>内容宽度:{width}px</p>
    </>
  );
}

这里使用 state 是因为宽度要显示在 JSX 中。若只需要在命令执行时读取一次,就不必把每次测量结果存入 state。

六、回调 ref:需要在“节点绑定时”执行逻辑

对象 ref 的形式是:

const nodeRef = useRef<HTMLDivElement>(null);

return <div ref={nodeRef} />;

如果需要在节点挂载或解绑的瞬间执行逻辑,可以使用回调 ref:

import { useCallback } from 'react';

const setNode = useCallback((node: HTMLDivElement | null) => {
  if (node) {
    console.log('节点已绑定', node);
  } else {
    console.log('节点已解绑');
  }
}, []);

return <div ref={setNode} />;

React 会在节点绑定时调用 setNode(node),解绑时调用 setNode(null)。在 React 19 中,回调 ref 还支持返回清理函数的形式;不过如果需要兼容更早版本,显式处理 null 是更通用的写法。

useCallback 的作用不是让 DOM 更快,而是稳定回调函数身份。若每次渲染都产生新的 ref 回调,React 可能先用 null 清理旧回调,再用节点调用新回调:

// 每次 render 都是新函数,可能导致反复解绑和绑定
return <div ref={(node) => console.log(node)} />;

这在普通场景中通常只是额外调用,但如果回调中注册了观察器或第三方实例,就可能造成重复初始化或清理时序错误。

七、命令式 API 的边界:暴露操作,不暴露内部结构

直接暴露 DOM 节点很方便:

useImperativeHandle(ref, () => inputRef.current);

但这会让父组件依赖子组件的内部实现。若子组件以后从 <input> 改成由多个元素组成,或者加入遮罩层,这个接口就会变得脆弱。

更稳定的方式是暴露语义化命令:

type DialogHandle = {
  open: () => void;
  close: () => void;
  focusConfirmButton: () => void;
};

父组件只表达意图:

dialogRef.current?.open();

而不是操作内部节点:

dialogRef.current?.querySelector('.confirm')?.focus();

命令式 API 的设计边界可以形式化为:

父组件依赖 = 暴露句柄的类型和行为
父组件不依赖 = 子组件内部 DOM 层级和节点数量

句柄中的每个方法都应该有明确的前置条件和失败行为。例如:

type MenuHandle = {
  open: () => void;
  close: () => void;
  focusFirstItem: () => boolean;
};

focusFirstItem() 返回 false 表示菜单当前没有可聚焦项,比无条件假设节点存在更容易测试和诊断。

一个带状态的句柄示例

import {
  useImperativeHandle,
  useRef,
  useState,
  type Ref,
} from 'react';

type DialogHandle = {
  open: () => void;
  close: () => void;
};

type DialogProps = {
  ref?: Ref<DialogHandle>;
};

export function Dialog({ ref }: DialogProps) {
  const [open, setOpen] = useState(false);
  const closeButtonRef = useRef<HTMLButtonElement>(null);

  useImperativeHandle(ref, () => ({
    open() {
      setOpen(true);
    },
    close() {
      setOpen(false);
    },
  }), []);

  return (
    <div hidden={!open} role="dialog" aria-modal="true">
      <p>对话框内容</p>
      <button
        ref={closeButtonRef}
        onClick={() => setOpen(false)}
      >
        关闭
      </button>
    </div>
  );
}

这里 open() 不是直接修改 DOM 的 hidden 属性,而是调用 setOpen(true),让 React 通过 state 重新渲染。命令式 API 只作为“触发状态转换”的入口,实际界面仍由 state 和 JSX 描述。

如果打开后还需要聚焦关闭按钮,不能在 setOpen(true) 后立即假设按钮已经可用:

open() {
  setOpen(true);
  closeButtonRef.current?.focus(); // 可能仍为 null,或元素仍处于 hidden
}

更可靠的方式是监听 open

import { useEffect } from 'react';

useEffect(() => {
  if (open) {
    closeButtonRef.current?.focus();
  }
}, [open]);

状态变化先导致渲染和提交,Effect 再访问已经存在的节点。若必须在绘制前完成聚焦或测量,可以根据视觉要求使用 useLayoutEffect,但仍要处理节点为空的情况。

八、Ref 与 React 的渲染模型

React 的渲染可能被开始、暂停、重新计算或丢弃。渲染阶段只是计算“应该是什么”,提交阶段才会把结果应用到 DOM 并更新 ref。

因此,不应在 render 中执行依赖真实 DOM 的操作:

function BadComponent() {
  const ref = useRef<HTMLDivElement>(null);

  // 不应在这里测量、聚焦或修改 DOM
  const width = ref.current?.getBoundingClientRect().width;

  return <div ref={ref}>{width}</div>;
}

这段代码还存在逻辑循环:width 不是由本次 JSX 提交后得到的尺寸,渲染结果无法可靠反映当前 DOM 的测量值。

正确的数据流是:

DOM 提交
  │
  ├─ 读取 ref
  ├─ 测量 DOM
  ├─ setState(测量结果)
  └─ 下一次渲染显示结果

例如:

function WidthLabel() {
  const boxRef = useRef<HTMLDivElement>(null);
  const [width, setWidth] = useState<number | null>(null);

  useLayoutEffect(() => {
    const box = boxRef.current;
    if (!box) return;

    setWidth(box.getBoundingClientRect().width);
  }, []);

  return (
    <>
      <div ref={boxRef}>内容</div>
      <span>{width === null ? '测量中' : `${width}px`}</span>
    </>
  );
}

开发环境下的 Strict Mode 可能让某些初始化和清理路径更容易暴露问题。不要用“只初始化一次”的假设掩盖缺少清理的资源;观察器、事件监听器和第三方实例都应在对应的清理阶段释放。

九、对象 ref 与回调 ref 的差异

对象 ref

const ref = useRef<HTMLButtonElement>(null);

<button ref={ref} />;

特点:

  • 使用方通过 ref.current 读取当前值;
  • 适合大多数 DOM 节点引用;
  • React 会在提交和解绑时更新 current
  • current 改变不会触发渲染。

回调 ref

const setButton = useCallback((button: HTMLButtonElement | null) => {
  // 绑定或解绑时立即收到通知
}, []);

<button ref={setButton} />;

特点:

  • 可以在绑定时立即执行自定义逻辑;
  • 适合动态节点、第三方库初始化和资源清理;
  • 必须考虑 null,因为节点会卸载或替换;
  • 回调身份不稳定时,可能产生额外的解绑和绑定。

useImperativeHandle 接受二者

父组件可以这样写:

const handleRef = useRef<SearchBoxHandle>(null);

<SearchBox ref={handleRef} />;

也可以这样写:

<SearchBox
  ref={(handle) => {
    if (handle) {
      handle.focus();
    }
  }}
/>

组件内部不需要知道父组件使用的是对象 ref 还是回调 ref;React 负责把 useImperativeHandle 返回的值交给对应的 ref。

十、客户端与服务端边界

焦点和测量都依赖浏览器 DOM:

  • focus() 是浏览器行为;
  • getBoundingClientRect() 只能在真实 DOM 存在时读取;
  • ResizeObserver 是浏览器 API;
  • ref 不应作为服务端与客户端之间需要序列化传输的数据。

在使用 React Server Components 的框架中,执行这些逻辑的组件必须位于客户端边界内。例如,在支持该约定的框架中,文件顶部通常需要:

'use client';

示意:

'use client';

import { useRef } from 'react';

export function ClientSearchPage() {
  const ref = useRef<SearchBoxHandle>(null);

  return <SearchBox ref={ref} />;
}

服务端渲染可以输出初始 HTML,但服务端不能执行:

window
document
element.focus()
element.getBoundingClientRect()

如果在服务端渲染阶段直接访问这些对象,会出现 window is not defineddocument is not defined 或 hydration 相关问题。把 DOM 操作放在事件处理器、useEffectuseLayoutEffect 中,只能解决“执行时机”问题;组件本身仍必须处于允许使用客户端 Hook 和浏览器 API 的边界内。

十一、常见失败表现与诊断方法

1. ref.current 一直是 null

可能原因:

  • 读取发生在首次 render 阶段;
  • 目标节点被条件渲染隐藏或卸载;
  • ref 没有真正传到子组件;
  • React 18 代码遗漏了 forwardRef
  • 父组件调用早于子组件挂载;
  • 使用了服务端组件而不是客户端组件。

可以按以下路径检查:

console.log('父组件句柄:', ref.current);

在子组件中检查内部节点:

console.log('内部节点:', inputRef.current);

如果父句柄存在但内部节点为空,说明句柄已经创建,但调用时机或条件渲染不满足。如果父句柄本身为空,则应检查 ref 传递和挂载时机。

2. 命令方法使用旧数据

表现:

界面已经显示新值,但 ref.current.logValue() 打印旧值

典型原因是依赖数组遗漏:

useImperativeHandle(ref, () => ({
  logValue: () => console.log(value),
}), []);

修复:

useImperativeHandle(ref, () => ({
  logValue: () => console.log(value),
}), [value]);

如果句柄需要保持对象身份稳定,也可以让方法通过一个 ref 读取最新值,但这会增加同步代码,只有在确实需要稳定句柄身份时才值得使用。

3. 测量值为零或不符合预期

常见原因包括:

display: none;

此时元素没有参与布局,矩形尺寸通常为零。其他原因还包括:

  • 元素尚未提交;
  • 测量发生在错误的 state 更新时机;
  • 父元素尺寸尚未稳定;
  • CSS transform 改变了视觉矩形;
  • 字体尚未加载完成;
  • 元素被替换或已经卸载;
  • 测量的是外层包装元素,而不是实际内容元素。

诊断时应同时记录:

const element = ref.current;

console.log({
  element,
  connected: element?.isConnected,
  rect: element?.getBoundingClientRect(),
  computedDisplay: element
    ? getComputedStyle(element).display
    : null,
});

这些信息可以区分“节点不存在”“节点不在布局中”和“节点存在但尺寸计算不同”三类问题。

4. 父组件过度依赖内部 DOM

如果父组件频繁调用:

ref.current?.querySelector(...)
ref.current?.classList.add(...)
ref.current?.style...

通常说明组件边界泄漏。此时应重新定义句柄,使其暴露业务操作,例如:

type DatePickerHandle = {
  focusInput: () => void;
  openCalendar: () => void;
  clear: () => void;
};

这不是为了隐藏所有 DOM,而是为了让父组件依赖稳定行为,而不是依赖偶然的节点结构。

十二、何时不应该使用 Ref

如果问题可以通过 props 和 state 表达,就不必引入命令式 API。

不推荐:

// 父组件通过 ref 命令子组件改变显示状态
panelRef.current?.show();

更适合:

<Panel visible={visible} />

因为 visible 是可追踪、可组合、可测试的数据流。命令式句柄更适合不可自然表达为 JSX 输入的动作:

  • 将焦点移动到某个 DOM 节点;
  • 读取浏览器布局;
  • 调用播放器、地图、编辑器等第三方实例;
  • 触发组件内部必须保留封装的短暂操作。

可以用一个判断条件概括:

如果需求是“界面应该呈现什么” → 优先 props/state
如果需求是“对已存在对象执行什么动作” → 可以考虑 ref

十三、生产取舍

ref 的核心保证是:在 React 提交完成后,React 会把关联对象写入 ref,并在解绑时清除或通知回调;它不保证:

  • 首次 render 时 ref 已经可用;
  • 调用句柄时目标一定存在;
  • ref.current 的变化会触发渲染;
  • 由 ref 读取的布局值会自动保持更新;
  • 服务端可以执行浏览器操作;
  • 句柄中的方法自动拥有最新的 props 和 state。

useImperativeHandle 的价值在于建立一个受控接口:

内部 DOM / 实例
       │
       ▼
useImperativeHandle
       │
       ▼
有限、可类型检查的命令式句柄
       │
       ▼
父组件在事件或 Effect 中调用

焦点控制应在节点提交后执行;测量应在正确的布局时机执行;状态变化仍应由 state 驱动;句柄方法则应明确空节点、卸载、旧闭包和服务端边界。掌握这几条因果关系后,ref 就不再只是“绕过 React 的 DOM 指针”,而会成为声明式组件中一个边界清晰的外部操作接口。


系列导航与关联阅读

官方资料

本文依据 React 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。