第九章 Refs:从存储数据到指令式API 上

文章出处:www.advanced-react.com/

专栏地址:juejin.cn/column/7443...

React中很厉害的一个地方在于,它抽象了复杂的处理真实DOM过程。在React中,我们不再需要手动访问真实的DOM,或者手动为元素添加类,或者为浏览器兼容性头疼,而只用专注于写组件和提升用户体验。但是呢,也有极少数情况下,我们需要访问真实的DOM。

为了实现这个目的,我们需要Refs。这次没有什么神秘复杂的东西,咱们就来实现一个带有输入字段简单验证功能的精美表单吧。在此过程中,我们将会学到:

  • 为什么我们还需要访问DOM元素。
  • 什么是Ref,Ref与state(状态)之间有什么不同/
  • 如何使用Ref来访问UI元素。
  • 什么是forwardRef,如何使用它,以及如何避免使用它。
  • 为什么我们仍然需要指令式API,如通在有useImperativeHandle或没有useImperativeHandle的情况下实现指令式API

在React访问DOM

假设,我为我正在组织的一个论坛实现一个登录表单。我希望来会人员在我提供他们详细信息前,先提供他们的名字、邮箱和Twitter号。其中,名字和邮箱号是必填的。但是,我不希望必填字段为空时,仅仅是出现红色的边框。我希望这个表单的交互可以更加酷炫。所以,我期望的校验交互是晃动空白的输入框。

React为我们提供了强大的特性,但并没有提供所有能力。像"手动关注一个元素",或者"摇动一个元素"这些功能,并不在React的核心包里面。我们得重拾生疏了的原生 JavaScript API 使用技能。为此,我们需要能够访问实际的 DOM 元素。

在非React的世界,我需要这样:

js 复制代码
const element = document.getElementById('bla');

然后,我们聚焦这个元素:

js 复制代码
element.focus();

或者,滑动到此:

js 复制代码
element.scrollIntoView();

或者,我们可以进行其他操作:

  • 手动聚焦一个已经渲染的元素,比如表单中的input
  • 当显示类似弹出框的元素时,检测组件外部的点击事件。
  • 在某个元素出现在屏幕上之后,手动滚动到该元素处。
  • 计算屏幕上各组件的尺寸和边界,以便正确定位类似提示工具(tooltip)之类的元素。

虽然我们仍然可以js原生API来操作元素,但是React仍然为我们提供了更加轻便的、无需传递id、无需关心DOM结构的方法:Refs。

什么是Ref

Ref是一个可变对象,在React重新渲染时,React会保留它。还记得吗,我们在组件内声明的东西会在重新渲染时再次创造。

js 复制代码
const Component = () => {
    // "data" object will be new with every re-render
    const data = { id: 'test' };
}

组件本质上就是函数,所以组件内部的一切基本上都是该函数的局部变量。引用(Refs)使我们能够绕过这一限制。

为了创建一个Ref,我们可以使用useRef钩子,并为这个钩子传递一个初始值:

js 复制代码
const Component = () => {
    const ref = useRef({ id: 'test' });
}

我们可以通过current属性来访问ref的当前值:

js 复制代码
const Component = () => {
    // pass intital value here
    const ref = useRef({ id: 'test' });
    
    useEffect(() => {
        // access it here
        console.log(ref.current);
    });
}

这个初始值被缓存了,重新渲染器在比对ref.current时,其索引始终是相同的。如果我们用useMemo来缓存一个对象,也是一样的。

Ref被创立后,我们可以在useEffect活事件处理器中为之传递任何值。它不过就是一个对象罢了:

js 复制代码
const Component = () => {
    useEffect(() => {
        // assign url as an id, when it changes
        ref.current = { id: url };
    }, [url])
}

这一切看起来很像状态,对吧?既然如此,为什么我们要四处使用状态,而很少使用Ref?

Ref和state的不同

让我们先实现表单的基本功能:

js 复制代码
const Form = () => {
    return (
        <>
            <input type="text" />
            <button onClick={submit}>submit</button>
        </>
    );
};

现在,为了实现提交功能,我们需要提取输入的内容。在React中,我们只要添加一个onChang回调函数给input即可:

js 复制代码
const Form = () => {
    const [value, setValue] = useState();
    
    const onChange = (e) => {
        setValue(e.target.value);
    };
    const submit = () => {
        // send to the backend here
        console.log(value);
    }

    return (
        <>
            <input type="text" />
            <button onClick={submit}>submit</button>
        </>
    );
};

但是我之前说过,我们存在Ref中的索引在重新渲染前后始终是相同的。而且,任何值都可以赋给Ref。那么,如果我把输入的内容存到Ref,而不是存到状态(state),会发生什么?

js 复制代码
const Form = () => {
    const ref = useRef();
    
    const onChange = (e) => {
        // save it to ref instead of state
        ref.current = e.target.value;
    };
    const submit = () => {
        // get it from ref instead of state
        console.log(ref.current);
    }

    return (
        <>
            <input type="text" />
            <button onClick={submit}>submit</button>
        </>
    );
};

这段代码看起来和使用状态差不多,也能跑起来。

代码示例: advanced-react.com/examples/09...

那么,这两者的不同到底在哪里?为什么在实践中很少看到有人这么写?我来说明相关原因。

Ref的更新并不会触发重新渲染

它们两者最明显的区别在于,Ref的更新并不会触发重新渲染。如果你在这两个表单里面添加console.log,你会发现:在使用了sate的Form的表单里,每一次输入内容都会触发重新渲染,而在使用了Ref的Form的表单则没有重新渲染。

js 复制代码
useEffect(() => {
    console.log('Form component re-renders');
});

表面上看,这是好事。我们都在探索如何通过减少重新渲染来提升性能。而不触发重新渲染的Ref更新,不正是提升应用性能最好的解方吗?

然而并不是这样的。我们知道,重新渲染是React生命周期中非常关键的一个环节。它是React实现信息更新的环节。比如说,如果我想在表单上统计输入内容的长度,我们就不能使用Ref了。

js 复制代码
const Form = () => {
    const ref = useRef();
    const numberOfLetters = res.current?.length ?? 0;
    
    return (
        <>
            <input type="text" onChange={onChange} />
            {/* Not going to work */}
            Characters count: {numberOfLetters}
            <button onClick={submit}>submit</button>
        </>
    )
}

Ref更新并不会触发重新渲染,所以numberOfLetters的返回值永远是0。

实际上,这比 0 的情况更有意思。如果某个与该输入完全无关的因素导致表单(Form)组件重新渲染,那么该值会突然更新为最新的值。要记得,引用(Ref)会在多次重新渲染之间存储那个值呀。如果我给这个表单添加一个类似简单模态对话框之类的东西,那么对话框的打开操作就会成为触发组件自我更新以及字母数量发生变化的诱因。

js 复制代码
const Form = () => {
  // state for the dialog
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef();
  const numberOfLetters = ref.current?.length ?? 0;
  return (
   <>
      <input type="text" onChange={onChange} />
      {/* This will not change when you type in the field */}
      {/* Only when you open/close the dialog */}
      Characters count: {numberOfLetters}
      <button onClick={submit}>submit</button>
      {/* Adding dialog here */}
      <button onClick={() => setIsOpen(true)}>
          Open dialog
      </button>
      {isOpen ? <ModalDialog onClose={() => setIsOpen(false)} /> : null}
   </>
  );
 };

代码示例: advanced-react.com/examples/09...

相关推荐
腾讯TNTWeb前端团队4 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom9 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom9 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom9 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试