React 新官网学习 - useRef 渣翻

215 阅读7分钟

beta.reactjs.org/reference/r…

概述

useRef 引用一个不需要被渲染的值

const ref = useRef(initialValue)


参考

useRef(initialValue)

调用 useRef: 在组件的最顶层声明 ref

import {useRef} from 'react';

function MyComponent() {
  const intervalRef = useRef(0);
  const inputRef = useRef(null);
}

参数

  • initialValue

ref 对象 current 属性初始化的值。可以是任意类型。第一次渲染之后此初始值会被忽略

返回值

useRef 返回只有一个属性的对象:

  • current

初始值: 设置的 initialValue 后续可以修改为其他值

如果传递了 ref 对象作为 ref 属性给 JSX 节点,React 会将其设置为 useRefcurrent 属性

下一次渲染: useRef 会返回相同的对象


附加说明

  • 可以更新 ref.current 属性值。和 state 不一样,useRef 是可以直接更新值的。但是如果 useRef 的值需要被渲染在页面上,那么建议不用 useRef
  • ref.current 属性更新时,React 不会再次渲染组件。React 不会意识到你改变了 ref.current 属性,因为 ref 是只是一个平平无奇的 JS 对象
  • 除了在初始化的情况下,不要在渲染(组件)时使用 ref.current, 这会使组件的渲染行为不可预测(结果也许不尽人意)
  • 严格模式下,React 为了找到多余的非纯函数会调用两次组件函数。这只会在开发环境下发生而不会影响到生产环境。这意味着每个 ref 对象会被创建两次,而其中一个 ref 对象会被销毁。如果组件函数是纯函数(事实上每个组件函数都应该是),这对组件逻辑不会产生任何影响

用法

用法1: 用 ref 存值

调用 useRef 在组件顶层声明一个或多个 refs

beta.reactjs.org/learn/refer…

import {useRef} from 'react';

function Stopwatch() {
  const intervalRef = useRef(0)
  //...
}

useRef 返回只有一个 current 属性ref 对象,初始值为设定的 initial value

在下一次渲染时,useRef 会返回相同的对象。可以使用 refcurrent 属性存储值并稍后读取

state 有点类似,但 refstate 有着重要的区别

beta.reactjs.org/reference/r… - state

改变 ref 的值不会触发组件的重新渲染。 因此 refs 非常适合用来存储不会影响组件页面渲染的信息

eg:如果你需要存储一个 interval ID 并稍后使用它,那么就可以将它赋值给 ref

ref 更新这个值,可以重新赋值给 current 属性

function handleStartClick() {
  const intervalId = setInterval(() => { xxx }, 1000);
  intervalRef.current = intervalId;
}

之后就可以从 ref 中读取到 interval ID, 从而清除该定时器:

function handleStopClick() {
  const inervalId = intervalRef.current;
  clearInterval(intervalId);
}

ref 很明确的是:

  • 可以在组件重新渲染之间存储信息(而常规的变量,每次渲染都会重置)
  • 更改值不会触发重复渲染 (state 变量会触发新的渲染)
  • 存储的信息对每个组件副本来说都是局部/本地的(不像外部的其他变量是共享的)

改变 ref 的值不会触发组件的重复渲染,因此 ref 不适合用来存储需要在页面上渲染的信息。这种情况下用 useState 更为合适

beta.reactjs.org/learn/refer… - useRef 和 useState 的使用区别

ref 存值 例子

1 Click counter

此组件使用 ref 来追踪按钮的点击次数。此处可以使用 ref 而不用 state 是因为点击的次数只在事件处理函数中读取

如果 JSX 中展示 {ref.current}, 点击按钮后这个值不会更新。因为 ref.current 不会触发页面渲染。用来渲染页面的信息应该使用 state 而不是 ref

import {useRef} from 'react';

export default function Counter() {
  const ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert(ref.current + 'times');
  }

  return (
    <button onClick={handleClice}>Click me</button>
  )
}
2 A stopwatch

此例同时用了 stateref. startTimenow 都是 state 变量,因为他们用于页面渲染

我们需要 intervalId 来处理按钮停止计时。因为 intervalId 没有用来渲染,所以将其存在 ref 中,人为控制更新

import {useState, useRef} from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);

    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function  handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if(startTime !== null && now !== null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleStop}>Stop</button>
    </>
  ) 
}

陷阱

在渲染中不要写入或读取 ref.current

React 希望组件像纯函数那样运行:

beta.reactjs.org/learn/keepi… - 纯函数

  • 如果输入(props, statecontext)一样,那组件返回值也需要返回完全一样的 JSX

beta.reactjs.org/learn/passi… - props

beta.reactjs.org/learn/state… - state

beta.reactjs.org/learn/passi… - context

  • 用不同的顺序或者传入不同的参数调用时不能影响其他调用的结果

在渲染过程中写入或读取 ref 不符合 React 的期待:

function MyComponent() {
  //🚩 Don't write a ref during rendering
  myRef.current = 123;
  //🚩 Don't read a ref during rendering
  return <h1>{myOtherRef.current}</h1>
}

可以在 事件处理函数 或者 effects 中写入或读取 refs

function MyComponent() {
  useEffect(() => {
    //✅ You can read or write refs in effects
    myRef.current = 123;
  });

  function handleClick() {
    //✅ You can read or write refs in event handlers
    doSomething(myOtherRef.current);
  }
}

如果必须要在渲染过程中写入和读取一些信息,可以用 state

如果打破了上述规则,也许组件也能运作,但是React 的大部分新特性都依赖于上述规则

用法2: 用 ref 操作 DOM

使用 ref 操作 DOM 元素是非常常见的。React 有内置支持

首先, 声明一个初始值为 nullref 对象:

import {useRef} from 'react';

function MyComponent() {
  const inputRef = useRef(null);
}

然后,将 ref 对象作为 ref 属性传递给你想操控的 DOM 节点的 JSX:

return <input ref={inputRef}/>;

React 创建完 DOM 节点和将其渲染到页面之后,React 会将 ref 对象的 current 属性设置到该 DOM 节点。 然后就可以进入 <input/>DOM 节点中调用 focus() 之类的方法:

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

当节点从页面中移除时,React 会将 current 属性设置为 null

beta.reactjs.org/learn/manip… - 使用 refs 操作 DOM

ref 操作 DOM 的例子

1 Focusing a text input

点击 按钮 会自动聚焦

import {useRef} from 'react';

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

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

  return (
    <>
      <input ref={inputRef}/>
      <button onClick={handleClick}>Focus the input</button>
    </>
  )
}
2 Scrolling an image into view

点击 按钮 会将图片滑动到可视区。在 DOM节点中设置了 list ref,然后调用 DOM querySelectorAll API 定位我们想要滑动的图片

import {useRef} from 'react';

export default function CatFriends() {
  const listRef = useRef(null);

  function scrollToIndex(index) {
    const listNode = listRef.current;
    // This lines assumes a particular DOM structure:
    const imgNode = listNode.querySelectorAll('li > img')[index];
    imgNode.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center',
    })
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToIndex(0)}>
          Tom
        </button>
        <button onClick={() => scrollToIndex(1)}>
          Maru
        </button>
        <button onClick={() => scrollToIndex(2)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul ref={listRef}>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
            />
          </li>
        </ul>
      </div>
    </>
  );
}
3 Playing and pausing a video

使用 ref 调用 <video> DOM 节点 的 play()pause()

import {useState, useRef} from 'react';

export default function VideoPlayer() {
  const [isPlaying, setIsPlaying] = useState(false);
  const ref = useRef(null);

  function handleClick() {
    const nextIsPlaying = !isPlaying;
    setIsPlaying(nextIsPlaying);

    if(nextIsPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }

  return (
    <>
      <button onClick={handleClick}>{isPlaying ? 'Pause' : 'Play'}</button>
      <video 
       width='250'
       ref={ref}
       onPlay={() => setIsPlaying(true)}
       onPause={() => setIsPlaying(false)}
      >
        <source
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          type="video/mp4"
        />
      </video>
    </>
  )
}

避免重复创建 ref

React 只会保存一次 ref 的初始值 且在下一次渲染中忽略:

function Video() {
  const playerRef = useRef(new VideoPlayer());
}

尽管 new VideoPlayer() 的结果只会用于首次渲染,但在每次渲染中都会调用这个函数。这是一种浪费,因为它正在创建不必要的对象

下面这种初始化 ref 的方式可以解决上面的问题:

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

通常,在渲染过程中写入或读取 ref.current 是不允许的。然而,在这种情况下是可以的,因为结果总是一样的,而且这个条件只在初始化过程中执行,所以它是完全可以预测的

如何避免 useRef 初始化后的 null 检查

如果你使用类型校验但是不想它检查 null 值,你可以尝试使用以下这种方式:

function Video() {
  const playerRef = useRef(null);

  function getPlayer() {
    if(playerRef.current !== null) {
      return playerRef.current;
    }
    const player = new VideoPlayer();
    playerRef.current = player;
    return player;
  }
}

如此,playerRef 就可以为 null。但是此时需要让类型校验判定 getPlayer() 的返回值不可能为 null,那就可以在事件处理函数中使用 getPlayer()


疑问解答

不能在自定义组件中获取 ref

如果你像下面这样传递 ref 到组件中:

const inputRef = useRef(null);

return <MyInput ref={inputRef} />

也许你会在控制台中收到一个警告:

Warning: 
Function components cannot be given refs.
Attempts to access this ref will fail.
Did you mean to use React.forwardRef() ?

默认情况下,你的组件不会暴露 refs 到组件内部的 DOM 节点

解决上述问题,可以找到你需要传递 ref 值的组件:

export default function MyInput({value, onChange}) {
  return (
    <input
      value={value}
      onChange={onChange}
    />
  )
}

使用 forwardRef 包裹这个组件:

beta.reactjs.org/reference/r… - forwardRef

import {forwardRef} from 'react';

const MyInput = forwardRef(({value, onChange}, ref)=>{
  return (
    <input 
      value={value}
      onChange={onChange}
      ref={ref}
    />
  )
})

export default MyInput;

如此,父组件就可以获取到此组件的 ref

我有话说

今天花了一天时间看 React 新官网的 useRef 的讲解,理解深入了一丢丢,本人水平有限,要是有错误,欢迎并感谢看官大佬们指出