useRef详细总结

本文全方面介绍 useRef,分别从什么是 useRef、为何使用 useRef 以及 useRef 三兄弟(指 useRef 、 forwardRef 以及 useImperativeHandle )来说解。javascript

本文全部示例java

1、什么是useRef

const refContainer = useRef(initialValue);
复制代码
  • 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue )。
  • 返回的 ref 对象在组件的整个生命周期内保持不变
  • 当更新 current 值时并不会 re-render ,这是与 useState 不一样的地方
  • 更新 useRef 是 side effect (反作用),因此通常写在 useEffect 或 event handler 里
  • useRef 相似于类组件的 this

简单示例

需求: 点击 button 的时候 选中文本框node

实现:react

import React, { MutableRefObject, useRef } from 'react'
const TextInputWithFocusButton: React.FC = () => {
   const inputEl: MutableRefObject<any> = useRef(null)
   const handleFocus = () => {
       // `current` 指向已挂载到 DOM 上的文本输入元素
       inputEl.current.focus()
   }
   return (
       <p> <input ref={inputEl} type="text" /> <button onClick={handleFocus}>Focus the input</button> </p>
   )
}
export default TextInputWithFocusButton
复制代码

经过ref获取组件内的DOM 小结: 经过useRef定义个inputEl变量,在input 元素上定义ref={inputEl},这样经过inputEl.current就能够获取到input Dom 元素,选中则调用下focus函数便可web

见示例库里的domUseRef.tsx数组

2、为何使用useRef

需求: 跨渲染取到状态值markdown

只用useState:

实现:antd

import React, { useState } from "react";
const LikeButton: React.FC = () => {
    const [like, setLike] = useState(0)
    function handleAlertClick() {
        setTimeout(() => {
            alert(`you clicked on ${like}`) 
            //造成闭包,因此弹出来的是当时触发函数时的like值
        }, 3000)
    }
    return (
        <> <button onClick={() => setLike(like + 1)}>{like}赞</button> <button onClick={handleAlertClick}>Alert</button> </>
    )
}
export default LikeButton
复制代码

现象: 在like为6的时候, 点击 alert , 再继续增长like到10, 弹出的值为 6, 而非 10. 因为闭包,函数里的变量值为调用函数时对应的快照like值 为何不是界面上like的实时状态? 当咱们更改状态的时候,React会从新渲染组件,每次的渲染都会拿到独立的like值,并从新定义个handleAlertClick函数,每一个handleAlertClick函数体里的like值也是它本身的,因此当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。闭包

小结: 不一样渲染之间没法共享state状态值app

见示例库里的likeButton

采用全局变量

在组件前定义一个相似 global 的变量

实现:

import React from "react";
let like = 0;
const LikeButton: React.FC = () => {
  function handleAlertClick() {
    setTimeout(() => {
      alert(`you clicked on ${like}`);
    }, 3000);
  }
  return (
    <> <button onClick={() => { like = ++like; }} > {like}赞 </button> <button onClick={handleAlertClick}>Alert</button> </>
  );
};
export default LikeButton;
复制代码

现象 在like为6的时候, 点击 alert , 再继续增长like到10, 弹出的值为10. 采用global变量 小结 因为like变量是定义在组件外,因此不一样渲染间是能够共用该变量,因此3秒后获取的like值就是最新的like值 该示例同时也说明,非state变量不会引发从新render

代码见示例库里的globalFix1

采用useRef

实现:

import React, { useRef } from "react";
const LikeButton: React.FC = () => {
  // 定义一个实例变量
  let like = useRef(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert(`you clicked on ${like.current}`);
    }, 3000);
  }
  return (
    <> <button onClick={() => { like.current = like.current + 1; }} > {like.current}赞 </button> <button onClick={handleAlertClick}>Alert</button> </>
  );
};
export default LikeButton;
复制代码

现象 在like为6的时候, 点击 alert , 再继续增长like到10, 弹出的值为10.跟上面使用全局变量现象一致 采用useRef 小结 采用useRef,做为组件实例的变量,保证获取到的数据确定是最新的。

该示例同时也说明,ref更改不会re-render

见示例库里的useRefFix2

useRef与全局变量的区别

上面两个法子均可以解决问题,那两个有什么区别呢

实现

import React, { useRef } from "react";
// 定义一个全局变量
let like = 0;
const LikeButton: React.FC = () => {
  let likeRef = useRef(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert(`you clicked on ${like}`);
      alert(`you clicked on ${likeRef.current}`);
    }, 3000);
  }
  return (
    <p> <button onClick={() => { like = ++like; likeRef.current = likeRef.current + 1; }} > 点赞 </button> <button onClick={handleAlertClick}>Alert</button> </p>
  );
};
export default LikeButton;
复制代码

两种法子区别 现象 三个按钮依次点下,点击任意alert,最早弹出的是3(表示global变量取的最后渲染组件的值),后弹出1(表示ref属于组件本身,互相不影响)

小结

  • useRef 是定义在实例基础上的,若是代码中有多个相同的组件,每一个组件的 ref 只跟组件自己有关,跟其余组件的 ref 没有关系。
  • 组件前定义的 global 变量,是属于全局的。若是代码中有多个相同的组件,那这个 global 变量在全局是同一个,他们会互相影响。

代码见示例库里的differenceFix1and2.tsx

3、useRef与createRef的区别

在一个组件的正常的生命周期中能够大体分为3个阶段:

  1. 从建立组件到挂载到DOM阶段。初始化props以及state, 根据state与props来构建DOM
  2. 组件依赖的props以及state状态发生变动,触发更新
  3. 销毁阶段

第一个阶段,useRef与createRef没有差异

第二个阶段,createRef每次都会返回个新的引用;而useRef不会随着组件的更新而从新建立

第三个阶段,二者都会销毁

实现:

import React, { useState, useRef, createRef } from 'react'
const RefDifference: React.FC = () => {
    let [renderIndex, setRenderIndex] = useState(1)
    let refFromUseRef = useRef<number>()
    let refFromCreateRef = createRef()
    console.info(refFromUseRef.current, 'refFromUseRef.current')
    console.info(refFromCreateRef.current, 'refFromCreateRef.current')
    if (!refFromUseRef.current) {
        refFromUseRef.current = renderIndex
    }

    if (!refFromCreateRef.current) {
        refFromCreateRef.current = renderIndex
    }
    return (
        <> <p>Current render index: {renderIndex}</p> <p> <b>refFromUseRef</b> value: {refFromUseRef.current} </p> <p> <b>refFromCreateRef</b> value: {refFromCreateRef.current} </p> <button onClick={() => setRenderIndex((prev) => prev + 1)}> Cause re-render </button> </>
    )
}
export default RefDifference
复制代码

现象: 点击按钮时,从控制台能够看到refFromUseRef.current一直为1(由于refFromUseRef.current已经存在该引用),而refFromCreateRef.current倒是undefined(由于createRef 每次渲染都会返回一个新的引用,因此if判断时为true,会被从新赋值,页面就会显示出新的值) useRef与createRef 小结: createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用

代码见示例库里的useRefAndCreateRef

以上讲的都是在当前组件使用ref的示例,后续接着说ref如何用在与子组件的交互上

4、获取子组件的属性或方法

需求: 调用子组件里的某个函数

自定义属性传入ref

实现: 父组件建立一个ref做为一个属性传入子组件。子组件根据内部方法的变化动态更改ref(useEffect)

import React, {
    MutableRefObject,
    useState,
    useEffect,
    useRef,
    useCallback
} from 'react'
interface IProps {
    //prettier-ignore
    label: string,
    cRef: MutableRefObject<any>
}
const ChildInput: React.FC<IProps> = (props) => {
    const { label, cRef } = props
    const [value, setValue] = useState('')
    const handleChange = (e: any) => {
        const value = e.target.value
        setValue(value)
    }
    const getValue = useCallback(() => {
        return value
    }, [value])
    useEffect(() => {
        if (cRef && cRef.current) {
            cRef.current.getValue = getValue
        }
    }, [getValue])
    return (
        <div> <span>{label}:</span> <input type="text" value={value} onChange={handleChange} /> </div>
    )
}

const ParentCom: React.FC = (props: any) => {
    const childRef: MutableRefObject<any> = useRef({})
    const handleFocus = () => {
        const node = childRef.current
        alert(node.getValue())
    }
    return (
        <div> <ChildInput label={'名称'} cRef={childRef} /> <button onClick={handleFocus}>focus</button> </div>
    )
}

export default ParentCom

复制代码

现象: 父组件按钮点击时,经过调用getValue,获取到子组件input里的value值 ref做为属性传入子组件 小结: 不够优雅,尤为是自定义一个属性传入ref

见示例库里的childComponentRef.tsx

经过useImperativeHandle,配合forwardRef

forwardRef: 将父类的ref做为参数传入函数式组件中

示例:

React.forwardRef((props, ref) => {})  
//建立一个React组件,
//这个组件将会接受到父级传递的ref属性,
//能够将父组件建立的ref挂到子组件的某个dom元素上,
//在父组件经过该ref就能获取到该dom元素
复制代码
const FancyButton = React.forwardRef((props, ref) => (  
  <button ref={ref} className="FancyButton"> {props.children} </button>
));
// 能够直接获取到button的DOM节点
const ref = React.useRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
复制代码

**useImperativeHandle:**在函数式组件中,用于定义暴露给父组件的ref方法,用来限制子组件对外暴露的信息,只有useImperativeHandle第二个参数定义的属性跟方法才能够在父组件获取到

为何使用: 由于使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了

解决: 使用 uesImperativeHandle 解决,在子函数式组件中定义父组件须要进行的 DOM 操做,没有定义的就不会暴露给父组件

useImperativeHandle(ref, createHandle, [deps]) // 第一个参数暴露哪一个ref;第二个参数暴露什么信息
复制代码
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()
复制代码

实现:

import React, {
    MutableRefObject,
    useState,
    useImperativeHandle,
    useRef,
    forwardRef,
    useCallback
} from 'react'
interface IProps {
    label: string
}
let ChildInput = forwardRef((props: IProps, ref: any) => {
    const { label } = props
    const [value, setValue] = useState('')
    // 做用: 减小父组件获取的DOM元素属性,只暴露给父组件须要用到的DOM方法
    // 参数1: 父组件传递的ref属性
    // 参数2: 返回一个对象,父组件经过ref.current调用对象中方法
    useImperativeHandle(ref, () => ({
        getValue
    }))
    const handleChange = (e: any) => {
        const value = e.target.value
        setValue(value)
    }
    const getValue = useCallback(() => {
        return value
    }, [value])
    return (
        <div> <span>{label}:</span> <input type="text" value={value} onChange={handleChange} /> </div>
    )
})
const ParentCom: React.FC = (props: any) => {
    const childRef: MutableRefObject<any> = useRef({})
    const handleFocus = () => {
        const node = childRef.current
        alert(node.getValue())
    }
    return (
        <div> <ChildInput label={'名称'} ref={childRef} /> <button onClick={handleFocus}>focus</button> </div>
    )
}
export default ParentCom

复制代码

现象:

只往外暴露getValue 小结: React 16.3版本以前,是不可以在函数组件里定义ref属性,由于函数组件没有实例。在16.3版本以后,引入了React.forwardRef,经过该函数就能够将父组件的ref传入子组件,子组件能够将该ref绑定在任何DOM元素上,但这样会将整个子组件暴露给父组件。经过useImperativeHandle就能够限制要将子组件里的哪些属性或方法暴露给父组件

注意: 使用 antd 的 Form.create() 包装子组件以后,在父组件中要将 ref 属性改为 wrappedComponentRef <ChildInput label={'名称'} wrappedComponentRef={childRef} />

见示例库里的childComponentRef2

5、总结

  1. useRef能够用来定义变量,这些变量更改以后不会引发页面从新渲染,好比分页获取数据时,存储页码。
  2. useRef也能够用来区分初始渲染仍是更新(经过current有没值,具体见示例库里的didOrUpdate.tsx)
  3. 在DOM节点上定义ref属性,经过.current就能够获取到该DOM元素
  4. 经过forwardRef就能够给函数子组件传入ref属性。
  5. 使用useImperativeHandle用于定义暴露给父组件的ref方法
相关文章
相关标签/搜索