什么是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 logicwhich runs for a component'sre-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)创建refconst [text, setText] = React.useState("Some text ...");function handleOnChange(event) {setText(event.target.value);}React.useEffect(() => {//(3)访问Refconst { 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 每次都会返回相同的引用**```javascriptimport 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); // 3console.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. 父组件定义refconst 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

