大家好,前几篇文章我们一起学习了 State Hook
、useEffect Hook
、React.memo
、useMemo
、useCallback
,本篇文章我们来继续学习另外一个很有用的钩子函数 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.*
跟useState
不同,useState
会回一个包含值的数组,第一个值是state的,第二个值是更新state的函数。每次更新、renderCount改变,就会触发重新render。
const [renderCount, setRenderCount] = useState(0);
useRef
传递一个值并返回一个对象,这个值更新,不会再触发组件重新渲染。
const renderCount = useRef(0); // renderCount = { current: 0 }
在介绍之前,我们先来快速回顾下,在原生 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
会输出啥?我们来做个简单的示例,一个输入框和提交按钮,如下图所示:
示例代码如下:
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,猜猜控制台会输出啥:
从上图可以看出,我们通过 refContainer.current
获取到了当前的 DOM 元素了。接下来我们做个简单的应用,页面加载时,设置当前输入框的默认状态为 focus
。我们只需要添加一个useEffect
方法即可,示例代码如下:
useEffect(() => { console.log(refContainer.current); refContainer.current.focus(); });
useRef
应用场景了解完什么是 useRef
后,我们来聊聊其存在的意义,在项目中如何应用。
有的同学可能会想到用 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> }
由于 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> </> ) }
假设您有一个组件,如下图所示,我们希望在用户单击菜单之外的任何位置时将其关闭,这是弹出菜单或弹出层的一个非常流行的功能。
首先我们来定一个 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 函数来找出一个元素是否在另一个元素内。
因为组件的每次渲染,数据状态值都是最新的,不会保存以前的值,这时我们可以使用useRef 记录以前的值,示例代码如下:
const App = () => { const previousName = useRef(''); useEffect(() => { previousName.current = name }, [name]) return <> <p> My previous name is {previousName.current} </p> </> }
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 值发生了变化:
因此通常在需要 manage focus, text selection, or media playback 或者不牵扯界面渲染时,我们会用 useRef 去控制 DOM
以下是关于 useRef() 使用总结
今天的文章就介绍到这里,感谢大家的阅读。
注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载