注意:本文假设你对React中的Refs有一个基本的了解。
尽管 refs 是可变的容器,理论上我们可以在其中存储任意的值,但它们最常被用来获取对 DOM 节点的访问:
1const ref = React.useRef(null)2
3return <input ref={ref} defaultValue="Hello world" />
ref 是内置基元上的一个保留属性,React会在DOM节点被渲染后将其存储在这里。当组件被卸载时,它将被设置为空。
与 refs 交互
对于大多数交互,你不需要访问底层DOM节点,因为React会自动为我们处理更新。一个很好的例子是,你可能需要一个引用,即焦点管理。
Devon Govett有一个很好的RFC,建议在react-dom中加入FocusManagement,但现在React中没有任何东西可以帮助我们做到这一点。
有效果的焦点
那么,现在你如何在一个输入元素渲染后进行聚焦呢?(我知道自动对焦的存在,这只是一个例子。如果这让你感到困扰,想象一下你想用动画来代替节点)。
嗯,我见过的大多数代码都试图这样做:
const ref = React.useRef(null)
React.useEffect(() => {
ref.current?.focus()
}, [])
return <input ref={ref} defaultValue="Hello world" />
这大部分是好的,没有违反任何规则。空的依赖关系数组是可以的,因为里面唯一使用的是ref,这很稳定。linter不会抱怨将其添加到依赖数组中,而且在渲染过程中也不会读取ref(这在React并发功能中可能会很麻烦)。
该效果将在 "加载时 "运行一次(在严格模式下运行两次)。到那时,React已经用DOM节点填充了ref,所以我们可以关注它。
然而,这并不是最好的方法,在一些更高级的情况下确实有一些注意事项。
具体来说,它假定当效果运行时,Ref已经 "填充"。如果它不是可用的,例如,因为你将引用传递给一个自定义组件,该组件将推迟渲染或只在其他用户交互后显示输入,那么当效果运行时,引用的内容仍然是空的,没有东西会被聚焦:
function App() {
const ref = React.useRef(null)
React.useEffect(() => {
// 🚨 ref.current is always null when this runs
ref.current?.focus()
}, [])
return <Form ref={ref} />
}
const Form = React.forwardRef((props, ref) => {
const [show, setShow] = React.useState(false)
return (
<form>
<button type="button" onClick={() => setShow(true)}>
show
</button>
// 🧐 ref is attached to the input, but it's conditionally rendered
// so it won't be filled when the above effect runs
{show && <input ref={ref} />}
</form>
)
})
下面是发生的情况:
- 表单渲染了。
- 输入没有被渲染,引用仍然是空的。
- 效果运行,什么都不做。
- 输入被显示,引用被填充,但不会被聚焦,因为效果不会再次运行。
问题是,效果被 "绑定 "到表单的渲染函数上,而我们实际上想表达的是。"当输入被渲染时聚焦输入",而不是 "当表单被加载时"。
回调引用
这就是回调引用发挥作用的地方。如果你曾经看过Refs的类型声明,我们可以看到我们不仅可以将一个Ref对象传入其中,还可以传入一个函数。
type Ref<T> = RefCallback<T> | RefObject<T> | null
从概念上讲,我喜欢把React元素上的Refs想成是组件渲染后被调用的函数。这个函数获得作为参数传递的已渲染的DOM节点。如果React元素取消了挂载,它将被再次调用,并且是null。
- 因此,从useRef(一个RefObject)向React元素传递一个ref只是语法上的糖:
<input
ref={(node) => {
ref.current = node;
}}
defaultValue="Hello world"
/>
让我再强调一次。
所有的 ref props 都是函数!
而这些函数在渲染后运行,在那里执行副作用是完全没有问题的。如果Ref只是在渲染后被调用,也许会更好。
有了这些知识,是什么阻止了我们将输入集中在回调ref里面,在那里我们可以直接访问节点?
<input
ref={(node) => {
node?.focus()
}}
defaultValue="Hello world"
/>
嗯,有一个小细节。React会在每次渲染后运行这个函数。因此,除非我们对经常集中输入感到满意(我们很可能不满意),否则我们必须告诉React只在我们想要的时候运行这个函数。
useCallback来拯救
幸运的是,React使用参考稳定性来检查是否应该运行回调引用。这意味着如果我们传递相同的参考(erence,双关语)给它,执行将被跳过。
这就是useCallback的作用,因为这就是我们如何确保一个函数不被无谓地创建。也许这就是为什么它们被称为回调引用--因为你必须一直用useCallback来包装它们。😂
这里是最终的解决方案。
callback-ref-with-use-callback
const ref = React.useCallback((node) => {
node?.focus()
}, [])
return <input ref={ref} defaultValue="Hello world" />
与最初的版本相比,它的代码更少,只用了一个钩子而不是两个。而且,它在所有情况下都能工作,因为回调引用被绑定到DOM节点的生命周期,而不是安装它的组件的生命周期。此外,它不会在严格模式下执行两次(在开发环境中运行时),这对许多人来说似乎很重要。
正如(旧的)React文档中的这个隐藏的宝石所示,你可以用它来运行任何形式的副作用,例如在其中调用setState。我就把这个例子留在这里吧,因为它实际上是非常好的。
function MeasureExample() {
const [height, setHeight] = React.useState(0)
const measuredRef = React.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>
</>
)
}
所以请大家注意,如果你需要在DOM节点渲染后直接与之交互,尽量不要直接跳到useRef+useEffect,而是考虑使用回调引用。