应用 useRef
有段时间了,最近梳理了 useRef
的应用细节。
一、动机
- 函数组件拜访 DOM 元素;
- 函数组件拜访之前渲染变量。
函数组件每次渲染都会被执行,函数外部的局部变量个别会从新创立,利用 useRef
能够拜访上次渲染的变量,相似 类组件的实例变量
成果。
1.2 函数组件应用 createRef
不行吗?
createRef
次要解决 class
组件拜访 DOM 元素问题,并且最佳实际是在组件周期内只创立一次(个别在构造函数里调用)。如果在函数组件内应用 createRef
会造成每次 render
都会调用createRef
:
function WithCreateRef() {const [minus, setMinus] = useState(0);
// 每次 render 都会从新创立 `ref`
const ref = React.createRef(null);
const handleClick = () => {setMinus(minus + 1);
};
// 这里每次都是 `null`
console.log(`ref.current=${ref.current}`)
useEffect(() => {console.log(`denp[minus]>`, ref.current && ref.current.innerText);
}, [minus]);
return (
<div className="App">
<h1 ref={ref}>Num: {minus}</h1>
<button onClick={handleClick}>Add</button>
</div>
);
}
二、应用
2.1 根本语法
见文档
- 每次渲染
useRef
返回值都不变; ref.current
发生变化并不会造成re-render
;ref.current
发生变化应该作为Side Effect
(因为它会影响下次渲染),所以不应该在render
阶段更新current
属性。
2.2 不能够
在render
里更新 ref.current
值
在 Is there something like instance variables 提到:
Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.
在 render
里更新 refs
导致什么问题呢?
在异步渲染里 render
阶段可能会屡次执行。
const RenderCounter = () => {const counter = useRef(0);
// counter.current 的值可能减少不止一次
counter.current = counter.current + 1;
return (<h1>{`The component has been re-rendered ${counter.current} times`}</h1>
);
}
2.3 能够
在render
里更新 ref.current
值
同样也是在 Is there something like instance variables 提到的:
Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.
为啥 lazy initialization
却能够在 render
里更新 ref.current
值?
这个跟 useRef
懒初始化的实现计划无关。
const instance = React.useRef(null)
if (instance.current == null) {
instance.current = {// whatever you need}
}
实质上只有保障每次 render
不会造成意外成果,都能够在 render 阶段
更新 ref.current
。但最好别这样,容易造成问题,useRef
懒初始化毕竟是个非凡的例外。
2.4 ref.current
不能够
作为其余 hooks(useMemo
, useCallback
, useEffect
)依赖项
ref.current
的值产生变更并不会造成 re-render
, Reactjs 并不会跟踪ref.current
的变动。
function Minus() {const [minus, setMinus] = useState(0);
const ref = useRef(null);
const handleClick = () => {setMinus(minus + 1);
};
console.log(`ref.current=${ref.current && ref.current.innerText}`)
// #1 uesEffect
useEffect(() => {console.log(`denp[ref.current] >`, ref.current && ref.current.innerText);
}, [ref.current]);
// #2 uesEffect
useEffect(() => {console.log(`denp[minus]>`, ref.current && ref.current.innerText);
}, [minus]);
return (
<div className="App">
<h1 ref={ref}>Num: {minus}</h1>
<button onClick={handleClick}>Add</button>
</div>
);
}
本例子中当点击 [Add] 按钮两次后 #1 uesEffect
就不会再执行了,如图:
起因剖析:
依赖项判断是在 render
阶段判断的,产生在在 ref.current
更新之前,而 useEffect
的 effect 函数执行在渲染之后。
-
第一次执行:
首次无脑执行,所以输入:ref.current=null denp[ref.current] > Num: 0 denp[minus]> Num: 0
并且此时
ref.current
为null
,所以#1 uesEffect
相当于useEffect(() => console.log('num 1'), [null])
-
点击 [Add],第二次执行:
此时ref.current
值为<h1>Num: 0<h1>
,所以#1 uesEffect
的依赖项发生变化,最终输入:ref.current=Num: 0 denp[ref.current] > Num: 1 denp[minus]> Num: 1
此时
#1 uesEffect
相当于useEffect(() => console.log('num 1'), [<h1>Num: 0<h1>])
-
点击 [Add],第三次执行:
此时ref.current
值为<h1>Num: 1<h1>
,所以#1 uesEffect
的依赖项 没有 发生变化,故#1 uesEffect
的 effect 函数不会被执行,最终输入:ref.current=Num: 1 denp[minus]> Num: 2
如果将 ref.current
作为依赖项,eslint-plugin-react-hooks
也会报警提醒的:
React Hook useEffect has an unnecessary dependency: ‘ref.current’. Either exclude it or remove the dependency array. Mutable values like ‘ref.current’ aren’t valid dependencies because mutating them doesn’t re-render the component react-hooks/exhaustive-deps
2.5 ref
作为其余 hooks(useMemo
, useCallback
, useEffect
)依赖项
ref
是不变的,没必要作为其余 hooks 依赖。
三、原理
实质上是记忆 hook,但也可作为 data hook,能够简略的用 useState
模仿useRef
:
const useRef = (initialValue) => {const [ref] = useState({current: initialValue});
return ref
}
参考
整顿自 gitHub 笔记:useRef