[TOC]
`React`中的`Refs`提供了一种访问`render()`方法中创建的`React`元素(或`DOM`节点)的方法。
当父组件需要与子组件交互时,我们通常使用 [props](https://reactjs.org/docs/components-and-props.html) 来传递相关信息。 但是,**在某些情况下,我们可能需要修改子项,而不用新的`props`重新呈现 (re-rendering) 它**。这时候就需要`refs`出场了。
# 什么时候使用 Refs ?
我们建议在以下情况下使用 `refs`:
* 与第三方 `DOM` 库集成
* 触发命令式动画
* 管理焦点,文本选择或媒体播放
> 译注:第三点是否也可以理解为使用 `event` 对象呢?在 React 中就是合成事件 (SyntheticEvent)。
> **官方文档中提到:避免使用 `refs` 来做任何可以通过声明式实现来完成的事情**。
所以一旦我们确定我们需要使用 `refs`,我们如何使用它们呢?
# 在 React 中使用 Refs
您可以通过多种方式使用`refs`:
* [React.createRef()](https://reactjs.org/docs/refs-and-the-dom.html)
* 回调引用 (Callback refs)
* String refs(已过时,这 API 将被弃用)
* 转发`refs`(Forwarding refs)
## `React.createRef()`
使用`React.createRef()`创建引用,并通过`ref`属性附加到React元素上。
在构造组件时,通常将 Refs 分配给实例属性,以便在整个组件中引用它们。
```
// Ref.js
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef(); // 先在 构造函数中创建并挂载在组件的一个属性上,然后就可以在该组件上使用了
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<input type="text" ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
```
在上面的代码块中,我们构建了一个按钮,当单击它时,**该页面会自动聚焦在输入框上。**
首先,我们在构造方法中**创建一个 React `ref` 实例**,并将其赋值给 `this.textInput`,然后通过`ref` 属性将其分配给 `input`元素。
```
<input type="text" ref={this.textInput} />
```
注意,当 `ref` 属性被一个`HTML` 元素使用时(比如当前示例中的 `input`元素),在 `constructor` 中使用 `React.createRef()` 创建的 `ref`会接收 **来自底层`DOM`元素的 `current`值**。
> 译注:这里的 `current` 应该是[合成事件(SyntheticEvent)](http://react.html.cn/docs/events.html)
这意味着访问 `DOM` 值,我们需要写这样的东西:
```
this.textInput.current;
```
## Refs 回调
**Refs 回调** 是在 React 中使用 `ref` 的另一种方式。要以这种方式使用 ref,我们需要为 ref 属性设置回调函数。
当我们设置 ref 时,React 会调用这个函数,并将 element 作为第一个参数传递给它。
```
// Refs.js
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
// 回调,传入的
this.setTextInputRef = element => {
this.textInput = element;
};
}
handleSubmit = e => {
e.preventDefault();
console.log(this.textInput.value);
};
render() {
return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<input type="text" ref={this.setTextInputRef} />
<button>Submit</button>
</form>
</div>
);
}
```
上面的示例中,我们将 input 标签的 `ref` 设置为 `this.setTextInputRef`。
* 当组件安装时,React 会将 DOM 元素传递给 ref 的回调;
* 当组件卸载时,则会传递 `null`。
(`ref` 回调会在 `componentDidMount` 和 `componentDidUpdate` 生命周期之前调用。)
## `React.forwardRef`
不能在函数组件上使用`ref`属性,因为函数组件没有实例。
如果您希望引用函数组件,您可以使用`forwardRef`可能与`useImperativeHandle`结合使用),或者您可以将它转换为类组件。
```
// Ref.js
// 普通的函数组件 是不会有 ref 参数的,React.forwardRef 返回一个组件
const TextInput = React.forwardRef((props, ref) => (
<input type="text" placeholder="Hello World" ref={ref} />
));
const inputRef = React.createRef();
class CustomTextInput extends React.Component {
handleSubmit = e => {
e.preventDefault();
console.log(inputRef.current.value);
};
render() {
return (
<div>
<form onSubmit={e => this.handleSubmit(e)}>
<TextInput ref={inputRef} />
<button>Submit</button>
</form>
</div>
);
}
}
```
`Ref forwarding`允许组件接收一个`ref`,并将它向下传递(换句话说,“转发”它)给子组件。
在上面的示例中,我们使用`input`标签创建了一个名为`TextInput`的组件。那么,我们如何将`ref`传递或转发到`input`标签呢?
首先,我们使用下面的代码创建一个`ref`:
```
const inputRef = React.createRef();
```
然后,我们**通过组件 `<TextInput ref={inputRef}>` 的 `ref` 属性的值,将 `ref` 向下传递**。然后`React` 将会把 `ref` 作为第二个参数转发给 `forwardRef` 函数(这个是在 React 框架层面完成的事情)。
接下来,我们将此 `ref` 参数转发给`<input ref={ref}>`。现在可以在外层组件通过 `inputRef.current` 访问 DOM 节点的值了。
## 高阶组件的 ref
如果我要操作一个高阶组件 的 DOM ,怎么办?
ref 是不能像 props 一样,往下面传递的,因此想要往下面传递,必须要用到`React.forwardRef`这个 API。
```
// by 司徒正美
const ThemeContext = React.createContext('light');
class ThemeProvider extends React.Component {
state = {theme: 'light'};
render() {
return (
<ThemeContext.Provider value={this.state.theme}>
{this.props.children}
</ThemeContext.Provider>
);
}
}
class FancyButton extends React.Component {
buttonRef = React.createRef();
focus() {
this.buttonRef.current.focus();
}
render() {
const {label, theme, ...rest} = this.props;
return (
<button
{...rest}
className={`${theme}-button`}
ref={this.buttonRef}>
{label}
</button>
);
}
}
function withTheme(Component) {
// React.forwardRef 会提供 第二个参数"ref",然后就可以直接把其附加到组件上
function ThemedComponent(props, ref) {
return (
<ThemeContext.Consumer>
{theme => (
<Component {...props} ref={ref} theme={theme} />
)}
</ThemeContext.Consumer>
);
}
// These next lines are not necessary,
// But they do give the component a better display name in DevTools,
// e.g. "ForwardRef(withTheme(MyComponent))"
const name = Component.displayName || Component.name;
ThemedComponent.displayName = `withTheme(${name})`;
// 告诉 React 传递 "ref" 到 ThemedComponent.
return React.forwardRef(ThemedComponent);
}
const fancyButtonRef = React.createRef();
const FancyThemedButton = withTheme(FancyButton);
// fancyButtonRef 现在指向 FancyButton
<FancyThemedButton
label="Click me!"
onClick={handleClick}
ref={fancyButtonRef}
/>;
```
# 结论
与通过`props`和`state`不同,`Refs`是一种将数据传递给特定子实例的好方法。
你必须要小心,因为`refs`操纵实际的`DOM`,而不是虚拟的`DOM`,这与`React`思维方式相矛盾。因此,虽然`refs`不应该是通过应用程序流动数据的默认方法,但是当您需要时,它们是可以从`DOM`元素读取数据的好方法。
# 参考
推荐下「司徒正美」大佬的[React v16.3.0: New lifecycles and context API](https://segmentfault.com/a/1190000014083970),createRef API,forwardRef API 中的示例可以作为补充阅读。
[https://segmentfault.com/a/1190000019277029](https://segmentfault.com/a/1190000019277029)