九、深入学习ref、forward和useRef、useImperativeHandle

本文介绍了在React中使用refs的传统方法与新特性,如class组件的React.createRef和callbackRef,以及函数组件的useRef和useImperativeHandle。重点讲解了如何在函数组件中管理状态、暴露DOM操作,并强调了useImperativeHandle在控制组件暴露行为中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

要学习useRef和useImperativeHandle之前,我们得先学习和回顾一下再class组件中的refs。

一、什么是Refs?

官网是这么说的:

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

在react数据流中,props是父子组件交流的唯一方式。如果要修改一个子组件的状态,那么就需要使用新的props去重新渲染。但是,在某些情况下,我们需要在props数据流之外,强制去修改子组件。被修改的子组件可能是一个React组件的实例,也可能是DOM元素。React官方对于这两种情况都给出了解决办法。

react对于在calss组件中创建一个ref有两种方式。16.3版本之前的回调函数ref与16.3版本之后的React.createRef()。

二、创建Ref

1、class组件之React.createRef()

这种方式是使用 React.createRef() 创建的ref,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

2、class组件之callback Ref

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
    // 实例上(比如 this.textInput)
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 refs 一定是最新的。

你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef() 创建的对象 refs 一样。

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

在上面的例子中,Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点。

3、访问refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。
1、为Dom元素添加ref
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // 创建一个 ref 来存储 textInput 的 DOM 元素
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // 直接使用原生 API 使 text 输入框获得焦点
    // 注意:我们通过 "current" 来访问 DOM 节点
    this.textInput.current.focus();
  }

  render() {
    // 告诉 React 我们想把 <input> ref 关联到
    // 构造器里创建的 `textInput` 上
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
2、为 class 组件添加 Ref

如果我们想包装上面的 CustomTextInput,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 focusTextInput 方法

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }

  componentDidMount() {
    this.textInput.current.focusTextInput();
  }

  render() {
    return (
      <CustomTextInput ref={this.textInput} />
    );
  }
}

请注意,这仅在 CustomTextInput 声明为 class 时才有效:

class CustomTextInput extends React.Component {
  // ...
}
3、Refs 与函数组件

默认情况下,你不能在函数组件上使用 ref 属性,因为它们没有实例:

function MyFunctionComponent() {
  return <input />;
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  render() {
    // 这个是不能使用!!!!!!!!
    return (
      <MyFunctionComponent ref={this.textInput} />
    );
  }
}

如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle 结合使用),或者可以将该组件转化为 class 组件。

不管怎样,你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件

function CustomTextInput(props) {
  // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

4、将 DOM Refs 暴露给父组件

​ 在极少数情况下,你可能希望在父组件中引用子节点的DOM节点,一般来说,是不建议这么做的。因为这样会打破组件的封装,但它偶尔可以用于触发焦点或者测量子DOM节点的大小或者位置。

​ 虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。

​ 如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发,也就是forward Ref。forward Ref使组件可以像暴露自己的 ref 一样暴露子组件的 ref

​ 如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用这个替代方案将 ref 作为特殊名字的 prop 直接传递。

1、什么是forward Ref?

​ Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧

2、转发 refs 到 DOM 组件

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

​ 使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  3. React 传递 refforwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

注意:

​ 第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 refRef 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

3、在高阶组件中转发 refs

forward Ref这个技巧对于高阶组件是非常有用的。

HOC 示例开始:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

“logProps” HOC 透传(pass through)所有 props 到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props:

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);

​ 上面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

这意味着用于我们 FancyButton 组件的 refs 实际上将被挂载到 LogProps 组件

import FancyButton from './FancyButton';

const ref = React.createRef();

// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

这个时候,我们就可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件React.forwardRef 接受一个渲染函数,其接收 propsref 参数并返回一个 React 节点。

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}
5、forward Ref的问题

forwardRef的做法本身没有什么问题, 但是我们是将子组件的DOM直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控
  • 父组件可以拿到DOM后进行任意的操作
  • 我们只是希望父组件可以操作的focus,其他并不希望它随意操作其他方法

三、useRef

上面说React在class组件中有两种创建ref的方式,但不能给函数式组件使用ref,但是可以在函数式组件内部使用回调函数ref和React.CreateRef()

1、函数组件之React.createRef()

import React, { useEffect } from 'react';

const InputFocus = () => {

  const inputRef = React.createRef();

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  })

  return <input type="text" ref={inputRef}/>
}

export default InputFocus;

2、函数组件之callback Ref

import React, { useEffect } from 'react';

const InputFocus = () => {

  let inputRef= null;

  useEffect(() => {
    if (inputRef) {
      inputRef.focus();
    }
  })

  return <input type="text" ref={(el) => inputRef = el}/>
}

export default InputFocus;

同样hooks也为我们提供了一个hook来创建ref。

3、useRef的使用

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

createRefuseRef不仅可以保存DOM元素和class组件实例,也可以用来保存任何值

function App() {
  const valRef = React.useRef(80);
  return <div>
    value: {valRef.current}
    <button onClick={() => valRef.current += 1}>+</button>
  </div>
}

valRef的current属性值为80,点击"+" 按钮将会在valRef.current的基础上+1,改变valRef.current不会触发组件的更新

因此我们需要别的方式来在ref.current变更时,触发组件更新,展示最新的ref.current值,使用useState强制更新,其实也可以用回调函数ref。

function App() {
  const valRef = React.useRef(80);
  const [, setChange] = React.useState();
  return <div style={{padding: "100px 200px"}}>
    value: {valRef.current}
    <button onClick={() => {
      valRef.current += 1;
      setChange({});
    }}>+
    </button>
  </div>
}

**React.createRef可以用于class组件和Function组件,React.useRef必须用于函数组件。**当React.createRef()用于Fucntion组件的时候,可能出现一些问题。

function App() {
  const valRef = React.createRef();
  const [, setChange] = React.useState();
  return <div style={{padding: "100px 200px"}}>
    value: {valRef.current}
    <button onClick={() => {
      valRef.current = 80;
      setChange({});
    }}>+
    </button>
  </div>
}

我们用createRef替换掉useRef,在这里无论点击多少次按钮,最后还是会显示为空(值为null):

这是因为函数组件的行为仍然是函数,他们在渲染和重渲染的 就像普通的函数一样

所以,当App这个函数组件被重新渲染时,App函数将会执行,并且重新创建、初始化所有的变量和表达式。因此,createRef每次都会被执行,所以对应的值总是为null。

4、useRef解决了什么问题

为了在函数组件中保存状态,useRef就被创造出来,它将会在函数组件的生命周期中,保持状态不变,除非手动进行修改。

createRef在class组件中能够发挥出作用,是因为class组件在更新的时候只会调用render、componentDidUpdate等几个method。这整个过程,只会在组件初始化的时候创建ref,之后的更新过程都不会重新初始化class组件的实例,因此ref的值会在class组件的声明周期中保持不变(除非手动更新)。

​ 所以在函数式组件中,我们应该使用useRef去创建我们的ref对象,useRef 总会在每次渲染时返回同一个 ref 对象,除非手动改变

但是,当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

import React, {useState, useCallback} from "react";
import ReactDOM from "react-dom";

function MeasureExample() {
  const [height, setHeight] = useState(0);

  // Because our ref is a callback, it still works
  // even if the ref only gets attached after button
  // click inside the child component.
  const measureRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <Child measureRef={measureRef} />
      {height > 0 &&
        <h2>The above header is {Math.round(height)}px tall</h2>
      }
    </>
  );
}

function Child({ measureRef }) {
  const [show, setShow] = useState(false);
  if (!show) {
    return (
      <button onClick={() => setShow(true)}>
        Show child
      </button>
    );
  }
  return <h1 ref={measureRef}>Hello, world</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<MeasureExample />, rootElement);


在这里,我们没有选择使用useRef,而是使用了Callback Ref可以确保 即便子组件延迟显示被测量的节点(比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1> 组件在整个重新渲染期间始终存在。如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver 或基于其构建的第三方 Hook。

四、useImperativeHandle

1、基本使用:

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例。不要直接使用forward Refforward Ref应该与useImperativeHandle一起使用

  • ref:定义 current 对象的 ref createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
  • 对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件
  • ref 的 current 属性上,如果为空数组,则不会重新输出。

2、它解决了什么问题

上面我们讲述了forward Ref的一些问题,forward Ref会将子组件的实例直接暴露给父组件,父组件可以对子组件实例做任意的操作,那么,useImperativeHandle就是来解决这个问题的,useImperativeHandle可以让我们选择暴露一些我们需要被特殊处理的操作。

  • 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
  • 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象

作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法

直接使用forward Ref:
import React, { useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';

// 实现 ref 的转发
const FancyButton = React.forwardRef((props, ref) => (
  <div>
    <input ref={ref} type="text" />
    <button>{props.children}</button>
  </div>
));

// 父组件中使用子组件的 ref
function App() {
  const ref = useRef();
  const handleClick = useCallback(() => ref.current.focus(), [ ref ]);

  return (
    <div>
      <FancyButton ref={ref}>Click Me</FancyButton>
      <button onClick={handleClick}>获取焦点</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

配合useImperativeHandle一起使用
import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

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

const App = props => {
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => fancyInputRef.current.focus()}
      >父组件调用子组件的 focus</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

​ 上面这个例子中与直接转发 ref 不同,直接转发 ref 是将 React.forwardRef 中函数上的 ref 参数直接应用在了返回元素的 ref 属性上,其实父、子组件引用的是同一个 ref 的 current 对象,官方不建议使用这样的 ref 透传,而使用 useImperativeHandle 后,可以让父、子组件分别有自己的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 方法来自定义开放给父组件的 current。

​ useImperativeHandle 的第一个参数是定义 current 对象的 ref第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象,这样可以像上面的案例一样,通过自定义父组件的 ref 来使用子组件 ref 的某些方法

最后再看一个useImperativeHandle的例子:

import React, {
  useState,
  useRef,
  useImperativeHandle,
  useCallback
} from 'react';
import ReactDOM from 'react-dom';

const FancyInput = React.forwardRef((props, ref) => {
  const [ fresh, setFresh ] = useState(false)
  const attRef = useRef(0);
  useImperativeHandle(ref, () => ({
    attRef,
    fresh
  }), [ fresh ]);

  const handleClick = useCallback(() => {
    attRef.current++;
  }, []);

  return (
    <div>
      {attRef.current}
      <button onClick={handleClick}>Fancy</button>
      <button onClick={() => setFresh(!fresh)}>刷新</button>
    </div>
  )
});

const App = props => {
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => console.log(fancyInputRef.current)}
      >父组件访问子组件的实例属性</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码搬运工_田先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值