要学习useRef和useImperativeHandle之前,我们得先学习和回顾一下再class组件中的refs。
一、什么是Refs?
官网是这么说的:
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在react数据流中,props是父子组件交流的唯一方式。如果要修改一个子组件的状态,那么就需要使用新的props去重新渲染。但是,在某些情况下,我们需要在props数据流之外,强制去修改子组件。被修改的子组件可能是一个React组件的实例,也可能是DOM元素。React官方对于这两种情况都给出了解决办法。
react对于在calss组件中创建一个ref有两种方式。16.3版本之前的回调函数ref与16.3版本之后的React.createRef()。
二、创建Ref
1、class组件之React.createRef()
这种方式是使用 React.createRef()
创建的ref,并通过 ref
属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
2、class组件之callback Ref
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 组件挂载后,让文本框自动获得焦点
this.focusTextInput();
}
render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 将在组件挂载时,会调用 ref
回调函数并传入 DOM 元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React 会保证 refs 一定是最新的。
你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef()
创建的对象 refs 一样。
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
在上面的例子中,Parent
把它的 refs 回调函数当作 inputRef
props 传递给了 CustomTextInput
,而且 CustomTextInput
把相同的函数作为特殊的 ref
属性传递给了 <input>
。结果是,在 Parent
中的 this.inputElement
会被设置为与 CustomTextInput
中的 input
元素相对应的 DOM 节点。
3、访问refs
当 ref 被传递给 render
中的元素时,对该节点的引用可以在 ref 的 current
属性中被访问。
const node = this.myRef.current;
ref 的值根据节点的类型而有所不同:
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 你不能在函数组件上使用
ref
属性,因为他们没有实例。
1、为Dom元素添加ref
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}
render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
2、为 class 组件添加 Ref
如果我们想包装上面的 CustomTextInput
,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 focusTextInput
方法
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
请注意,这仅在 CustomTextInput
声明为 class 时才有效:
class CustomTextInput extends React.Component {
// ...
}
3、Refs 与函数组件
默认情况下,你不能在函数组件上使用 ref
属性,因为它们没有实例:
function MyFunctionComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// 这个是不能使用!!!!!!!!
return (
<MyFunctionComponent ref={this.textInput} />
);
}
}
如果要在函数组件中使用 ref
,你可以使用 forwardRef
(可与 useImperativeHandle
结合使用),或者可以将该组件转化为 class 组件。
不管怎样,你可以在函数组件内部使用 ref
属性,只要它指向一个 DOM 元素或 class 组件:
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
4、将 DOM Refs 暴露给父组件
在极少数情况下,你可能希望在父组件中引用子节点的DOM节点,一般来说,是不建议这么做的。因为这样会打破组件的封装,但它偶尔可以用于触发焦点或者测量子DOM节点的大小或者位置。
虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。
如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发,也就是forward Ref。forward Ref使组件可以像暴露自己的 ref 一样暴露子组件的 ref。
如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用这个替代方案将 ref 作为特殊名字的 prop 直接传递。
1、什么是forward Ref?
Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧
2、转发 refs 到 DOM 组件
Ref 转发是一个可选特性,其允许某些组件接收 ref
,并将其向下传递(换句话说,“转发”它)给子组件。
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
的组件可以获取底层 DOM 节点 button
的 ref ,并在必要时访问,就像其直接使用 DOM button
一样。
以下是对上述示例发生情况的逐步解释:
- 我们通过调用
React.createRef
创建了一个 React ref 并将其赋值给ref
变量。 - 我们通过指定
ref
为 JSX 属性,将其向下传递给<FancyButton ref={ref}>
。 - React 传递
ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。 - 我们向下转发该
ref
参数到<button ref={ref}>
,将其指定为 JSX 属性。 - 当 ref 挂载完成,
ref.current
将指向<button>
DOM 节点。
注意:
第二个参数 ref
只在使用 React.forwardRef
定义组件时存在。常规函数和 class 组件不接收 ref
参数,且 props 中也不存在 ref
。Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。
3、在高阶组件中转发 refs
forward Ref这个技巧对于高阶组件是非常有用的。
HOC 示例开始:
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return LogProps;
}
“logProps” HOC 透传(pass through)所有 props
到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props:
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);
上面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref
不是 prop 属性。就像 key
一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。
这意味着用于我们 FancyButton
组件的 refs 实际上将被挂载到 LogProps
组件
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
组件。React.forwardRef
接受一个渲染函数,其接收 props
和 ref
参数并返回一个 React 节点。
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
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} />;
});
}
5、forward Ref的问题
forwardRef的做法本身没有什么问题, 但是我们是将子组件的DOM直接暴露给了父组件:
- 直接暴露给父组件带来的问题是某些情况的不可控
- 父组件可以拿到DOM后进行任意的操作
- 我们只是希望父组件可以操作的focus,其他并不希望它随意操作其他方法
三、useRef
上面说React在class组件中有两种创建ref的方式,但不能给函数式组件使用ref,但是可以在函数式组件内部使用回调函数ref和React.CreateRef()。
1、函数组件之React.createRef()
import React, { useEffect } from 'react';
const InputFocus = () => {
const inputRef = React.createRef();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
})
return <input type="text" ref={inputRef}/>
}
export default InputFocus;
2、函数组件之callback Ref
import React, { useEffect } from 'react';
const InputFocus = () => {
let inputRef= null;
useEffect(() => {
if (inputRef) {
inputRef.focus();
}
})
return <input type="text" ref={(el) => inputRef = el}/>
}
export default InputFocus;
同样hooks也为我们提供了一个hook来创建ref。
3、useRef的使用
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
createRef和useRef不仅可以保存DOM元素和class组件实例,也可以用来保存任何值。
function App() {
const valRef = React.useRef(80);
return <div>
value: {valRef.current}
<button onClick={() => valRef.current += 1}>+</button>
</div>
}
valRef的current属性值为80,点击"+" 按钮将会在valRef.current的基础上+1,改变valRef.current不会触发组件的更新
因此我们需要别的方式来在ref.current变更时,触发组件更新,展示最新的ref.current值,使用useState强制更新,其实也可以用回调函数ref。
function App() {
const valRef = React.useRef(80);
const [, setChange] = React.useState();
return <div style={{padding: "100px 200px"}}>
value: {valRef.current}
<button onClick={() => {
valRef.current += 1;
setChange({});
}}>+
</button>
</div>
}
**React.createRef可以用于class组件和Function组件,React.useRef必须用于函数组件。**当React.createRef()用于Fucntion组件的时候,可能出现一些问题。
function App() {
const valRef = React.createRef();
const [, setChange] = React.useState();
return <div style={{padding: "100px 200px"}}>
value: {valRef.current}
<button onClick={() => {
valRef.current = 80;
setChange({});
}}>+
</button>
</div>
}
我们用createRef替换掉useRef,在这里无论点击多少次按钮,最后还是会显示为空(值为null):
这是因为函数组件的行为仍然是函数,他们在渲染和重渲染的 就像普通的函数一样
所以,当App这个函数组件被重新渲染时,App函数将会执行,并且重新创建、初始化所有的变量和表达式。因此,createRef每次都会被执行,所以对应的值总是为null。
4、useRef解决了什么问题
为了在函数组件中保存状态,useRef就被创造出来,它将会在函数组件的生命周期中,保持状态不变,除非手动进行修改。
createRef在class组件中能够发挥出作用,是因为class组件在更新的时候只会调用render、componentDidUpdate等几个method。这整个过程,只会在组件初始化的时候创建ref,之后的更新过程都不会重新初始化class组件的实例,因此ref的值会在class组件的声明周期中保持不变(除非手动更新)。
所以在函数式组件中,我们应该使用useRef去创建我们的ref对象,useRef
总会在每次渲染时返回同一个 ref 对象,除非手动改变。
但是,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
import React, {useState, useCallback} from "react";
import ReactDOM from "react-dom";
function MeasureExample() {
const [height, setHeight] = useState(0);
// Because our ref is a callback, it still works
// even if the ref only gets attached after button
// click inside the child component.
const measureRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<Child measureRef={measureRef} />
{height > 0 &&
<h2>The above header is {Math.round(height)}px tall</h2>
}
</>
);
}
function Child({ measureRef }) {
const [show, setShow] = useState(false);
if (!show) {
return (
<button onClick={() => setShow(true)}>
Show child
</button>
);
}
return <h1 ref={measureRef}>Hello, world</h1>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<MeasureExample />, rootElement);
在这里,我们没有选择使用useRef,而是使用了Callback Ref可以确保 即便子组件延迟显示被测量的节点(比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。
在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1>
组件在整个重新渲染期间始终存在。如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver
或基于其构建的第三方 Hook。
四、useImperativeHandle
1、基本使用:
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例。不要直接使用forward Ref,forward Ref应该与useImperativeHandle一起使用
- ref:定义 current 对象的 ref createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
- 对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件
- ref 的 current 属性上,如果为空数组,则不会重新输出。
2、它解决了什么问题
上面我们讲述了forward Ref的一些问题,forward Ref会将子组件的实例直接暴露给父组件,父组件可以对子组件实例做任意的操作,那么,useImperativeHandle就是来解决这个问题的,useImperativeHandle可以让我们选择暴露一些我们需要被特殊处理的操作。
- 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
- 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
直接使用forward Ref:
import React, { useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
// 实现 ref 的转发
const FancyButton = React.forwardRef((props, ref) => (
<div>
<input ref={ref} type="text" />
<button>{props.children}</button>
</div>
));
// 父组件中使用子组件的 ref
function App() {
const ref = useRef();
const handleClick = useCallback(() => ref.current.focus(), [ ref ]);
return (
<div>
<FancyButton ref={ref}>Click Me</FancyButton>
<button onClick={handleClick}>获取焦点</button>
</div>
)
}
ReactDOM.render(<App />, root);
配合useImperativeHandle一起使用
import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';
const FancyInput = React.forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} type="text" />
});
const App = props => {
const fancyInputRef = useRef();
return (
<div>
<FancyInput ref={fancyInputRef} />
<button
onClick={() => fancyInputRef.current.focus()}
>父组件调用子组件的 focus</button>
</div>
)
}
ReactDOM.render(<App />, root);
上面这个例子中与直接转发 ref 不同,直接转发 ref 是将 React.forwardRef 中函数上的 ref 参数直接应用在了返回元素的 ref 属性上,其实父、子组件引用的是同一个 ref 的 current 对象,官方不建议使用这样的 ref 透传,而使用 useImperativeHandle 后,可以让父、子组件分别有自己的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 方法来自定义开放给父组件的 current。
useImperativeHandle 的第一个参数是定义 current 对象的 ref,第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象,这样可以像上面的案例一样,通过自定义父组件的 ref 来使用子组件 ref 的某些方法
最后再看一个useImperativeHandle的例子:
import React, {
useState,
useRef,
useImperativeHandle,
useCallback
} from 'react';
import ReactDOM from 'react-dom';
const FancyInput = React.forwardRef((props, ref) => {
const [ fresh, setFresh ] = useState(false)
const attRef = useRef(0);
useImperativeHandle(ref, () => ({
attRef,
fresh
}), [ fresh ]);
const handleClick = useCallback(() => {
attRef.current++;
}, []);
return (
<div>
{attRef.current}
<button onClick={handleClick}>Fancy</button>
<button onClick={() => setFresh(!fresh)}>刷新</button>
</div>
)
});
const App = props => {
const fancyInputRef = useRef();
return (
<div>
<FancyInput ref={fancyInputRef} />
<button
onClick={() => console.log(fancyInputRef.current)}
>父组件访问子组件的实例属性</button>
</div>
)
}
ReactDOM.render(<App />, root);