Skip to content

单项数据流的“后门”

需要先理解react的单向数据流,才能理解“后门”的原理。


有时候我们需要命令式地去控制组件(“喂,子组件,立刻给我把输入框清空并聚焦!”)。

这需要三个搭档配合:

  1. useRef:父组件手里的“遥控器”。
  2. forwardRef:连接线,让父组件的“遥控器”能连上子组件。
  3. useImperativeHandle:子组件里的“控制面板”,决定父组件能按哪些按钮。

代码实现

  1. 第一步:子组件使用 forwardRefuseImperativeHandle

子组件必须把自己包裹在 forwardRef 里,否则它接收不到父组件传来的 ref。

jsx

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

// 1. 使用 forwardRef 包裹组件,这样才能接收第二个参数 ref
const MyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  const [val, setVal] = useState('');

  // 2. 使用 useImperativeHandle 定义暴露给父组件的方法
  // useImperativeHandle(ref, createHandle, [deps])
  useImperativeHandle(ref, () => ({
    
    // 方法 A: 聚焦
    focus: () => {
      inputRef.current.focus();
    },

    // 方法 B: 清空并聚焦 (组合拳)
    clearAndFocus: () => {
      setVal(''); // 清空状态
      inputRef.current.focus(); // 聚焦 DOM
    },
    
    // 方法 C: 一个完全自定义的动作
    sayHello: () => {
      alert("来自子组件的问候!");
    }

  }));

  return (
    <input
      ref={inputRef} // 绑定内部真实的 DOM
      value={val}
      onChange={e => setVal(e.target.value)}
      placeholder="写点什么..."
      style={{ padding: '10px', fontSize: '16px' }}
    />
  );
});
  1. 父组件创建 Ref 并调用
jsx
function App() {
  // 1. 创建一个 ref (也就是遥控器)
  const childRef = useRef(null);

  const handleClick = () => {
    // 2. 通过 ref.current 直接调用子组件暴露的方法
    // 注意:这里只能调用 useImperativeHandle 里定义的那几个方法
    if (childRef.current) {
      childRef.current.clearAndFocus();
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>父组件控制台</h1>
      <button onClick={handleClick}>
        清空并聚焦子组件
      </button>
      <button onClick={() => childRef.current.sayHello()}>
        让子组件打招呼
      </button>
      
      <hr />
      
      {/* 3. 把 ref 传给子组件 */}
      <MyInput ref={childRef} />
    </div>
  );
}

export default App;

原理解析

你可以把这个过程想象成 “定制 API”

  1. 没有 useImperativeHandle 时: 如果父组件把 ref 传给一个原生的 <input ref={ref} />,父组件拿到的就是整个 DOM 节点。父组件可以随心所欲地操作 DOM,甚至做一些破坏性的事情(比如 dom.remove()),这很不安全。
  2. 有了 useImperativeHandle 时: 子组件说:“父组件你别直接碰我的 DOM。我给你提供一个手柄 (Handle),上面只有 focusclear 两个按钮。你只能按这两个按钮,别的不能动。”
    • 安全性:隐藏了内部实现细节(比如内部用了什么 DOM 结构)。
    • 灵活性:可以将多个操作组合成一个方法(比如 clearAndFocus)。

什么时候用🚀?

React 官方文档: Ref 是逃生舱 🚀 (Escape Hatch),不是首选方案。

场景推荐做法为什么?
想要修改子组件的内容✅Props符合 React 单向数据流,易于维护。
想要控制显示/隐藏✅Props(State) isOpen={true} 比调用 open() 方法更清晰。
媒体播放 (Play/Pause)🚀 Ref视频播放是时间相关的动作,不是状态。
焦点管理 (Focus)🚀 Ref聚焦是一个瞬时动作。
强制滚动 (Scroll to)🚀 Ref滚动位置通常不存放在 State 里。
集成第三方非 React 库🚀 Ref比如集成 D3.jsGoogle Maps,必须操作 真实DOM