在典型的React数据流当中,props是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的props重新渲染它。但在某些情况下,你需要强制修改子组件,子组件可能是一个React组件实例,也可能是一个DOM元素,React提供了Refs这种解决办法。
何时使用Refs
- 管理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
Refs类型
包括String类型的Refs、React.createRef()、回调Refs、React.useRef()
String类型的Refs(已过时,未来版本可能会被移除)
<div ref="divRef"></div>
缺陷:
- 它不允许一个实例具有多个拥有者
- 动态字符串可能会破坏VM中的优化
详细了解,点击 这里
React.createRef()(v16.3)
class MyComponent extends React.Component {
constructor(props){
super();
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />
}}
// 访问ref
const node = this.myRef.current;
何时挂载:react会在组件挂载时给current属性传入DOM元素,并在组件卸载时传入null。ref会在componentDidMount或componentDidUpdate生命周期钩子触发前更新。
回调Refs
接收React组件实例或者HTML DOM元素作为参数,以使它们能在其他地方被储存和访问。
DOM元素:
<input ref={ e => this.inputRef = e } >
// 调用input元素focus()方法
this.divRef.focus()
组件实例:
<Child ref={ e => this.childRef = e }
// 调用childRef.onSubmit()方法
this.childRef.onSubmit()
如果ref回调函数是以内联函数的方式定义的,在它的更新过程中会被执行两次,第一次传入参数null,第二次传入DOM元素或组件实例。这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并设置新的。通过把ref的回调函数改成class的绑定函数的方式可以避免上述问题,但大多数情况下它是无关紧要的。class的函数绑定可参考我之前那篇 react定义一个函数
回调refs与createRef()的不同点:
- 回调refs能更精准的控制何时被设置和解除
- 回调 refs 接收组件实例或 DOM 元素作为参数,而 createRef() 接收组件实例或 DOM 元素给 current 属性,多了一层 .current
useRef()
默认情况下,**不能在函数组件上使用ref属性,**因为他们没有实例。
如果你要在函数式组件中使用ref,可以使用 useRef(v16.8)。
useRef 返回一个可变的 ref 对象,其.current属性被初始化为传入的参数(initialValue)。返回的ref对象在组件的整个生命周期内保持不变。
在函数式组件中访问DOM:
import React, {createRef, useRef} from 'react';
export default CustomTextInput = (props) => {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
// const textInput = createRef(); // 只能保存 DOM 节点
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input ref={textInput} />
<input type="button" value="Focus the text input" onClick={handleClick} />
</div>
);
}
前面提到的访问 DOM 的两种主要方式(React.createRef() 和 回调Refs),无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。然而 useRef 比 ref 属性更有用的地方是可以很方便的保存任何可变值,其类似于 class 中使用实例字段的方法,不会因为重复 render 而重复声明。
在函数式组件中访问函数组件下的 DOM,要使用 Refs 转发将其包裹然后向下传递
import React, {useRef, forwardRef} from 'react';
const FanceInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
})
const CustomTextInputSub = (props) => {
const fancyInputRef = useRef();
function handleClick() {
fancyInputRef.current.focus();
}
return (
<div>
<FanceInput ref={fancyInputRef} />
<button onClick={handleClick}>btn</button>
</div>
);
}
export default CustomTextInputSub;
Refs转发(React.forwardRef)
refs 转发允许某些组件接收 ref,并将其向下传递给子组件。
react.forwardRef 接受一个渲染函数,其接受一个 props 和 ref 参数,并返回一个 React 节点。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
在渲染函数内部,我们接收 FancyButton 传递下来的 ref 和 props,并把 ref 传递给 button 元素,这样就可以直接获取到 button 的 ref。
Refs与高阶组件
function logProps(WrappedComponent) {
class LogProps extends React.Component {
render() {
return <WrappedComponent {...this.props} />; }
}
return LogProps;
}
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton)
refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。
import FancyButton from './FancyButton';
const ref = React.createRef();
// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}/>;
这时候,我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件
function logProps(Component) {
class LogProps extends React.Component {
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />; }
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
Ant Design 中的 Form.create
被 form.create() 包裹的组件需要使用 wrappedComponentRef 代替 ref,不然获取不到组件实例。
小结
- string 类型的 ref 一个实例不允许具有多个拥有者
- 回调 refs 比 React.createRef() 能更精准的控制何时被设置和解除
- 回调 refs 接收组件实例或 DOM 元素作为参数,而 createRef() 接收组件实例或 DOM 元素给 current 属性,多了一层 .current
- 不能在函数式组件上使用 ref 属性,因为它们没有实例,但是我们可以在函数式组件内部使用 useRef 获取 DOM,或者使用 Refs 转发将渲染函数包裹然后向下传递 ref 获取渲染函数内部的 DOM
- useRef()只能在函数式组件内使用,相比回调 refs 和 createRef() , 它可以保存任何可变值,其类似于 class 中使用实例字段的方法,不会因为重复 render 而重复声明
- 如果一个组件被高阶组件包裹,直接使用 refs 时获取到的是高阶组件实例,refs 转发能帮助我们我们获取到被包裹的实例,这很有用