当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是不是安全?
Posted
技术标签:
【中文标题】当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是不是安全?【英文标题】:Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是否安全? 【发布时间】:2021-06-14 04:33:23 【问题描述】:我知道 ref 是一个可变容器,因此它不应该列在 useEffect
的依赖项中,但是 ref.current
可能是一个变化的值。
当一个 ref 用于存储像 <div ref=ref>
这样的 DOM 元素时,并且当我开发一个依赖于该元素的自定义钩子时,假设 ref.current
可以随着时间的推移而改变,如果组件有条件地返回:
const Foo = (inline) =>
const ref = useRef(null);
return inline ? <span ref=ref /> : <div ref=ref />;
;
我的自定义效果接收ref
对象并使用ref.current
作为依赖项是否安全?
const useFoo = ref =>
useEffect(
() =>
const element = ref.current;
// Maybe observe the resize of element
,
[ref.current]
);
;
我读过 this comment 说 ref 应该在 useEffect
中使用,但我无法弄清楚 ref.current
更改但不会触发效果的任何情况。
正如那个问题所建议的,我应该使用回调 ref,但是作为参数的 ref 对集成多个钩子非常友好:
const ref = useRef(null);
useFoo(ref);
useBar(ref);
虽然回调 ref 更难使用,因为强制用户编写它们:
const fooRef = useFoo();
const barRef = useBar();
const ref = element =>
fooRef(element);
barRef(element);
;
<div ref=ref />
这就是为什么我要问在useEffect
中使用ref.current
是否安全。
【问题讨论】:
【参考方案1】:这是不安全的,因为改变引用不会触发渲染,因此,不会触发useEffect
。
React Hook useEffect 有一个不必要的依赖:'ref.current'。 排除它或删除依赖数组。可变值,例如 'ref.current' 不是有效的依赖项,因为改变它们不会 重新渲染组件。 (react-hooks/exhaustive-deps)
反模式示例:
const Foo = () =>
const [, render] = useReducer(p => !p, false);
const ref = useRef(0);
const onClickRender = () =>
ref.current += 1;
render();
;
const onClickNoRender = () =>
ref.current += 1;
;
useEffect(() =>
console.log('ref changed');
, [ref.current]);
return (
<>
<button onClick=onClickRender>Render</button>
<button onClick=onClickNoRender>No Render</button>
</>
);
;
与此模式相关的现实生活用例是当我们希望有一个持久引用时,即使在元素卸载时。
查看下一个示例,在该示例中,我们无法在卸载时保留元素大小。我们将尝试将useRef
与useEffect
组合使用,但它不起作用。
// BAD EXAMPLE, SEE SOLUTION BELOW
const Component = () =>
const ref = useRef();
const [isMounted, toggle] = useReducer((p) => !p, true);
const [elementRect, setElementRect] = useState();
useEffect(() =>
console.log(ref.current);
setElementRect(ref.current?.getBoundingClientRect());
, [ref.current]);
return (
<>
isMounted && <div ref=ref>Example</div>
<button onClick=toggle>Toggle</button>
<pre>JSON.stringify(elementRect, null, 2)</pre>
</>
);
;
令人惊讶的是,要修复它,我们需要直接处理 node
,同时使用 useCallback
记忆函数:
// GOOD EXAMPLE
const Component = () =>
const [isMounted, toggle] = useReducer((p) => !p, true);
const [elementRect, setElementRect] = useState();
const handleRect = useCallback((node) =>
setElementRect(node?.getBoundingClientRect());
, []);
return (
<>
isMounted && <div ref=handleRect>Example</div>
<button onClick=toggle>Toggle</button>
<pre>JSON.stringify(elementRect, null, 2)</pre>
</>
);
;
查看 React Docs 中的另一个示例:How can I measure a DOM node?
进一步阅读和更多示例参见uses of useEffect
【讨论】:
如果将 ref 传递给 DOM 元素,我认为每个 DOM 更改都是由渲染引起的,并且应该在提交渲染后触发效果,从比较ref.current
开始,是否可能当它仅用于引用 DOM 时仍然会错过一些 ref 更新?
我没有一个例子说明 DOM 更改不会触发渲染,所以我想知道为什么在 useEffect 中使用 DOM ref.current 是不安全的
@otakustay 如果您在 DOM 上进行中继更改,为什么要使用 dep 数组?在没有 dep 数组的情况下将逻辑放在 useEffect 中。 ref.current 作为依赖项是无用的,如答案中所述
因为 ref 可以从 div 更改为 span,或者在多个渲染中从 null 更改为 div,但是空的 deps 数组只能在第一个挂载的 DOM 元素上运行一次效果。
@Slbox 这就像转发 ref 的正常模式,没关系。【参考方案2】:
2021 年答案:
这篇文章解释了使用 refs 和 useEffect
:Ref objects inside useEffect Hooks:
如果将 useRef 钩子与跳过渲染的 useEffect 结合使用,则该钩子可能会成为自定义钩子的陷阱。您的第一反应是将 ref.current 添加到 useEffect 的第二个参数,因此一旦 ref 更改它就会更新。 但是 ref 直到组件渲染后才会更新——这意味着,任何跳过渲染的 useEffect 在下一次渲染之前都不会看到 ref 的任何更改。
同样如本文所述,官方的 react 文档现已更新为推荐的方法(即使用回调而不是 ref + 效果)。见How can I measure a DOM node?:
function MeasureExample()
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node =>
if (node !== null)
setHeight(node.getBoundingClientRect().height);
, []);
return (
<>
<h1 ref=measuredRef>Hello, world</h1>
<h2>The above header is Math.round(height)px tall</h2>
</>
);
【讨论】:
该死,React 感觉就像在过去写 Java。简单的事情就这么啰嗦了。【参考方案3】:我遇到了同样的问题,我使用 Typescript 创建了一个自定义钩子,并使用 ref 回调创建了一个官方方法。希望对您有所帮助。
export const useRefHeightMeasure = <T extends htmlElement>() =>
const [height, setHeight] = useState(0)
const refCallback = useCallback((node: T) =>
if (node !== null)
setHeight(node.getBoundingClientRect().height)
, [])
return height, refCallback
【讨论】:
【参考方案4】:我遇到了类似的问题,其中我的 ESLint 抱怨 ref.current
在 useCallback
中的使用。我在我的项目中添加了一个自定义钩子来规避这个 eslint 警告。每当 ref 对象发生变化时,它都会切换一个变量以强制重新计算 useCallback
。
import RefObject, useCallback, useRef, useState from "react";
/**
* This hook can be used when using ref inside useCallbacks
*
* Usage
* ```ts
* const [toggle, refCallback, myRef] = useRefWithCallback<HTMLSpanElement>();
* const onClick = useCallback(() =>
if (myRef.current)
myRef.current.scrollIntoView( behavior: "smooth" );
// eslint-disable-next-line react-hooks/exhaustive-deps
, [toggle]);
return (<span ref=refCallback />);
```
* @returns
*/
function useRefWithCallback<T extends HTMLSpanElement | HTMLDivElement | HTMLParagraphElement>(): [
boolean,
(node: any) => void,
RefObject<T>
]
const ref = useRef<T | null>(null);
const [toggle, setToggle] = useState(false);
const refCallback = useCallback(node =>
ref.current = node;
setToggle(val => !val);
, []);
return [toggle, refCallback, ref];
export default useRefWithCallback;
【讨论】:
【参考方案5】:我已经停止使用useRef
,现在只使用一次或两次useState
:
const [myChart, setMyChart] = useState(null)
const [el, setEl] = useState(null)
useEffect(() =>
if (!el)
return
// attach to element
const myChart = echarts.init(el)
setMyChart(myChart)
return () =>
myChart.dispose()
setMyChart(null)
, [el])
useEffect(() =>
if (!myChart)
return
// do things with attached object
myChart.setOption(... data ...)
, [myChart, data])
return <div key='chart' ref=setEl style= width: '100%', height: 1024 />
对图表、身份验证和其他非反应库很有用,因为它保留了一个元素 ref 和初始化的对象,并且可以根据需要直接处理它。
我现在不确定为什么 useRef
首先存在...?
【讨论】:
基于 Dan 在此处的评论:github.com/facebook/react/issues/14387#issuecomment-503616820Refs are for values whose changes don't need to trigger a re-render.
我同意这种说法,在某些情况下您不需要重新渲染,只需要在您的代码例如组件、计数器等。
奇怪,return <div ref=ref>
中的ref
不打算与useRef
一起使用吗?因为我确实想在分配了div
元素后触发块的执行,并且触发通常使用useEffect
完成,就像问题中一样。
引用更改不会触发重新渲染。有可能您的某些组件触发了重新渲染,并且您的 ref 也已更改并被您的 useEffect
捕获。
但是我们确实希望在分配 DOM 元素后捕获这些案例。这就是为什么useRef
在大多数情况下没有那么有用,以及为什么我认为它不应该连接到元素的ref=ref
。根据文档,推荐的方法似乎是使用“回调参考”(参见 Gyum Fox 的回答)。我在想useState
也可以工作而且更简单。以上是关于当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章