React高手都善于使用useImprativeHandle

简介: React高手都善于使用useImprativeHandle

在 React Hooks 中,useImperativeHandle 是一个非常简单的 hook,他比较小众,刚开始接触 React 学习的朋友可能并不熟悉他。不过对于 React 顶尖高手而言,这是非常重要的 Hook,他能让我们对 React 的使用变得更加得心应手。应对更多更复杂的场景。


01useRef


学习 useImperativeHandle,得从 useRef 说起。我们前面已经学习过了 useRef,它能够结合元素组件的 ref 属性帮我们拿到该元素组件对应的真实 DOM


例如,我想要拿到一个 input 元素的真实 DOM 对象,并调用 input 的 .focus() 方法,让 input 获得焦点。

import {useRef} from "react";
export default function Demo() {
  const inputRef = useRef<HTMLInputElement>(null);
  const focusTextInput = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }
  return (
    <>
      <input type="text" ref={inputRef} />
      <button onClick={focusTextInput}>
        点击我让input组件获得焦点
      </button>
    </>
  );
}

每一个 React 提供的元素组件,都具备 ref 属性。在上面的章节中我们可以知道,当我们拿到了元素的原生 DOM 对象之后,就可以脱离 React 的开发思路,从而应对更多更复杂的场景。


那么问题就来了,原生组件有自己的 ref 属性,那么自定义组件呢?当然是没有的,因此我们得自己想办法处理


02forwardRef


forwardRef 能够在我们自定义组件时,把内部组件的 ref 属性传递给父组件。


它接受我们自定义的组件作为参数,并返回一个新的组件。新组件具备我们自定义组件的全部能力,并得到一个 ref 属性,父组件通过 useRef 获取到的内容与内部组件的 ref 完全一致。


我们来看一个案例。


现在我们要实现如下效果,当点击 Edit 按钮时,输入框自动获得焦点

我们知道,在 DOM 中,只要得到 input 对象,然后就可以调用 .focus() 方法来实现目标。现在我们要封装一个自定义的 MyInput 组件,他具备 input 同样的能力,同时,我们还要封装一个标题进去

<label>Enter your name</label>
<input />

我们的代码如下

import {forwardRef, LegacyRef} from 'react'
type MyInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string
}
function MyInput(props: MyInputProps, ref: LegacyRef<HTMLInputElement>) {
  const {label, ...other} = props
  return (
    <label>
      {label}
      <input {...other} ref={ref} />
    </label>
  )
}
export default forwardRef(MyInput)

MyInput 在声明时要传入两个参数,一个 props,一个 ref。通过展开运算符,我们能够确保 MyInput 支持 input 所有的属性。


封装好之后,我们就可以在点击实践中,通过 ref 得到的引用去调用 .focus() 达到 input 获取焦点的目标。

import { useRef } from 'react'
import MyInput from './MyInput'
export default function ImperativeHandle() {
  const ref = useRef<any>(null)
  function handleClick() {
    ref.current?.focus()
  }
  return (
    <form>
      <MyInput 
        label='Enter your name:' 
        ref={ref} 
      />
      <button type='button' onClick={handleClick}>Edit</button>
    </form>
  )
}


03useImperativeHandle


在实践中,很多时候,我们并不想通过 ref 去获取子组件内部的某个元素组件的真实 DOM 对象。而是希望父组件能够调用子组件内部的某些方法


但是在 React 中,又无法直接 new 一个子组件的实例,像面向对象那样通过子组件实例去调用子组件的方法。


因此,React 提供了一个 hook,useImperativeHandle,让我们能够重写子组件内部 ref 对应的引用,从而达到在父组件中,调用子组件内部方法的目的


例如,上面的 MyInput 组件,我们可以修改代码为:

import {forwardRef, useImperativeHandle, useRef} from 'react'
type MyInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string
}
function MyInput(props: MyInputProps, ref: any) {
  const {label, ...other} = props
  const inputRef = useRef<any>(null)
  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus()
      }
    }
  }, [])
  return (
    <label>
      {label}
      <input {...other} ref={inputRef} />
    </label>
  )
}
export default forwardRef(MyInput)
useImperativeHandle(
  ref, 
  createHandle, 
  dependencies?
)

useImperativeHandle 接收三个参数,分别是


  • ref: 组件声明时传入的 ref
  • createHandle: 回调函数,需要返回 ref 引用的对象,我们也是在这里重写 ref 引用
  • deps: 依赖项数组,可选。state,props 以及内部定义的其他变量都可以作为依赖项,React 内部会使用 Object.is 来对比依赖项是否发生了变化。依赖项发生变化时,createHandle 会重新执行,ref 引用会更新。如果不传入依赖项,那么每次更新 createHandle 都会重新执行


useImperativeHandle 执行本身返回 undefined


04官方案例


官方文档中有这种一个案例,效果如图所示。当点击按钮时,我希望下方的 input 自动获得焦点,并切中间的滚动条滚动到最底部。

现在,我们结合前面的知识来分析一下这个案例应该如何实现。


首先我们先进行组件拆分,将整个内容拆分为按钮部分与信息部分,信息部分主要负责信息的暂时与输入,因此页面组件大概长这样

<>
  <button>Write a comment</button>
  <Post />
</>

我们期望点击按钮时,信息部分的输入框自动获取焦点,信息部分的信息展示区域能滚动到最底部,因此整个页面组件的代码可以表示为如下:

import { useRef } from 'react';
import Post from './Post.js';
export default function Page() {
  const postRef = useRef(null);
  function handleClick() {
    postRef.current.scrollAndFocusAddComment();
  }
  return (
    <>
      <button onClick={handleClick}>
        Write a comment
      </button>
      <Post ref={postRef} />
    </>
  );
}

信息部分 Post 又分为两个部分,分别是信息展示部分与信息输入部分


此时这两个部分的 ref 要透传给 Post,并最终再次透传给页面组件。


所以信息展示部分 CommentList 组件的代码为

import { forwardRef, useRef, useImperativeHandle } from 'react';
const CommentList = forwardRef(function CommentList(props, ref) {
  const divRef = useRef(null);
  useImperativeHandle(ref, () => {
    return {
      scrollToBottom() {
        const node = divRef.current;
        node.scrollTop = node.scrollHeight;
      }
    };
  }, []);
  let comments = [];
  for (let i = 0; i < 50; i++) {
    comments.push(<p key={i}>Comment #{i}</p>);
  }
  return (
    <div className="CommentList" ref={divRef}>
      {comments}
    </div>
  );
});
export default CommentList;

信息输入部分 AddComment 的代码为

import { forwardRef, useRef, useImperativeHandle } from 'react';
const AddComment = forwardRef(function AddComment(props, ref) {
  return <input placeholder="Add comment..." ref={ref} />;
});
export default AddComment;

Post 要把他们整合起来

import { forwardRef, useRef, useImperativeHandle } from 'react';
import CommentList from './CommentList.js';
import AddComment from './AddComment.js';
const Post = forwardRef((props, ref) => {
  const commentsRef = useRef(null);
  const addCommentRef = useRef(null);
  useImperativeHandle(ref, () => {
    return {
      scrollAndFocusAddComment() {
        commentsRef.current.scrollToBottom();
        addCommentRef.current.focus();
      }
    };
  }, []);
  return (
    <>
      <article>
        <p>Welcome to my blog!</p>
      </article>
      <CommentList ref={commentsRef} />
      <AddComment ref={addCommentRef} />
    </>
  );
});
export default Post;

这样,我们整个案例的代码就写完了。useRef、useImprativeHandle、forwardRef 一起配合帮助我们完成了这个功能。


05Lottie


我上上周周末直播分享了在小程序中如何实现 lottie 动画并封装成为简单易用的 React 组件。具体效果如下

组件封装好之后使用如下:

<Lottie
  data={profile}
  ref={l4}
  id="mt"
  width={40}
  height={20}
/>

使用时,我需要拿到内部的 lottie 对象,以通过 lottie.stop() 的方式来控制 lottie 动画的开始、暂停、停止等行为。此时使用 useImprativeHandle 来重写 ref 引用是最好的方式


因此内部实现代码为

function Lottie(props: LottieProps, ref) {
  const lottieRef = useRef({} as LoadAnimationReturnType)
  const {data, path, loop = false, autoplay = false, width = 300, height = 150, id} = props
  useImperativeHandle(ref, () => {
    return lottieRef.current
  })
  useEffect(() => {
    const s = `#${id}`
    const q = createSelectorQuery()
    q.select(s).fields({ node: true, size: true }).exec(res => {
      if (!res[0]) {
        return console.log('节点获取失败')
      }
      const canvas = res[0].node
      const ctx = canvas.getContext('2d')
      lottie.setup(canvas)
      lottieRef.current = lottie.loadAnimation({
        loop: loop,
        autoplay: autoplay,
        animationData: data, //具有导出的动画数据的对象
        path,
        rendererSettings: {
          context: ctx
        },
      })
    })
  }, [])
  return (
    <Canvas type='2d' id={id} style={{width, height}} />
  )  
}
export default forwardRef(Lottie)

这样封装好之后,我们就可以通过 ref 在父组件中去调用 lottie 对象的所有操作行为。


06总结


通过这几个案例,我们明显能够感受到,ref、useImperativeHandle、forwardRef 结合起来所能发挥的想象空间远不止于此。这种方式给 React 提供了一种扩展 React 能力的重要手段,因此当你成为 React 高手之后,你一定会非常喜欢使用他们,他们的组合能让 React 项目变得更加多样化


接下来的一篇文章,我们将会分享useReducer ,一个在 React 进阶过程中,不得不面对的一个重要难点,与它极为相似的 redux 曾经是大多数 React 学习者的终极拦路虎,有的人甚至谈他色变,还好后来 React hooks 流行起来,才让所有人都松了一口气。但对于个人能力而言,对它的掌握,依然十分关键!

相关文章
|
7月前
|
Web App开发 前端开发 JavaScript
React技能
【5月更文挑战第27天】React技能
49 1
|
7月前
|
前端开发
【边做边学】系统解读一下React Hooks
【边做边学】系统解读一下React Hooks
|
前端开发 JavaScript 算法
react基础理论
react基础理论
61 0
|
7月前
|
前端开发 JavaScript
你可能需要的React开发小技巧!(下)
你可能需要的React开发小技巧!(下)
|
7月前
|
前端开发 API
你可能需要的React开发小技巧!(上)
你可能需要的React开发小技巧!(上)
|
存储 资源调度 前端开发
如何高效学习React:探索React的魅力与实践
React作为目前最受欢迎的前端框架之一,具有强大的性能和灵活的组件化开发方式,是每个前端开发者必须掌握的技能之一。本文将介绍一些学习React的有效方法,帮助读者快速入门并掌握React的核心概念与实践。
|
JavaScript 前端开发 算法
|
存储 JSON 前端开发
React系列教程(2)React哲学
React系列教程(2)React哲学
97 0
|
前端开发 JavaScript
聊聊 React 中被低估的 useSyncExternalStore Hooks
聊聊 React 中被低估的 useSyncExternalStore Hooks
284 0
|
前端开发
React项目经验总结
React项目经验总结
React项目经验总结