目录
useRef
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
useRef
有什么作用呢,其实很简单,总共有两种用法
- 获取子组件的实例(只有类组件可用)
- 在函数组件中的一个全局变量,不会因为重复
render
重复申明, 类似于类组件的this.xxx
useRef
在使用的时候,可以传入默认值来指定默认值,需要使用的时候,访问 ref.current
即可访问到组件实例
一个常见的用例便是命令式地访问子组件:
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>
</>
);
}
获取子组件实例
上面提到了一点,useRef
只能获取子组件的实例,这在类组件中也是同样的道理,具体看下面的例子
// 使用 ref 子组件必须是类组件
class Children extends PureComponent {
render () {
const { count } = this.props
return (
<div>{ count }</div>
)
}
}
function App () {
const [ count, setCount ] = useState(0)
const childrenRef = useRef(null)
// const
const onClick = useMemo(() => {
return () => {
console.log('button click')
console.log(childrenRef.current)
setCount((count) => count + 1)
}
}, [])
return (
<div>
点击次数: { count }
<Children ref={childrenRef} count={count}></Children>
<button onClick={onClick}>点我</button>
</div>
)
}
类组件属性
有些情况下,我们需要保证函数组件每次 render
之后,某些变量不会被重复申明,比如说 Dom
节点,定时器的 id
等等,在类组件中,我们完全可以通过给类添加一个自定义属性来保留,比如说 this.xxx
, 但是函数组件没有 this
,自然无法通过这种方法使用,有的朋友说,我可以使用useState
来保留变量的值,但是 useState
会触发组件 render
,在这里完全是不需要的,我们就需要使用 useRef
来实现了,具体看下面例子
function App () {
const [ count, setCount ] = useState(0)
const timer = useRef(null)
let timer2
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + 1)
}, 500)
timer.current = id
timer2 = id
return () => {
clearInterval(timer.current)
}
}, [])
const onClickRef = useCallback(() => {
clearInterval(timer.current)
}, [])
const onClick = useCallback(() => {
clearInterval(timer2)
}, [])
return (
<div>
点击次数: { count }
<button onClick={onClick}>普通</button>
<button onClick={onClickRef}>useRef</button>
</div>
)
}
当我们们使用普通的按钮去暂停定时器时发现定时器无法清除,因为 App
组件每次 render
,都会重新申明一次 timer2
, 定时器的 id
在第二次 render
时,就丢失了,所以无法清除定时器,针对这种情况,就需要使用到 useRef
,来为我们保留定时器 id
,类似于 this.xxx
,这就是 useRef
的另外一种用法
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle : 第一个参数,接收一个通过forwardRef引用父组件的ref实例,第二个参数一个回调函数,返回一个对象,对象里面存储需要暴露给父组件的属性或方法
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
在本例中,渲染 <FancyInput ref={inputRef} />
的父组件可以调用 inputRef.current.focus()
。
function Kun (props, ref) {
const kun = useRef()
const introduce = useCallback (() => {
console.log('i can sing, jump, rap, play basketball')
}, [])
useImperativeHandle(ref, () => ({
introduce: () => {
introduce()
}
}));
return (
<div ref={kun}> { props.count }</div>
)
}
const KunKun = forwardRef(Kun)
function App () {
const [ count, setCount ] = useState(0)
const kunRef = useRef(null)
const onClick = useCallback (() => {
setCount(count => count + 1)
kunRef.current.introduce()
}, [])
return (
<div>
点击次数: { count }
<KunKun ref={kunRef} count={count}></KunKun>
<button onClick={onClick}>点我</button>
</div>
)
}
React.forwardRef
React.forwardRef
会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:
forwardRef:
引用父组件的ref实例,成为子组件的一个参数,可以引用父组件的ref绑定到子组件自身的节点上.
React.forwardRef
接受渲染函数作为参数。React 将使用 props
和 ref
作为参数来调用此函数。此函数应返回 React 节点。
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
示例:
//Test.tsx文件
import React, { FC, Fragment, useRef, MutableRefObject, forwardRef, ForwardRefExoticComponent, Ref, useImperativeHandle, ChangeEvent, SyntheticEvent, memo } from "react";
const Test: FC = (): JSX.Element => {
const testRef: MutableRefObject<any> = useRef('test');
const handleClick = (e:SyntheticEvent<HTMLButtonElement>):void =>{
console.log('自身button的内容:',e.currentTarget.innerText);
console.log('子组件input的对象:',testRef.current);
console.log('子组件input的value值:',testRef.current.value);
console.log('子组件input的类型:',testRef.current.type());
}
return (
<Fragment>
<TestChildForward ref={testRef} />
<button onClick={handleClick}>获取子组件的input的value和type</button>
</Fragment>
);
}
export default Test;
function TestChild(props:{},ref: Ref<any>): JSX.Element {
const testRef: MutableRefObject<any> = useRef();//创建一个自身的ref,绑定到标签节点上
//暴露出一个想要让父组件知道的对象,里面可以是属性也可以是函数
useImperativeHandle(ref,()=>{//第一个参数,要暴露给哪个(ref)?第二个参数要暴露出什么?
return {
//(testRef.current as HTMLInputElement) 类型断言,自己肯定就是这样的类型
value:(testRef.current as HTMLInputElement).value,//暴露出input的value
type:()=>(testRef.current as HTMLInputElement).type//暴露出input的type类型
}
});
return (
<>
<input type="text" value={'input的内容'} ref={testRef} onChange={(e:ChangeEvent<HTMLInputElement>)=>{
console.log(e.currentTarget.value);
console.log(e.currentTarget.type);
}}/>
</>
);
}
const TestChildForward:ForwardRefExoticComponent<any> = memo(forwardRef(TestChild));
在上述的示例中,React 会将 <FancyButton ref={ref}>
元素的 ref
作为第二个参数传递给 React.forwardRef
函数中的渲染函数。该渲染函数会将 ref
传递给 <button ref={ref}>
元素。
因此,当 React 附加了 ref 属性之后,ref.current
将直接指向 <button>
DOM 元素实例。
转发 refs 到 DOM 组件
考虑这个渲染原生 DOM 元素 button
的 FancyButton
组件:
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}
React 组件隐藏其实现细节,包括其渲染结果。其他使用 FancyButton
的组件通常不需要获取内部的 DOM 元素 button
的 ref。这很好,因为这防止组件过度依赖其他组件的 DOM 结构。
虽然这种封装对类似 FeedStory
或 Comment
这样的应用级组件是理想的,但其对 FancyButton
或 MyTextInput
这样的高可复用“叶”组件来说可能是不方便的。这些组件倾向于在整个应用中以一种类似常规 DOM button
和 input
的方式被使用,并且访问其 DOM 节点对管理焦点,选中或动画来说是不可避免的。
Ref 转发是一个可选特性,其允许某些组件接收 ref
,并将其向下传递(换句话说,“转发”它)给子组件。
在下面的示例中,FancyButton
使用 React.forwardRef
来获取传递给它的 ref
,然后转发到它渲染的 DOM button
:
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 组件实例中。
在高阶组件中转发 refs
这个技巧对高阶组件(也被称为 HOC)特别有用。让我们从一个输出组件 props 到控制台的 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} />; });}