forwardRef

169 阅读5分钟

导读--实际上函数式组件是没有ref的。那我们想拿到函数组件内部某个dom实例,就可以通过forwardRef转发出去。

react 生命周期

image.png


参考 

forwardRef(render) 

使用 forwardRef() 让组件接收 ref 并将其传递给子组件:

import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
  // ...
});
参数 
  • render:组件的渲染函数。React 会调用该函数并传入父组件传递的 props 和 ref。返回的 JSX 将作为组件的输出。
返回值 

forwardRef 返回一个可以在 JSX 中渲染的 React 组件。与作为纯函数定义的 React 组件不同,forwardRef 返回的组件还能够接收 ref 属性。

警告 
  • 在严格模式中,为了 帮助找到意外的副作用,React 将会 调用两次渲染函数。不过这仅限于开发环境,并不会影响生产环境。如果渲染函数是纯函数(也应该是),这不应该影响组件逻辑。其中一个调用的结果将被忽略。

render 函数 

forwardRef 接受一个渲染函数作为参数。React 将会使用 propsref 调用此函数:

const MyInput = forwardRef(function MyInput(props, ref) {
  return (
    <label>
      {props.label}
      <input ref={ref} />
    </label>
  );
});
参数 
  • props:父组件传递过来的 props。
  • ref:父组件传递的 ref 属性。ref 可以是一个对象或函数。如果父组件没有传递一个 ref,那么它将会是 null。你应该将接收到的 ref 转发给另一个组件,或者将其传递给 useImperativeHandle
返回值 

forwardRef 返回一个可以在 JSX 中渲染的 React 组件。与作为纯函数定义的 React 组件不同,forwardRef 返回的组件还能够接收 ref 属性。


用法 

将 DOM 节点暴露给父组件 

默认情况下,每个组件的 DOM 节点都是私有的。然而,有时候将 DOM 节点公开给父组件是很有用的,比如允许对它进行聚焦。将组件定义包装在 forwardRef() 中便可以公开 DOM 节点:

import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
  const { label, ...otherProps } = props;
  return (
    <label>
      {label}
      <input {...otherProps} />
    </label>
  );
});

你将在 props 之后收到一个 ref 作为第二个参数。将其传递到要公开的 DOM 节点中:

import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
  const { label, ...otherProps } = props;
  return (
    <label>
      {label}
      <input {...otherProps} ref={ref} />
    </label>
  );
});

这样,父级的 Form 组件就能够访问 MyInput 暴露的 <input> DOM 节点:

function Form() {
  const ref = useRef(null);
  function handleClick() {
    ref.current.focus();
  }
  return (
    <form>
      <MyInput label="Enter your name:" ref={ref} />
      <button type="button" onClick={handleClick}>
        编辑
      </button>
    </form>
  );
}

Form 组件 将 ref 传递至 MyInputMyInput 组件将该 ref 转发<input> 浏览器标签。因此,Form 组件可以访问该 <input> DOM 节点并对其调用 focus()

请记住,将组件内部的 ref 暴露给 DOM 节点会使得在稍后更改组件内部更加困难。通常会暴露可重用的低级组件的 DOM 节点,例如按钮或文本输入框,但不会在应用程序级别的组件中这样做,例如头像或评论。

示例

第 1 个示例 共 2 个挑战: 聚焦文本输入框 

点击该按钮将聚焦输入框。Form 组件定义了一个 ref 并将其传递到 MyInput 组件。MyInput 组件将该 ref 转发至浏览器的 <input> 标签,这使得 Form 组件可以聚焦该 <input>

//APP.JS
import { useRef } from 'react';
import MyInput from './MyInput.js';

export default function Form() {
  const ref = useRef(null);

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

  return (
    <form>
      <MyInput label="Enter your name:" ref={ref} />
      <button type="button" onClick={handleClick}>
        Edit
      </button>
    </form>
  );
}
//MyInput.js
import { forwardRef } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  const { label, ...otherProps } = props;
  return (
    <label>
      {label}
      <input {...otherProps} ref={ref} />
    </label>
  );
});

export default MyInput;
第 2 个示例 共 2 个挑战: 播放和暂停视频 

点击按钮将调用 <video> DOM 节点上的 play()pause() 方法。App 组件定义了一个 ref 并将其传递到 MyVideoPlayer 组件。MyVideoPlayer 组件将该 ref 转发到浏览器的 <video> 标签。这使得 App 组件可以播放和暂停 <video>

//app.js
import { useRef } from 'react';
import MyVideoPlayer from './MyVideoPlayer.js';

export default function App() {
  const ref = useRef(null);
  return (
    <>
      <button onClick={() => ref.current.play()}>
        Play
      </button>
      <button onClick={() => ref.current.pause()}>
        Pause
      </button>
      <br />
      <MyVideoPlayer
        ref={ref}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
        type="video/mp4"
        width="250"
      />
    </>
  );
}
import { forwardRef } from 'react';

const VideoPlayer = forwardRef(function VideoPlayer({ src, type, width }, ref) {
  return (
    <video width={width} ref={ref}>
      <source
        src={src}
        type={type}
      />
    </video>
  );
});

export default VideoPlayer;

在多个组件中转发 ref 

除了将 ref 转发到 DOM 节点外,还可以将其转发到自定义组件,例如 MyInput 组件:

const FormField = forwardRef(function FormField(props, ref) {
  // ...
  return (
    <>
      <MyInput ref={ref} />
      ...
    </>
  );
});

如果 MyInput 组件将 ref 转发给它的 <input>,那么 FormField 的 ref 将会获得该 <input>

function Form() {
  const ref = useRef(null);
  function handleClick() {
    ref.current.focus();
  }
  return (
    <form>
      <FormField label="Enter your name:" ref={ref} isRequired={true} />
      <button type="button" onClick={handleClick}>
        Edit
      </button>
    </form>
  );
}

Form 组件定义了一个 ref 并将其传递给 FormFieldFormField 组件将该 ref 转发给 MyInput,后者又将其转发给浏览器的 <input> DOM 节点。这就是 Form 获取该 DOM 节点的方式。

陷阱

不要滥用 ref。只应在无法使用 props 表达的 命令式 行为中使用 ref:例如滚动到节点、将焦点放在节点上、触发动画,以及选择文本等等。

如果可以将某些东西使用 props 表达,那就不应该使用 ref。例如,不要从一个 Modal 组件中暴露像 { open, close } 一样的命令式句柄,更好的做法是将 isOpen 作为 prop,像这样 <Modal isOpen={isOpen} />Effect 可以帮助通过 props 暴露命令式行为。


疑难解答 

我的组件使用了 forwardRef,但是它的 ref 总是为 null 

这通常意味着你忘记实际使用你所接收到的 ref 了。

例如,这个组件的 ref 没有被使用:

const MyInput = forwardRef(function MyInput({ label }, ref) {
  return (
    <label>
      {label}
      <input />
    </label>
  );
});

为了修复它,将 ref 传递给一个可以接受 ref 的 DOM 节点或另一个组件:

const MyInput = forwardRef(function MyInput({ label }, ref) {

  return (
    <label>
      {label}
      <input ref={ref} />
    </label>
  );
});

如果某些逻辑是有条件的,MyInputref 可能也会为 null

const MyInput = forwardRef(function MyInput({ label, showInput }, ref) {
  return (
    <label>
      {label}
      {showInput && <input ref={ref} />}
    </label>
  );
});

如果 showInputfalse,则 ref 将不会被转发到任何节点,并且 MyInputref 会保持为空。如果这个条件隐藏在另一个组件中,那么很容易忽略这一点,比如这个例子中的 Panel

const MyInput = forwardRef(function MyInput({ label, showInput }, ref) {

  return (
    <label>
      {label}
      <Panel isExpanded={showInput}>
        <input ref={ref} />
      </Panel>
    </label>
  );
});