React中Hooks的useRef 的高级用法

React中的Refs为我们提供了一种在组件的整个生命周期中存储可变值的方法,并且通常用于与DOM交互而无需重新渲染组件。换句话说,我们不需要依赖状态管理来使用Refs更新元素。这在某些特定的使用案例中非常有用,但在代替状态管理或生命周期方法集成时,也被视为一种反模式。

hooks已经集成到react的框架中,使用生命周期的类组件,现在可以替换成为函数组件和hooks。

useRefhook已实现为在函数组件中使用React ref的解决方案。在本文中,我们将与其他可以一起工作的钩子一起探索这个钩子。更具体地说,我们将:

  • 介绍如何将useRefhook与函数组件结合使用,并介绍与useEffect和共同使用的hook的一些用例useLayoutEffect
  • 如何useRef与并发React一起正确使用
  • 探索useRef演示组件的用例

这里我们主要关注function组件,尽管这绝不意味着它们应该在类组件上使用,这也带来了它们的好处。

将项目移至函数组件是否值得?

函数组件提供速度和样板优化的地方,类组件提供了久经考验的组件生命周期方法和众所周知的结构,我们都已经习惯了(使用this类属性等)。

为了促进代码的一致性,开发人员通常选择对项目使用纯类组件方法或纯函数组件方法。我个人选择创建函数组件作为我的默认选择,并退回到有意义的类组件,例如当我想显式定义生命周期方法,许多类属性等时。

以我的经验,Typescript似乎比功能性组件更多地补充了类组件,能够将类型以及属性和方法插入类型本身。类具有更详细的结构。

在任何情况下,React Ref都可以在类和函数组件中使用。让我们探讨一下如何使用带有useRef钩子的引用。

使用 useRef Hook

函数组件Ref的实现已通过名为的钩子实现useRef。让我们看看它是如何集成的,然后探究它的特性以及何时使用它。

Ref可以在组件内定义:

import React, { useRef } from 'react';
...
const refContainer = useRef(initialValue);

这个钩子有一个非常简单的API-可以说比它的类更简单。选择该refContainer名称(来自官方文档)以反映该变量实际上充当基础引用的容器。

为了与Refs的基类实现一致,被引用的对象本身存储在current此容器变量的属性中。有关此current属性的两个关键事实:

  • 该属性是可变的
  • 它可以在组件生命周期中随时更改

函数组件仍然具有类组件的生命周期,尽管没有生命周期方法。组件生命周期的考虑将变得越来越重要。

另外,initialValue我们上面传入的参数可用于使用current默认值进行初始化。该值通常充当占位符,直到我们实际引用DOM中的元素或为其分配任意值为止。

事件尽管Ref通常用于引用DOM元素,但它们也可以存储原始类型和对象。我们将介绍这两种情况的示例。

传入的初始值也是完全合法的null:

//初始化一个空引用
const myRef = useRef(null);

无论current是什么,我们都可以在组件生命周期的任何时间log该属性以查看其值:

//亲自查看Ref实际引用的是什么
console.log(refContainer.current);

通过ref属性完成对DOM元素的引用。我们return通过ref属性在语句内的JSX级别上执行此操作。下面使用button元素完全做到了这一点:

// referencing a `button` element
...
render() {
  return(
    <button ref={refContainer}>
      Press Me
    </button>
  );
}

记住,我们引用的是DOM HTML元素,而不是React组件。

如果引用的是按钮,则将refContainer.current指向该<button />DOM元素,从而使我们能够访问诸如使按钮聚焦/模糊的控件以及其样式和事件处理程序(onClick例如,触发click事件)。

模糊是用于使活动元素处于非活动状态的术语。如果一个文本框处于活动状态(光标在内部闪烁,准备输入文本),则可以通过单击该元素外部的来模糊该元素以取消选择它,或者通过编程使用诸如Refs之类的功能。

用refs管理按钮状态

让我们来看一下useRef按钮的第一个有效用例。

按钮是一个很好的用例,可以与useRef按钮一起使用,从而可以控制按钮的状态(不要与组件状态混淆),而无需重新渲染整个组件。

让我们考虑一个真实的场景。也许表单已经完成,并且需要从默认的禁用状态启用提交按钮。仅为了执行此操作而重新渲染我的整个表单将需要我:

  • 将所有当前表单值保存到状态
  • 使用这些当前值再次重新渲染整个表单
  • 保持子组件中可能存在的任何其他状态,例如验证消息和可视指示器
  • 重置可能发生的所有过渡或动画

React要在后台处理很多工作。只需引用DOM中的按钮以切换其disabled属性,在这里就更有意义了:

refContainer.current.setAttribute("disabled", true);
// or 
refContainer.current.removeAttribute("disabled");

到此为止,我们现在已经介绍了的基本APIuseRef以及可行的用例-现在让我们看看如何useRefuseEffecthook一起正确实现。

在Commit组件阶段正确实现useRef

为了完全理解如何实现useRef,我们需要了解React组件执行的两个阶段,以及这与React ref的工作如何联系。

可以在函数组件的主块中定义Ref,但是在确定了将在Component中更新的内容之后,必须在组件的commit阶段中定义与Ref相关的任何副作用,例如事件侦听器或计时器。DOM发生。让我们更详细地访问它。

渲染与提交阶段

一个组件经历两个高级阶段:

  • 所述渲染阶段确定从先前的对DOM做出呈现的变化,并调用的方法,如componentWillMount,render和setState(主要)
  • commit(提交)阶段,顾名思义,提交更改(即呈现阶段确定)的DOM,并调用方法,包括componentDidMount,componentDidUpdate,和componentDidCatch

如果要实现引用,则第一阶段渲染具有我们需要关注的关键特征:在执行提交阶段之前可能会多次调用它–这是有问题的,在我们的应用程序中引入了不可预测性和错误的可能性

引用应在提交阶段实现

另一方面,提交阶段只能调用一次,这是我们应该定义副作用的阶段,通常来说,我们只希望实例化一次。

副作用是任何会影响正在执行的函数范围之外的内容的东西。这些可以是API请求,网络套接字,计时器,记录器,甚至是引用中的任何内容。

如果某个组件重新渲染多次并在该阶段重新初始化Ref,则该Ref逻辑将执行相同的次数。当我们考虑React中的并发模式时,这更令人担忧,因为组件的渲染阶段可以在初始渲染上执行多次。

这是因为并发模式将渲染过程分解为多个部分,经常在需要执行其他更高优先级的异步过程时暂停和恢复工作。这样做的结果是有可能在提交之前多次调用渲染阶段生命周期方法(如果有错误,则根本不调用)。

定义副作用或超出组件范围的任何内容都是不可靠的。我们可以做很多事情来确保在开发时不会落入这些陷阱。

1.使用严格模式

我们可以做的第一件事就是实现严格模式。严格模式旨在通过控制台突出显示应用程序编码的各种问题,并有完整的章节专门用于检测意外的副作用

可以通过JXS在整个应用程序中启用严格模式,也可以仅在一定数量的子组件上启用严格模式。包装整个应用程序以在整个过程中应用严格模式:

// wrapping your app in Strict Mode
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
, document.getElementById('root'));

与一样React.Fragment,严格模式不会在您的应用中产生任何其他标记。

这是我偏爱的方法,可确保正确设计函数组件的逻辑,但还有其他选择可用。

2.在并发模式下测试您的应用

一种更直接的方法是在并发模式下开发应用程序,这还将在开发过程中标记问题。

有两种启用并发模式的方法,要么包装元素的子集,要么使用并发模式声明包装整个应用程序:

// section of an app (not final API)
<React.unstable_ConcurrentMode>
  <MyComponent />
</React.unstable_ConcurrentMode>
// entire app (not final API)
ReactDOM.unstable_createRoot(domNode).render(<App />);

这些不是最终的API,但是为开发人员提供了至少一些测试React应用程序异步版本的方法。该博客文章实际上建议使用严格模式作为准备应用程序并发的主要方法。

由于进行中的工作性质不稳定,建议不要在最终版本之前部署启用并发模式的应用程序的生产版本。

考虑到以上几点,现在让我们useRef在提交阶段检查一下实际完成的实现。

使用useEffect实现useRef

避免我们一直在讨论的不可预测的Ref行为的解决方案是在useEffect or useLayoutEffect钩子内部实现Ref副作用。

为什么是这样?因为根据官方文档,函数组件的主体内部不允许出现诸如变异,订阅,计时器和日志记录之类的副作用。该主体内部的所有逻辑都在渲染阶段执行,因此导致UI中令人困惑的错误和不一致。

useEffect另一方面,在浏览器中更新了实际的DOM,它将运行一次。useEffect因此将在组件的提交阶段运行。

可以创建一个简单的计数器组件来演示这一点,每次重新渲染组件时我们都在其中进行计数。为了在单击按钮时强制重新渲染组件,useReducer还实现了该hook:


import React, { useEffect, useReducer, useRef } from "react";
const useForceRerender = () => useReducer(state => !state, false)[1];

const Counter = () => { 
  
  const forceRerender = useForceRerender();
  const refCount = useRef(0);
 
  useEffect(() => {
    refCount.current += 1;
  });
  
  return (
    <>
      <p>Count: {refCount.current}</p>
      <p>
        <button onClick={forceRerender}>
          Increment Counter
        </button>
      </p>
    </>;
  );
};

export default Counter;

在上面的示例中,refCount.current从值开始0,并在组件更新的提交阶段递增。

请记住,如果我们要在主功能块中进行此增量,则更新将在返回render函数之前在render阶段进行,使增量遭受不可预测的重复。

现在,让我们看一下另一个引用DOM元素的组件,并通过其ref将事件侦听器附加到该组件。事件侦听器再次在useEffecthook中定义。此外,useEffect当组件被卸载时,act的返回函数可作为整齐的手段触发,在这里我们可以从ref中删除事件监听器:

import React, { useEffect, useRef } from 'react';

function App () {
  const refInput = useRef();

  useEffect(() => {
    const { current } = refInput;

    const handleFocus = () => {
      console.log('input is focussed');
    }
    const handleBlur = () => {
      console.log('input is blurred');
    }

    current.addEventListener('focus', handleFocus);
    current.addEventListener('blur', handleBlur);

    return () => {
      current.removeEventListener('focus', handleFocus);
      current.removeEventListener('blur', handleBlur);
    }
  });

  return (
    <p>
      <input
        type="text"
        ref={refInput}
        defaultValue="Focus me"
      />
    </p>
  );
}

export default App;

现在,如果您单击文本输入,然后单击相应的console.log输出,则相应的输出将通知您该文本输入正在聚焦和模糊。

让我们进一步了解这个概念。下一个示例通过refInputRef操作按钮元素的类。我们还引入了<Wrapper />样式化组件来定义一个active类,该类会更改文本输入边框和文本颜色:

import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  input {
    color: #666;
    border: 1px solid #ccc;
    outline: none;
    &.active {
      color: #000;
      border-color: #000;
    }
  }
`;

function App () {
  const refInput = useRef();

  useEffect(() => {
    const { current } = refInput;

    const handleFocus = () => {
      current.classList.add('active');
    }
    const handleBlur = () => {
      current.classList.remove('active');
    }

    current.addEventListener('focus', handleFocus);
    current.addEventListener('blur', handleBlur);

    return () => {
      current.removeEventListener('focus', handleFocus);
      current.removeEventListener('blur', handleBlur);
    }
  });

  return (
    <Wrapper>
      <input
        type="text"
        ref={refInput}
        defaultValue="Focus me"
      />
    </Wrapper>
  );
}

export default App;

现在,我们无需依赖状态更新就可以进行一些CSS操作。

演示的最后阶段是实现我们之前讨论的内容—实现一个Submit按钮,如果文本输入的值为空,则禁用它。为此,我useRef为提交按钮本身引入了一个额外的钩子。为了切换disabled属性,RefsetAttribute和removeAttributeJavascript API已被使用refSubmit。

完整的解决方案如下:

import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  .text {
    color: #666;
    border: 1px solid #ccc;
    outline: none;
    &.active {
      color: #000;
      border-color: #000;
    }
  }
`;

function App () {
  const refInput = useRef();
  const refSubmit = useRef();

  useEffect(() => {

    const { current } = refInput;

    const handleFocus = () => {
      current.classList.add('active');
    }

    const handleBlur = () => {
      current.classList.remove('active');

      current.value !== ''
        ? refSubmit.current.removeAttribute('disabled')
        : refSubmit.current.setAttribute('disabled', true);
    }

    current.addEventListener('focus', handleFocus);
    current.addEventListener('blur', handleBlur);

    return () => {
      current.removeEventListener('focus', handleFocus);
      current.removeEventListener('blur', handleBlur);
    }
  });

  return (
    <Wrapper>
      <p>
        <input
          className='text'
          type="text"
          ref={refInput}
          defaultValue="Focus me"
        />
      </p>
      <p>
        <input
          ref={refSubmit}
          type="submit"
          value="Submit"
        />
      </p>
    </Wrapper>
  );
}

export default App;

disabled一旦我们在文本输入之外单击(或点击),或者当它变得模糊时,提交按钮的属性就会更新,这是确定表单是否有效的自然时间。

one more thing

本文的最后一部分将介绍值得注意的使用技巧useRef。

当useEffect引起问题时,请使用useLayoutEffect

在本文开头,我们提到了该useLayoutEffect钩子,该钩子也常与一起使用useRefuseLayoutEffectcomponentDidMountcomponentDidUpdate类组件生命周期方法在同一阶段触发,因此您可能倾向于使用它代替useEffect

但是,官方文档建议开发人员应useEffect主要尝试使用,useLayoutEffect如果出现问题请退后。使用会牺牲一些速度useLayoutEffect,因为只有在所有DOM突变/更新都发生后才被同步调用-除了这个细节,它与相同useEffect

转发useRef的

就像在类组件中初始化的转发Refs一样useRef,只要遵循相同的约定,也可以转发通过钩子初始化的Refs 。不要将ref作为“ ref”属性传递-这是React中保留的属性名称,会导致错误。相反,一个名为的道具forwardRef

...
// defining `refInput` within `App`, forwarding it to `MyInput`
function App () {
  const refInput = useRef();
  return <MyInput 
    forwardRef={refInput};
}

// referencing `input` element with `forwardRef` in child component
function MyInput (props) {
  // verifying `input` is referenced correctly after DOM updates
  useLayoutEffect(() => {
    console.log(props.forwardRef.current);
  });
  const { forwardRef } = props;
  return (
    <input
      ref={forwardRef}
      type="submit"
      value="Submit"
  />);
}

用useRef切换焦点

除了监听事件,我们还可以触发事件。如果我们不展示此演示,则演示将不完整。在下面的组件中,单击一个按钮将再次关注另一个文本输入,再次使用useRef:

// focussing an element with a button press
function TextInput () {
const refInput = useRef();
  function handleFocus () {
    refInput.current.focus();
  }
  return (
    <>
      <input ref={refInput} placeholder="Input Here..." />
      <button onClick={handleFocus}>Focus Input</button>
    </>
  );
}

以编程方式聚焦的元素可以改善用户体验,例如在第一次加载表单并自动聚焦第一输入时。

总结

本文是的useRef总结,并介绍了如何在考虑组件生命周期的情况下正确实现引用。对于这里讨论的用例,使用refs可能是一种便捷的方法:

  • 使用焦点,模糊,禁用和其他与表单管理相关的属性来微管理输入
  • 要从元素中添加或删除类,可能控制过渡或关键帧动画
  • 官方文档中推荐的ref的另一个用例是与其他HTML5库(例如媒体播放器)进行交互。这样的库将无法通过React状态访问,而Refs为我们提供了一个后备功能,可以在与组件的生命周期一致的同时直接与其他元素进行交互

参考

React: Using Refs with the useRef Hook

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容