发布于 2026-01-06 1 阅读
0

React Hooks 系列:useRef 第三部分 - useRef

React Hooks 系列:useRef

第三部分 - useRef

介绍

请务必先查看我的定时器代码沙箱。您可以随意使用定时器,fork 这个沙箱,查看代码,甚至可以重构代码使其更好!

我的 React Hooks 系列前两篇文章分别讲解了 useState 和 useEffect。本文将重点介绍 useRef,这是我最喜欢的 Hook 之一。我坦白承认,我绝非 useRef 专家,本文仅介绍我如何在 Timer 示例中实现 useRef Hook。

短暂绕道

我们来讨论一下为什么我的计时器应用需要 useRef hook。

这与按钮及其行为有关PAUSE。最初,我的暂停功能没有绑定 useRef。当用户尝试暂停时,经常会出现延迟,而且计时器还会再倒计时一秒。

我们应该研究这种特定行为,因为我们还可以更好地了解 useEffect 和 setTimeout。

提醒一下,PAUSE当两个start === trueANDcounter都不完全相等时,我会有条件地渲染按钮0

{
   start === true && counter !== 0
   ? 
   <button style={{fontSize: "1.5rem"}} onClick={handlePause}>PAUSE</button> 
   : 
   null 
}
Enter fullscreen mode Exit fullscreen mode

换句话说,计时器运行时,暂停按钮会显示出来。

const handlePause = () => {
    setStart(false)
}
Enter fullscreen mode Exit fullscreen mode

如您所见,handlePause设置startfalse暂停按钮消失(显示为 null),开始按钮则显示在暂停按钮的位置。

状态start已从 true 变为 false,触发了我们的第一个 useEffect(pauseTimer.current暂时忽略它):

 useEffect(() => {
      if (start === true) {
        pauseTimer.current = counter > 0 && setTimeout(() => setCounter(counter - 1), 1000)
      }
      return () => {
        clearTimeout(pauseTimer.current)
      }
  }, [start, counter, setCounter])
Enter fullscreen mode Exit fullscreen mode

当用户按下按钮时PAUSE,useEffect 会检查是否已关闭start === true(现在不再检查了),但之前渲染的 setTimeout 仍在运行,直到 useEffect 确定已start关闭true才会再次运行 setTimeout。然而,延迟的出现是因为之前的 setTimeout 已经运行完毕。到那时往往为时已晚,又过了一秒钟。

想亲眼看看这个现象吗?打开Timer CodeSandbox,删除pauseTimer.current =第 19 行的代码,运行计时器并尝试暂停几次。你会发现计时器不会立即暂停。

既然我们已经了解了问题所在,我们就可以解决它了!

使用 useRef 钩子来解决问题!

第三部分 - useRef

理解 useRef 可能需要一些时间。我知道我当初也是这样。首先,让我们看看React 文档是怎么说的:

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

你说什么?

如果你不确定这些话是什么意思,你并不孤单!

我发现Lee Warrick写的这篇博客文章很有帮助,特别是他对 useRef 的解释:

refs 存在于重新渲染周期之外。
你可以把 refs 想象成一个你暂时设置好的变量。当你的组件重新运行时,它会忽略这个 ref,直到你在某个地方使用 `.current` 调用它为止。

男人头顶上的灯泡亮了起来。

那一刻我恍然大悟。ref 是一个可以基于状态中的对象定义的变量,即使状态发生变化,它也不会受到影响。它会一直保持其值,直到你告诉它执行其他操作为止!

让我们在计时器应用程序中看看它的实际效果。

将 useRef 添加到我们的 React 导入中:

import React, { useState, useEffect, useRef } from "react";
Enter fullscreen mode Exit fullscreen mode

文档原文:

const refContainer = useRef(initialValue);

定义一个对象实例以便稍后“引用”。

我们的长这样:

const pauseTimer = useRef(null)
Enter fullscreen mode Exit fullscreen mode

务必给它起一个有意义的名字,尤其是在使用多个 useRefs 时。我的 useRefs 之所以叫这个名字,是pauseTimer因为这就是我希望它在被调用时执行的操作。`is`null是我的初始值,useRef()因为在我的函数中,`is` 的初始状态并不重要pauseTimer。我们只关心计时器开始倒计时后对 `pauseTimer` 的引用是什么。

pauseTimer是一个具有属性的对象current。通过 useRef 创建的每个 ref 都将是一个具有属性的对象currentpauseTimer.current将是一个我们可以设置的值。

让我们再来看一下 useEffect,这次特别注意一下pauseTimer.current。这里我们将条件(counter大于0?)setTimeout 的值设置为pauseTimer.current。这样我们就可以在任何地方访问 setTimeout 的值了!

useEffect(() => {
   if (start === true) {
     pauseTimer.current = counter > 0 && setTimeout(() => 
   setCounter(counter - 1), 1000)
   }
   return () => {
     clearTimeout(pauseTimer.current)
   }
}, [start, counter, setCounter])
Enter fullscreen mode Exit fullscreen mode

从这里开始就很简单了。当用户选择PAUSE“立即”时,start更新会执行useEffect 函数false,但 useEffect 无法运行 setTimeout 函数,因此会运行清理函数:

return () => {
     clearTimeout(pauseTimer.current)
}
Enter fullscreen mode Exit fullscreen mode

如果我们不在pauseTimer.currentclearTimeout 中使用 setTimeout,计时器会像以前一样继续运行一秒钟,因为if (start === true)即使我们提前将 setTimeout 设置startfalse一秒,条件块中的 setTimeout 也会运行完整个周期。

但是!由于我们pauseTimer.current在 clearTimeout 中引用了当前的 setTimeout 值,useEffect 将跳过该引用if (start === true),立即运行其清理函数,从而阻止 setTimeout 的执行!

这就是 useRef 的强大之处!它允许你在任何地方访问对某个值的引用(你甚至可以将它们从父级传递给子级!),而且这些引用不会改变,直到你告诉它改变为止(就像我们每秒更新一次计时器一样)。

奖金

这只是 useRef 的冰山一角。您可能更熟悉 useRef 以及如何与 DOM 元素交互。

在我的作品集网站中,useRef 决定了我如何打开和关闭动画导航屏幕。

在我的组件函数 SideNavBar 中:

我定义我的参考

const navRef = useRef()
Enter fullscreen mode Exit fullscreen mode

创建用于关闭和打开导航的函数

function openNav() {
    navRef.current.style.width = "100%"
}

function closeNav() {
    navRef.current.style.width = "0%"
}
Enter fullscreen mode Exit fullscreen mode

并将 Reactref属性设置divnavRef

<div id="mySidenav" className="sidenav" ref={navRef}>
Enter fullscreen mode Exit fullscreen mode

我的 CSS 文件中包含sidenav该类

.sidenav {
  height: 100%;
  width: 0;
  position: fixed;
  z-index: 2;
  top: 0;
  left: 0;
  background-color: #212121;
  overflow-x: hidden;
  transition: 0.6s;
  padding-top: 5rem;
}
Enter fullscreen mode Exit fullscreen mode

很酷吧?

navRef它与 DOM 元素交互,div className="sidenav"因为它具有该属性ref={navRef},并且当openNav()调用时,navRef.current.style.width会被更新为"100%"

反之亦然,当调用“closeNav()”时也是如此。

总结

希望您喜欢我的 React Hooks 系列文章的第三篇!如果您已经读到这里,首先……

罗恩·斯旺森真的为你感到骄傲 gif

其次

Fez正在冲浪,竖起大拇指,谢谢!

我计划继续推出 React Hooks 系列文章。我可能会讲解同一个 Hooks 的不同方面,也可能探索全新的 Hooks。敬请期待!一如既往,再次感谢大家的支持。有人愿意阅读我的文章,对我来说意义非凡。

请留下评论、反馈或更正。我确信我遗漏了某些内容,或者可能对某些概念的解释有误。如果您发现任何问题,请告诉我!我这样做也是为了自我学习。

下次再见……

祝您编程愉快!

文章来源:https://dev.to/jamesncox/react-hooks-series-useref-27mk