React hooks重温之useRef

95 阅读4分钟

前情

React和Vue是目前前端流行的主要框架,平时工作中都有用到,对于新版的react来说,函数组件和hooks是react的重中之重,特别是hooks给react带来了非常棒的开发体验,工作中疲于业务开发,停留在会使用相关hooks的阶段,并未细细学习react相关hooks,现在正是细细学习的时候了。

思考?

问题1:除了使用全局变量,怎么在函数组件生命周期中实现一个保持不变的值?

问题2:react怎么获取Dom元素实例?

问题3:react父组件怎么引用函数式子组件,访问子组件数据+调用子组件的方法?

useRef

useRef函数返回一个可变的ref对象,该对象只有一个current属性,调用useRef函数时可以为其指定初始值,并且这个返回的ref对象在组件的整个生命周期内保持不变

基础语法

const refObj = useRef(初始值)
console.log(refObj.current)

存储渲染周期之间的共享数据

假设我们有如下一个需求,就是每次点击updateCount按钮的时候希望重起一个定时器递增count。

错误示例:如下代码多次点击会发现定时器运行出现异常,变化频率随着你点击次数越多,变的越快,因为你每点一次都是新启一个定时器

import { useState } from 'react';

export default function CountTimer() {
  const [count, setCount] = useState(0);

  let timer = null;

  const updateCount = () => {
    clearInterval(timer);
    timer = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }
  console.log('Count run');
  return (
    <div className='App'>
      <p>当前count的值:{count}</p>
      <button onClick={updateCount}>updateCount</button>
    </div>
  );
}

正确代码代码:

import { useState, useRef } from 'react';

export default function CountTimerOk() {
  const [count, setCount] = useState(0);

  let timer = useRef();

  const updateCount = () => {
    clearInterval(timer.current);
    timer.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }
  console.log('Count run');
  return (
    <div className='App'>
      <p>当前count的值:{count}</p>
      <button onClick={updateCount}>updateCount</button>
    </div>
  );
}

注:因为组件rerender时useRef不会被重新初始化

useRef获取Dom元素实例

假设我们有如下一个需求,希望点击focus表单按钮,让表单获得焦点,此需求也不一定要通过useRef来实现,直接使用js原生dom获取方法也是可以实现。

示例代码:

import { useRef } from 'react';

export default function InputFocus() {

  let inputRef = useRef();

  const focus = () => {
    inputRef.current.focus();
  }
  return (
    <div className='App'>
      <input type="text" ref={inputRef} />
      <button onClick={focus}>focus表单</button>
    </div>
  );
}

useRef获取函数式子组件引用,并获取子组件数据和调用子组件方法

useRef无法直接获取函数式子组件引用,需要借助forwardRef和useImperativeHandle的配合才能达到理想效果

示例代码:


//----- Child组件代码  -----
import { useState, forwardRef, useImperativeHandle } from 'react'

const RefChild = forwardRef((props, ref) => {
  const [count, setCount] = useState(0)

  const updateCount = () => {
    setCount((count) => count + Number(props.step));
  }

  // 向外面ref暴露内部数据和方法
  useImperativeHandle(ref, () => {
    return {
        count,
        updateCount
    }
  })

  return (
    <div className="warp">
      <div>child count:{count}</div>
      <button onClick={updateCount}>updateCount</button>
    </div>
  )
})

export default RefChild;

//----- Parent组件代码  -----
import { useRef } from 'react';
import ChildRef from './Child.jsx';

export default function ParentRef() {

  let childRef = useRef();

  const updateChildCount = () => {
    childRef.current.updateCount();
    console.log('---- updateChildCount ----:', childRef.current.count);
  }
  console.log('Count run');
  return (
    <div className='App'>
      <ChildRef step={5} ref={childRef} />
      <button onClick={updateChildCount}>updateChildCount</button>
    </div>
  );
}

useImperativeHandle还支持第三个参数:

  • 如果省略则内部函数的任何状态变化都会重新useImperativeHandle初始化,这样外部ref能及时的获取到子组件的更新
  • 如果传一个空数据,则是useImperativeHandle只会初始化一次,如果暴露内部的状态,外面ref获取的是不会跟子组件保持更新新的
  • 如果传的是[状态值,状态值…],则只有这些状态变化时才会重新useImperativeHandle初始化,这样是有利于性能的,这是推荐做法。

useRef使用注意事项

  • 组件rerender时useRef不会被重新初始化
  • ref.current变化不会使组件重新渲染,如果要实现重新渲染应该使用useState
  • ref.current变化不会使组件重新渲染,所以ref.current不能作为其它Hooks如useEffect、useMemo、useCallback等的依赖项

友情提示

如果你使用的是vs code,在书写React hooks时可以安装第三方的插件,快捷输入相关hooks,我个人有编写一款常用代码段插件**Common-Code-Snippet**,里面就有与hooks相关的代码段,除了hooks相关的代码段,还有很多好用的代码段,具体使用规则可查看插件的说明文挡,插件会一直持续更新。

hooks相关示例说明:

// 输入rusestate会生成如下代码段
const [example, setexample] = useState();

// 输入ruseeffect会生成如下代码段
useEffect(() => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  }, [state]);