什么是Ref
Ref意味着reference,所以ref可以指定任何(DOM node, JavaScript value, …)
基本使用
类组件中通过React.createRef()来创建一个ref,并且在dom或者组件中传递ref的props
函数式组件中通过useRef来创建ref
ref 的值根据节点的类型而有所不同:
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
- 你不能在函数组件上使用
**ref**
属性,因为他们没有实例。 - createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用
//类组件
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
**********************************************
//函数式组件
const MyComponent = () => {
myRef = React.useRef();
return <div ref={myRef} />;
}
React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。
Instance Variable
used to create a mutable object. The mutable object’s properties can be changed at will without affecting the Component’s state.
在组件中,Ref可以用作保存Instance Variable。
例如,我们控制组件初次挂载和更新的时候分别展示不同渲染内容:
import * as React from "react";
const App = () => {
const [count, setCount] = React.useState(0);
function onClick() {
setCount(count + 1);
}
const isFirstRender = React.useRef(true);
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log(
`
I am a useEffect hook's logic
which runs for a component's
re-render.
`
);
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
{/*
Only works because setCount triggers a re-render.
Just changing the ref's current value doesn't trigger a re-render.
*/}
<p>{isFirstRender.current ? "First render." : "Re-render."}</p>
</div>
);
};
export default App;
**useRef**
在渲染周期内永远不会变,因此可以用来引用某些数据。- 修改
**ref.current**
不会引发组件重新渲染。
Rule of thumb: Whenever you need to track state in your React component which shouldn’t trigger a re-render of your component, you can use React’s useRef Hooks to create an instance variable for it.
更多详细使用参见 useEffect-ONLY ON UPDATE
Dom Refs
很多场景下,我们需要和HTML元素进行交互,需要访问DOM,
下面我们通过一个示例来说明如何获取dom
import * as React from "react";
const App = () => {
const ref = React.useRef(); //(1)创建ref
const [text, setText] = React.useState("Some text ...");
function handleOnChange(event) {
setText(event.target.value);
}
React.useEffect(() => {
//(3)访问Ref
const { width } = ref.current.getBoundingClientRect();
console.log(`Width:${width}`);
},[text]);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
// (2)给HTML元素提供一个ref的HTML attribute
<span ref={ref}>{text}</span>
</div>
</div>
);
};
export default App;
我们来看下获取dom的ref使用方法,(1) (2) (3)
- 通过React.useRef创建一个ref object
- 给HTML元素提供一个ref的HTML attribute,React会自动为的给dom节点赋予ref特性
- 最后我们可以通过ref.current来获取dom节点
每次改变输入框的输入值也就是text状态发生变化的时候 我们都可以访问到输入框span的宽度。
Callback Refs
React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。
不同于传递 createRef()
创建的 ref
属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
在dom节点上添加ref属性,通过ref = (node)=> {} 来获取dom
https://stackblitz.com/edit/react-tvvq5r
import React from "react";
import "./style.css";
export default function App() {
const [text, setText] = React.useState("Some text ...");
function handleOnChange(event) {
setText(event.target.value);
}
const ref = React.useCallback(
node => {
console.log(node);
if (!node) return;
const { width } = node.getBoundingClientRect();
if (width >= 100) {
node.style.color = "red";
} else {
node.style.color = "blue";
}
console.log(`Width:${width}`);
},
[text]
);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}
不需要创建ref,更不需要ref.current来访问ref
通过回调函数,直接可以获取到dom。
Refs转发-forwardRef
Ref 转发是一项将 ref 自动地通过组件传递到其子组件的技巧。(说白了就是父组件获取子组件的一种方式)
Ref 转发是一个可选特性,其允许某些组件接收 **ref**
,并将其向下传递(换句话说,“转发”它)给子组件。
我们先看下类组件中,如何获取子组件的ref:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.AddButtonRef = React.createRef();
}
handleAdd = () => {
const { count } = this.state;
this.setState({ count: count + 1 });
};
render() {
return (
<>
Counter子组件count:{this.state.count}
<button onClick={this.handleAdd} ref={this.AddButtonRef}>
点击增加
</button>
</>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.CountRef = React.createRef();
}
handleRef = () => {
console.log(this.CountRef);
const buttonText = this.CountRef.current.AddButtonRef.current.innerHTML;
console.log(buttonText);
// this.CountRef.current.handleAdd();
};
render() {
return (
<>
<div>App</div>
<button onClick={this.handleRef}>点击获取子组件内容</button>
<Counter ref={this.CountRef} />
</>
);
}
}
export default App;
Counter子组件中有button,父组件APP想要获取button的ref,通过在子组件Counter上定义createRef就可以获取相应的ref了,比较简单。
class Demo extends React.Component {
render() {
return <>类组件</>;
}
}
function Counter() {
return <>函数组件</>;
}
const App = () => {
const countRef = React.useRef();
setTimeout(() => console.log(222, countRef), 2000);
return (
<div>
<div>类组件Demo可以拿到ref,函数式组件Counter拿不到</div>
<Demo />
<Counter ref={countRef} />
</div>
);
};
函数式组件没有this,通过forwardRef可以获取函数式组件的ref
在函数组件中要获取子组件的数据,需要两步骤
- 将ref传递到子组件中,
需要使用forwardRef对子组件进行包装 ```javascript //2. 子组件通过React.forwardRef 进行转发 const Counter = React.forwardRef(({ item }, ref) => { const [count, setCount] = useState(0);
const handleAdd = () => { setCount(count + 1); };
return (
Counter子组件count:{count} {/* 3. 子组件向上暴露ref/}); }); function App() { //1. 父组件定义ref const CountRef = useRef(); const handleRef = () => { const buttonText = CountRef.current.innerHTML; console.log(buttonText); }; return ( <>App</> ); }
export default App;
App和Counter组件是父子组件关系,想在App父组件中访问子组件Counter中的li dom节点
- 子组件被React.forwardRef(Component(props,ref))包裹,子组件第二个参数是ref,指向子组件的dom节点
- 父组件创建ref,访问子组件ref
> 注意
> 第二个参数 `ref` 只在使用 `React.forwardRef` 定义组件时存在。常规函数和 class 组件不接收 `ref` 参数,且 props 中也不存在 `ref`。
> Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。
<a name="JuIjC"></a>
# useRef和createRef区别
- 两者都是获取 ref 的方式,都有一个 current 属性。
- useRef 只能用于函数组件,createRef 可以用在类组件中。
- useRef 在每次重新渲染后都保持不变,而 createRef 每次都会发生变化。?
- **createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用**
```javascript
import React, { useRef, useEffect, useState } from 'react';
const Page1 = () => {
const myRef2 = useRef(0);
const [count, setCount] = useState(0)
useEffect(()=>{
myRef2.current = count;
});
function handleClick(){
setTimeout(()=>{
console.log(count); // 3
console.log(myRef2.current); // 6
},3000)
}
return (
<div>
<div onClick={()=> setCount(count+1)}>点击count</div>
<div onClick={()=> handleClick()}>查看</div>
</div>
);
}
export default Page1;
类组件在更新的时候只会调用render、componentDidUpdate等生命周期
类组件只在组件初始化的时候创建ref,之后的更新过程都不会重新初始化class组件的实例,因此ref的值会在class组件的声明周期中保持不变(除非手动更新)。
useImperativeHandle
使用场景:通过 ref 获取到的是整个 dom 节点,通过 useImperativeHandle 可以控制只暴露一部分方法和属性,而不是整个 dom 节点。
useImperativeHandle可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过useImperativeHandle添加到父组件中useRef定义的对象中
基本使用
useImperativeHandle(ref,create,[deps])函数前2个参数为必填项,第3个参数为可选项。
第1个参数为父组件通过useRef定义的引用变量;
第2个参数为子组件要附加给ref的对象,该对象中的属性即子组件想要暴露给父组件的函数(方法);
第3个参数为可选参数,为函数的依赖变量。凡是函数中使用到的数据变量都需要放入deps中,如果处理函数没有任何依赖变量,可以忽略第3个参数。
请注意:
1、这里面说的“勾住子组件内自定义函数”本质上是子组件将内部自定义的函数添加到父组件的ref.current上面。
2、父组件若想调用子组件暴露给自己的函数,可以通过 res.current.xxx 来访问或执行。
const xxx = () => {
//do smoting...
}
useImperativeHandle(ref,() => ({xxx}));
特别注意:() => ({xxx}) 不可以再简写成 () => {xxx},如果这样写会直接react报错。
因为这两种写法意思完全不一样:
1、() => ({xxx}) 表示 返回一个object对象,该对象为{xxx}
2、() => {xxx} 表示 执行 xxx 语句代码
import React, { useState, useEffect, useRef, useImperativeHandle } from 'react';
import './style.css';
//2. 子组件通过React.forwardRef 进行转发
const Counter = React.forwardRef(({ item }, ref) => {
const [count, setCount] = useState(0);
//3.将子组件的方法暴露给父组件
useImperativeHandle(ref, () => ({
handleAdd,
}));
const handleAdd = () => {
setCount(count + 1);
};
return (
<div>
Counter子组件count:{count}
<button onClick={handleAdd}>点击增加</button>
</div>
);
});
function App() {
//1. 父组件定义ref
const CountRef = useRef();
const handleRef = () => {
CountRef.current.handleAdd();
console.log(CountRef.current);
};
return (
<>
<div>App</div>
<button onClick={handleRef}>点击获取子组件内容</button>
<Counter ref={CountRef} />
</>
);
}
export default App;
可以看到,父组件中获取到子组件中的handleAdd方法并执行了,count+1
拆解说明:
1、子组件内部先定义一个 xxx 函数
2、通过useImperativeHandle函数,将 xxx函数包装成一个对象,并将该对象添加到父组件内部定义的ref中。
3、若 xxx 函数中使用到了子组件内部定义的变量,则还需要将该变量作为 依赖变量 成为useImperativeHandle第3个参数,上面示例中则选择忽略了第3个参数。
4、若父组件需要调用子组件内的 xxx函数,则通过:res.current.xxx()。
5、请注意,该子组件在导出时必须被 React.forwardRef()包裹住才可以。
思考一下真的有必要使用useImperativeHandle吗?
从实际运行的结果,无论点击子组件还是父组件内的按钮,都将执行 addCount函数,使 count+1。
react为单向数据流,如果为了实现这个效果,我们完全可以把需求转化成另外一种说法,即:
1、父组件内定义一个变量count 和 addCount函数
2、父组件把 count 和 addCount 通过属性传值 传递给子组件
3、点击子组件内按钮时调用父组件内定义的 addCount函数,使 count +1。
你会发现即使把需求中的 父与子组件 描述对调一下,“最终实际效果”是一样的。
所以,到底使用哪种形式,需要根据组件实际需求来做定夺。
Reference
https://www.robinwieruch.de/react-ref
https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
https://zh-hans.reactjs.org/docs/forwarding-refs.html
https://github.com/puxiao/react-hook-tutorial/blob/master/13%20useImperativeHandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md