转发 Refs
Ref 转发是一种将 React.ref 通过组件自动传递给它的子组件的技术。对于应用中的大部分组件来说,这通常是不必要的。然而,它对某些组件非常有用,尤其是可复用组件库中的组件。下面描述了最常见的应用场景。
为什么要转发 Ref ?
考虑一个渲染原生 button DOM 的 FancyButton 组件:
RES// FancyButton.res
@react.component
let make = (~children) => {
<button className="FancyButton">
children
</button>
}
React 组件隐藏了实现细节,也包括渲染输出。其他使用 FancyButton 的组件通常不需要获取它内部的 button DOM 元素。这很好,因为它可以防止组件过度依赖彼此的 DOM 结构。
这种封装对于类似 FeedStory
或 Comment
的应用级组件来说是可取的,但是对于类似 FancyButton
或 MyTextInput
的高度可复用的“叶子“组件来说可能有点不方便。通常我们倾向于像一个常规 button 和 input DOM 一样使用这些组件,访问它们的 DOM 节点来管理焦点,控制选择或动画,可能是不可避免的。
目前有两种策略来通过组件转发引用。在 ReScript 和 React 中我们强烈推荐将 ref 作为 prop 传递,但也有一个专用 API React.forwardRef
。
下面我们会讨论这两种方法。
通过 Props 转发 Refs
React.ref
能像其他 prop 一样传递。组件会将 ref 转发到正确的 DOM 元素。
不需要学习任何新概念!
下面的例子中,FancyInput
定义了一个 inputRef
prop,会被转发到它的 input
元素:
RES// App.res
module FancyInput = {
@react.component
let make = (~children, ~inputRef: ReactDOM.domRef) =>
<div> <input type_="text" ref=inputRef /> children </div>
}
@send external focus: Dom.element => unit = "focus"
@react.component
let make = () => {
let input = React.useRef(Js.Nullable.null)
let focusInput = () =>
input.current
->Js.Nullable.toOption
->Belt.Option.forEach(input => input->focus)
let onClick = _ => focusInput()
<div>
<FancyInput inputRef={ReactDOM.Ref.domRef(input)}>
<button onClick> {React.string("Click to focus")} </button>
</FancyInput>
</div>
}
我们使用 ReactDOM.domRef
类型来表示 inputRef
。我们可以像将 ref
属性传给 DOM 一样传递 inputRef
(<input ref={ReactDOM.Ref.domRef(myRef)} />
)。
[不推荐] React.forwardRef
注意: 我们不推荐这种方法,因为它可能会在某个时间点消失,而且与前面提到的传递 ref prop 相比,它没有任何明显的优势。
React.forwardRef
提供了一种在自定义组件中“模拟 ref
prop”的方法。在组件内部,组件会把传递的 ref
值转发给目标 DOM 元素。
上面例子的 React.forwarRef
版本:
// App.res
module FancyInput = {
@react.component
let make = React.forwardRef((~className=?, ~children, ref_) =>
<div>
<input
type_="text"
?className
ref=?{Js.Nullable.toOption(ref_)->Belt.Option.map(
ReactDOM.Ref.domRef,
)}
/>
children
</div>
)
}
@send external focus: Dom.element => unit = "focus"
@react.component
let make = () => {
let input = React.useRef(Js.Nullable.null)
let focusInput = () =>
input.current
->Js.Nullable.toOption
->Belt.Option.forEach(input => input->focus)
let onClick = _ => focusInput()
<div>
<FancyInput className="fancy" ref=input>
<button onClick> {React.string("Click to focus")} </button>
</FancyInput>
</div>
}
注意: 在我们的 React.forwardRef
函数中,@react.component
装饰器使用与 make
函数相同的方式转换标签参数。
这样,使用 FancyInput
的组件可以获得底层 input
DOM 节点的引用,并在必要时访问它,就像在直接使用 input
一样。
组件库维护者须知
当你开始在组件库中使用 ref 转发时,你应该把它当作一个 breaking change,并且发布一个新的 major 版本。 这是因为你的库可能会具有不同的行为(例如分配给哪些 refs,哪些类型会被导出),这可能会破坏依赖于旧行为的应用或其他库。