你不知道的React系列(八)Refs

167 阅读3分钟

「回顾 2022,展望 2023,我正在参与2022 年终总结征文大赛活动

概念

当一条信息用于渲染时,将它保存在 state 中。当一条信息仅被事件处理器需要,并且更改它不需要重新渲染时,使用 ref

  • ref 对象在组件的整个生命周期内持续存在
  • 可以很方便地保存任何可变值,类似于一个 class 的实例属性
  • 变更 .current 属性不会引发组件重新渲染
  • 通常,你将从事件处理器访问 refs。 如果你想使用 ref 执行某些操作,但没有特定的事件可以执行此操作,你可能需要一个 effect

何时使用 Refs

  • 管理焦点,文本选择或媒体播放
  • 触发强制动画
  • 集成第三方 DOM 库
  • 存储 timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要被用来计算 JSX 的其他对象

React 何时添加 refs

React 在组件挂载设置 ref.current。在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。

ref 的最佳实践

  • 将 ref 视为应急方案。  当你使用外部系统或浏览器 API 时,ref 很有用。如果你很大一部分应用程序逻辑和数据流都依赖于 ref,你可能需要重新考虑你的方法。
  • 不要在渲染过程中读取或写入 ref.current  唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。

使用

事件处理器中清除循环定时器

function Timer() {
  const intervalRef = useRef();
  useEffect(() => {
    const id = setInterval(() => {});
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });
  function handleCancelClick() {
    clearInterval(intervalRef.current);
  }
}

使用 ref 操作 DOM

避免更改由 React 管理的 DOM 节点。

import { useRef } from 'react';
const myRef = useRef(null);
<div ref={myRef}>
myRef.current.scrollIntoView();

访问另一个组件的 DOM 节点(ref 转发)

默认情况下,React 不允许组件访问其他组件的 DOM 节点。

ref 可以是对象或者函数,没有传递 null

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>
  );
}

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

ref 转发更高的灵活性

通过 props 明文传递 ref 给子组件的 ref 属性

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

const Parent = () => {
  const inputElement = useRef(null);
  return <CustomTextInput inputRef={inputElement} />;
};

注意

  • 避免使用 refs 来做任何可以通过声明式实现来完成的事情
  • 勿过度使用 Refs 尽量使用 state 提升
  • 除了 初始化 外不要在渲染期间写入 或者读取 ref.current

避免重复创建 ref 的内容

// 每次渲染时都会调用 new VideoPlayer
function Video() {
  const playerRef = useRef(new VideoPlayer());
  // ...

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  // ...

回调 ref

绑定或解绑 DOM 节点的 ref 时运行某些代码

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

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

问答

ref 总是 null

转发组件忘记使用 ref

ref 指定的节点被隐藏了

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

原理

useRef 内部是如何运行的?

尽管 useState 和 useRef 都是由 React 提供的,原则上 useRef 可以在 useState 的基础上实现。 你可以想象在 React 内部,useRef 是这样实现的:

// React 内部
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

ref 和 state

refstate
useRef(initialValue)返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
更改时不会触发重新渲染更改时触发重新渲染。
可变 —— 你可以在渲染过程之外修改和更新 current 的值。“不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。
你不应在渲染期间读取(或写入) current 值。你可以随时读取 state。但是,每次渲染都有自己不变的 state 快照