forwardRef()
建议您在学习完 useRef() 之后再阅读此章节。
什么是 forwardRef()?
forwardRef() 是一个内建函数,用于「转发」组件的参考到指定目标上。更明确的说,他是用来改变 ref 属性套用在子组件时的默认目标。
forwardRef<T, P>() 中有两个泛型型别;T 是要暴露给父组件的值的型别 (也就是父组件中 useRef<T> 的 T),P 是子组件属性的型别。
范例
forwardRef() 对于在子函数组件上使用 ref 属性是不可或缺的。与 ref 属性被应用在在类别组件时不同的是,我们无法光凭 forwardRef() 来获取函数组件的实体。我们最多只能取得某个 DOM 节点的实体,或是将参考传递给更深层的组件。
举例来说,如果我们有这样一个组件:
import { useRef } from 'react'
interface IInputGroupProps {
label: string
}
export const InputGroup = ({ label }: IInputGroupProps) => {
return (
<div>
<label>{label}</label>
<input />
</div>
)
}
在父组件中,我们可能会这样使用它:
import { InputGroup } from './InputGroup'
export const Parent = () => {
return (
<div>
<InputGroup label="First Name" />
<InputGroup label="Last Name" />
</div>
)
}
结果就会像是这个样子:
目前一切都运作良好,但是我们现在被要求增加一个新的功能-在某个父组件的按钮被点击时,我们要聚焦 (focus) 在 "Last Name" 的输入框上。由于 <input> 标签被放在子组件中,似乎没有优雅的方式可以达成这个目的。
这就是 forwardRef() 有用的地方。它可以让 ref 属性也能在函数组件上运作,并且转发参考的对象至 InputGroup 中的 <input> 上。例如:
import { forwardRef } from 'react'
interface IInputGroupProps {
label: string
}
export const InputGroup = forwardRef<HTMLInputElement, IInputGroupProps>(
({ label }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} />
</div>
)
}
)
如您所见,ref 并不会被放在属性 (props) 之中;相反地,它被放在 forwardRef() 的第二个参数中供我们使用。在将 ref 绑定到 <input> 身上之后,我们终于可以从父组件使用参考取得子组件 <input> 的实体:
import { useRef } from 'react'
import { InputGroup } from './InputGroup'
export const Parent = () => {
const lastNameInput = useRef<HTMLInputElement>(null)
const focusLastNameInput = () => {
lastNameInput.current?.focus()
}
return (
<div>
<InputGroup label="First Name" />
<InputGroup
ref={lastNameInput}
label="Last Name"
/>
<button onClick={focusLastNameInput}>
Focus Last Name Input
</button>
</div>
)
}
forwardRef() 能用在类别组件身上吗?
可以,但是我们不建议这么做;为了让他动起来,一些怪招数是无法避免的。举例来说:
import { Component, forwardRef } from 'react'
interface IInputGroupProps {
label: string
}
interface IInputGroupState {}
export const InputGroup = forwardRef<HTMLInputElement, IInputGroupProps>(
(props, ref) => {
class MyComponent extends Component<IInputGroupProps, IInputGroupState> {
render() {
return (
<div>
<label>{this.props.label}</label>
<input ref={ref} />
</div>
)
}
}
return <MyComponent {...props} />
}
)
为了取得 forwardRef() 中的 ref 并在类别组件中使用,我们得将类别组件定义在 forwardRef() 之中 (或是做差不多的事情)。
此外,在这个范例中,由于 MyComponent (它是一个组件) 被定义在 InputGroup 中 (也是一个组件),每次 InputGroup 重新渲染,MyComponent 就会被重新定义;代表「旧的」<MyComponent {...props} /> 会被卸载,「新的」<MyComponent {...props} /> 会被挂载,导致我们失去 MyComponent 中所有的状态。
要解决这个问题,最简单的解决方法就是在第一次渲染之前将 MyComponent 的定义记下来,并且从那时起只使用它来进行渲染。例如:
import { Component, forwardRef } from 'react'
let MemoizedComponent: Component
export const InputGroup = forwardRef(
(props, ref) => {
class MyComponent extends Component {
// ...
}
if (!MemoizedComponent) {
MemoizedComponent = MyComponent
}
return <MemoizedComponent {...props} />
}
)
总而言之,为了让事情变得更简单,我们建议使用类别组件内建的 ref 就好了!
useImperativeHandle()
虽然他的名字听起来好像和事件监听或是拖拉功能有关,但其实一点关系也没有。useImperativeHandle() 是一个内建的钩子 (hook),用于改变子组件的 ref 属性暴露给父组件的值;这个钩子必须和 forwardRef() 一起使用 (因为那是唯一一个能在子组件取得 ref 属性值的方法)。
useImperativeHandle()中有三个参数,分别为:- 从父组件传递下来的
ref属性;也就是forwardRef()的第二个参数。 - 一个用于暴露数值给父组件的函数。
- 一个非必要的依赖值阵列
dependencies,用于决定被暴露的数值何时该被重新计算。类似于useEffect(),dependencies的默认值为undefined,代表被暴露的数值会在组件重新渲染时重新计算。
- 从父组件传递下来的
useImperativeHandle<T, R extends T>()中有两个泛型别;T是参考的型别 (就是父组件中useRef<T>的T),R则是被暴露的值的型别,必须扩展 (extends)T。
useImperativeHandle() 的运作方式就像是把ref「拦截」下来,并返回任何我们想要曝光给父组件的值。
useImperativeHandle() 范例
在 useImperativeHandle() 的帮助下,我们现在能从父组件调用定义在子组件中的方法,就像类别组件的 ref 属性那样。
我们必须在强调一次,这个作法只该在标准的属性/状态无法达成您的需求,或是标准的属性/状态不便使用时才被使用。下方是我们在 useRef() 章节中提到的其中一个范例,但是使用函数组件的写法。
import { useRef } from 'react'
import { Child, IChild } from './Child'
export const Parent = () => {
const child = useRef<IChild>(null)
const makeChilGetOld = () => {
child.current?.getOld()
}
return (
<div>
<Child ref={child} />
<button onClick={makeChilGetOld}>
Make Child Get Old
</button>
</div>
)
}
import { forwardRef, useImperativeHandle, useState } from 'react'
export interface IChild {
getOld: () => void
}
export const Child = forwardRef<IChild>((props, ref) => {
const [age, setAge] = useState(5)
const getOld = () => {
setAge((prev) => prev + 1)
}
useImperativeHandle(ref, () => ({ getOld }), [])
return (
<h1>Hello, I am {age} years old</h1>
)
})