首页与我联系

「React Hooks 学习笔记」关于 useRef 的使用介绍(五)

By 前端达人
Published in 4-React
October 04, 2022
1 min read
「React Hooks 学习笔记」关于 useRef 的使用介绍(五)

大家好,前几篇文章我们一起学习了 State HookuseEffect HookReact.memouseMemouseCallback ,本篇文章我们来继续学习另外一个很有用的钩子函数 useRef ,简单的来说,使用这个钩子函数,可以在不 re-render 的状态下更新值,获取 DOM 进而控制 DOM 的行为(Imperative)。

一、什么是 useRef

首先我们来看下官方是怎么解释的,什么是 useRef ?

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.*

1_FyqUtvPiu8Chn_hB7CL13g.jpg

useState不同,useState会回一个包含值的数组,第一个值是state的,第二个值是更新state的函数。每次更新、renderCount改变,就会触发重新render。

const [renderCount, setRenderCount] = useState(0);

useRef 传递一个值并返回一个对象,这个值更新,不会再触发组件重新渲染。

const renderCount = useRef(0);  
// renderCount = { current: 0 }

二、访问DOM元素

在介绍之前,我们先来快速回顾下,在原生 JS 的环境下,如何获取指定的 DOM 元素,并进行操作示例代码如下:

<body>
  <h1 id="title"></h1>
</body>
<script>
  const el = document.getElementById('#title')
  el.textContent = "Hello World"
</script>

在 React 中,我们也可以使用 State Hook 完成上述的功能

const Title = () => {
  const [title, setTitle] = useState("")
  useEffect(() => { setTitle("Hello World") }, [])
  return <h1>{title}</h1>
}

这样操作 DOM 更加抽象,有利于我们更加专注的业务,但是问题来了,我们仍然能像过去一样,类似 ID 的方式获取 DOM 元素吗,答案是肯定的。

接下来我们来看看使用 useRef 会输出啥?我们来做个简单的示例,一个输入框和提交按钮,如下图所示:

userefinputdemo.png

示例代码如下:

import React, { useRef } from 'react';

// preserves value
// DOES NOT trigger re-render
// target DOM nodes/elements

const UseRefBasics = () => {
  const refContainer = useRef(null);
  const handleSubmit= (e)=>{
    e.preventDefault();
    console.log(refContainer);
    console.log(refContainer.current)
  }
  return(
      <>
        <form className='form' onSubmit={handleSubmit}>
          <div>
            <input type='text' ref={refContainer} />
          </div>
          <button type='submit'>submit</button>
        </form>
      </>
  );
};

export default UseRefBasics;

接下来我们在输入框里输入 test,猜猜控制台会输出啥:

userefconsole.png

从上图可以看出,我们通过 refContainer.current 获取到了当前的 DOM 元素了。接下来我们做个简单的应用,页面加载时,设置当前输入框的默认状态为 focus 。我们只需要添加一个useEffect 方法即可,示例代码如下:

useEffect(() => {
    console.log(refContainer.current);
    refContainer.current.focus();
  });

三、聊一聊 useRef 应用场景

了解完什么是 useRef 后,我们来聊聊其存在的意义,在项目中如何应用。

3.1、记录组件渲染了几次

有的同学可能会想到用 state 来记录,但是 state 好用吗?我们来看看下段代码:

const App = () => {
  const [name, setName] = useState('');
  const [renderCount, setRenderCount] = useState(0);
  
  useEffect(() => {
    setRenderCount(prev => prev + 1)
  })
  return <p>renderCount</p>
}

聪明的大家应该知道这会造成无限循环,因为setRenderCount更新了renderCount所以会触发重新渲染,然后再重新调用setRenderCount又更新 renderCount……(不断循环)

这时候使用 useRef 就派上用场了,因为用它不会重新渲染组件。修改后的代码如下:

const App = () => {
  const renderCount = useRef(0);  // { current: 0 }
  
  useEffect(() => {
    renderCount.current += 1;
  })
return <p>renderCount.current</p>
}

3.2、控制子组件的 Ref

由于 ref 基本上是一个对象,因此它可以作为 prop 传递给子组件。因此,传入的 ref 对象可以附加到子元素中的 DOM 元素,我们先声明一个 Child 组件,示例代码如下:

const Child = ({ childRef }) => {
  return <input ref={childRef} />
}

接下来我们在父组件里进行调用,就能控制操作子组件的DOM,示例代码如下:

const Title = () => {
  const ref = useRef()
  const onClick = () => {
    ref.current.focus()
  }
  return (
    <>
      <Child childRef={ref} />
      <button onClick={onClick}>focus</button>
    </>
  )
}

3.3、在空白处点击关闭菜单

假设您有一个组件,如下图所示,我们希望在用户单击菜单之外的任何位置时将其关闭,这是弹出菜单或弹出层的一个非常流行的功能。

000006.webp

首先我们来定一个 Menu 组件,示例代码如下:

const Menu = () => {
  const [on, setOn] = useState(true)
  if (!on) return null
  return (
    <ul>
      <li>Home</li>
      <li>Price</li>
      <li>Produce</li>
      <li>Support</li>
      <li>About</li>
    </ul>
  )
}

在上述代码中,创建了一个 on 状态并将其初始设置为 true,从而使菜单项列表可见。但是,当我们在此列表之外单击时,我们希望将其设置为 false 以隐藏它。

我们知道如何使用状态处理菜单的显示与隐藏,但我们如何知道用户何时点击了菜单外部的某个地方?我们是否需要知道整个屏幕上所有组件的位置?

我们可以将 ref 附加到 ul 元素,示例代码如下:

const Menu = () => {
  const [on, setOn] = useState(true)
  const ref = useRef()
  if (!on) return null
  return (
    <ul ref={ref}>
      ...
    </ul>
  )
}

除了将 click 事件处理程序附加到一个元素之外,我们还需要监听 mousedown 事件。通过这种方式,我们可以知道用户的点击,无论它是在菜单组件内部还是外部:

const Menu = () => {
  const ref = useRef()
  useEffect(() => {
    const listener = e => { ... }
    window.addEventListener('mousedown', listener)
    return () => {
      window.removeEventListener('mousedown', listener)
    }
  }, [])
  ...
}

接下来我们继续定义 mousedown 鼠标点击事件,我们可以使用 ref 来确定鼠标位置是否包含在 ul 元素的边界内:

const listener = e => {
      if (!ref.current) return
      if (!ref.current.contains(e.target)) {
        setOn(false)
      }
    }

我们通过 ref.current 检查元素是否已挂载,如果未挂载直接返回不做操作,然后通过 e.target 检查鼠标下的元素是否是 ul 元素的子元素。如果用户点击了 ul 中的任何子项,那么我们就知道他们点击了菜单。如果没有,我们知道用户点击了外部,然后我们将 on 状态设置为 false,从而关闭菜单。

简而言之,在 ref 的帮助下,我们可以调用 contains 函数来找出一个元素是否在另一个元素内。

3.4、获取上一个数据状态的值

因为组件的每次渲染,数据状态值都是最新的,不会保存以前的值,这时我们可以使用useRef 记录以前的值,示例代码如下:

const App = () => {
  const previousName = useRef('');
  
  useEffect(() => {
    previousName.current = name
  }, [name])
return 
  <>
    <p> My previous name is {previousName.current} </p>
  </>
}

3.5、计时器

import { useRef, useState, useEffect } from 'react';
function Stopwatch() {
  const timerIdRef = useRef(0);
  const [count, setCount] = useState(0);
  const startHandler = () => {
    if (timerIdRef.current) { return; }
    timerIdRef.current = setInterval(() => setCount(c => c+1), 1000);
  };
  const stopHandler = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = 0;
  };
  useEffect(() => {
    return () => clearInterval(timerIdRef.current);
  }, []);
  return (
    <div>
      <div>Timer: {count}s</div>
      <div>
        <button onClick={startHandler}>Start</button>
        <button onClick={stopHandler}>Stop</button>
      </div>
    </div>
  );
}

startHandler() 函数,当单击 Start 按钮时调用,启动计时器并将计时器 id 保存在引用 timerIdRef.current = setInterval(…) 中。 要停止秒表,用户单击停止按钮。停止按钮处理程序 stopHandler() 从引用中访问计时器 id 并停止计时器 clearInterval(timerIdRef.current)。 此外,如果组件在秒表处于活动状态的情况下卸载,则 useEffect() 的清理功能也会停止计时器。 在秒表示例中,ref 用于存储计时数据 - 活动计时器 ID。

四、可以用 useRef 取代 useState 吗?

不能!因为useRef并不会触发重新渲染,所以你的值useRef存在的,值更新了但是界面不会重新渲染。通过以下示例,我们来做下验证,一个按钮是控制 State 数据状态的,一个按钮是控制 useRef 中的变量,并在界面上分别显示其值,示例代码如下:

const { useState, useRef } = React;

const MyComponent = () => {
  const [stateExp, setStateExp] = useState(1000);
  const refExp = useRef(1000);
  
  const spendStateMoney = () => setStateExp(prevVal => prevVal - 1);
  
  const spendRefMoney = () => {
    refExp.current = refExp.current - 1;
    console.log('refExp.current', refExp.current);
  };
  
  return (
    <section className="box">
      <div className="values">
        <span>useState value: ${stateExp}</span>
        <button onClick={spendStateMoney}>Spend</button>
      </div>
      <div className="values">
        <span>useRef value: ${refExp.current}</span>
        <button onClick={spendRefMoney}>Spend</button>
      </div>
    </section>
  )
}

ReactDOM.render(<MyComponent />, document.getElementById('root'))

我们尝试分别点击按钮,效果如下图所示,点击最下方的按钮,界面不会发生变化,但是控制台输出的 Ref 值发生了变化:

1_jA4nUSawWVbKnkQAyFOgzg.gif

因此通常在需要 manage focus, text selection, or media playback 或者不牵扯界面渲染时,我们会用 useRef 去控制 DOM

总结

以下是关于 useRef() 使用总结

  • useRef() 钩子函数创建 references 变量。
  • 使用初始值调用 const reference = useRef(initialValue) 会返回一个名为 reference 的特殊对象。引用对象有一个属性current,你可以使用这个属性来读取引用值reference.current,或者更新reference.current = newValue。
  • 在组件重新渲染时,useRef() 的值是持久的。
  • 与更新状态相反,更新 ref 不会触发组件重新渲染。
  • useRef() 也可以访问 DOM 元素。将 ref 分配给您要访问的元素的 ref 属性,通过 reference.current 进行访问。

今天的文章就介绍到这里,感谢大家的阅读。

前端达人公众号.jpg

注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载


Tags

reacthook
Previous Article
「React Hooks 学习笔记」关于 useMemo 和 useCallback 的使用介绍(四)
前端达人

前端达人

专注前端知识分享

Table Of Contents

1
一、什么是 useRef ?
2
二、访问DOM元素
3
三、聊一聊 useRef 应用场景
4
四、可以用 useRef 取代 useState 吗?
5
总结

相关文章

「React Hooks 学习笔记」关于Custom Hooks 的使用介绍(八)
October 28, 2022
1 min

前端站点

VUE官网React官网TypeScript官网

公众号:前端达人

前端达人公众号