React Hooks小记(四)_useImperativeHandle

useImperativeHandle

【写在前面】

​ 1、直接使用 ref 获取 DOM 实例,会全面暴露 DOM 实例上的 API,从而导致外部使用 ref 时有更大的自由度。

​ 2、在实际开发中,我们应该严格控制 ref 的暴露颗粒度,控制它能调用的方法,只向外暴露主要的功能函数,其它功能函数不暴露。

​ 3、React 官方提供 useImperativeHandle 的目的,就是让你在使用 ref 时可以自定义暴露给外部组件哪些功能函数或属性。

语法结构如下:

useImperativeHandle(通过forwardRef接收到的父组件的ref对象, () => 自定义ref对象, [依赖项数组])

其中,第三个参数(依赖项数组)是可选的。

1. useImperativeHandle 的基本使用

在被 React.forwardRef() 包裹的组件中,需要结合 useImperativeHandle 这个 hooks API,向外按需暴露子组件内的成员:

import React, { useRef, useState, useImperativeHandle } from 'react'

// 子组件
const Child = React.forwardRef((_, ref) => {
  const [count, setCount] = useState(0)

  const add = (step: number) => {
    setCount((prev) => (prev += step))
  }

  // 1. 向外暴露一个空对象
  // 2. useImperativeHandle(ref, () => ({}))
  // 向外暴露一个对象,其中包含了 name 和 age 两个属性
  useImperativeHandle(ref, () => ({
    name: 'liulongbin',
    age: 22
  }))

  return (
    <>
      <h3>Child 子组件 {count}</h3>
      <button onClick={() => add(-1)}>-1</button>
      <button onClick={() => add(1)}>+1</button>
    </>
  )
})

2. 基于 useImperativeHandle 按需向外暴露成员

在子组件中,向外暴露 count 和 setCount 这两个成员:

// 子组件
const Child = React.forwardRef((_, ref) => {
  const [count, setCount] = useState(0)

  const add = (step: number) => {
    setCount((prev) => (prev += step))
  }

  // 向外暴露 count 的值和 setCount 函数
  useImperativeHandle(ref, () => ({
    count,
    setCount
  }))

  return (
    <>
      <h3>Child 子组件 {count}</h3>
      <button onClick={() => add(-1)}>-1</button>
      <button onClick={() => add(1)}>+1</button>
    </>
  )
})

在父组件中,添加一个重置按钮,当点击重置按钮时,调用 ref 向外暴露的 setCount 函数,把子组件内部的 count 重置为 0。示例代码如下:

// 父组件
export const Father: React.FC = () => {
  const childRef = useRef<{ count: number; setCount: (value: number) => void }>(null)

  // 按钮的点击事件处理函数
  const onShowRef = () => {
    console.log(childRef.current)
  }

  // 重置按钮的点击事件处理函数
  const onReset = () => {
    childRef.current?.setCount(0)
  }

  return (
    <>
      <h1>Father 父组件</h1>
      {/* 点击按钮,打印 ref 的值 */}
      <button onClick={onShowRef}>show Ref</button>
      {/* 点击按钮,重置数据为 0 */}
      <button onClick={onReset}>重置</button>
      <hr />
      <Child ref={childRef} />
    </>
  )
}

3. 控制成员暴露的粒度

在 Child 子组件中,我们希望对外暴露一个重置 count 为 0 的函数,而不希望直接把 setCount() 暴露出去,因为父组件调用 setCount() 时可以传任何数值。因此,我们可以基于 useImperativeHandle,向外提供一个 reset() 函数而非直接把 setCount() 暴露出去:

// 子组件
const Child = React.forwardRef((_, ref) => {
  const [count, setCount] = useState(0)

  const add = (step: number) => {
    setCount((prev) => (prev += step))
  }

  // 向外暴露 count 的值和 reset 函数
  useImperativeHandle(ref, () => ({
    count,
    // 在组件内部封装一个重置为 0 的函数,API 的粒度更小
    reset: () => setCount(0)
  }))

  return (
    <>
      <h3>Child 子组件 {count}</h3>
      <button onClick={() => add(-1)}>-1</button>
      <button onClick={() => add(1)}>+1</button>
    </>
  )
})

在父组件中,调用 ref.current.reset() 即可把数据重置为 0:

// 父组件
export const Father: React.FC = () => {
  const childRef = useRef<{ count: number; reset: () => void }>(null)

  // 按钮的点击事件处理函数
  const onShowRef = () => {
    console.log(childRef.current)
  }

  // 重置按钮的点击事件处理函数
  const onReset = () => {
    childRef.current?.reset()
  }

  return (
    <>
      <h1>Father 父组件</h1>
      {/* 点击按钮,打印 ref 的值 */}
      <button onClick={onShowRef}>show Ref</button>
      {/* 点击按钮,重置数据为 0 */}
      <button onClick={onReset}>重置</button>
      <hr />
      <Child ref={childRef} />
    </>
  )
}

4. useImperativeHandle 的第三个参数

再来回顾一下 useImperativeHandle 的参数项:

useImperativeHandle(ref, createHandle, [deps])

1. 第一个参数为父组件传递的 ref

2. 第二个参数是一个函数,返回的对象会自动绑定到 ref 上。 即子组件可以将自己内部的方法或者值通过 useImperativeHandle 添加到父组件中 useRef 定义的对象中;

3. 第三个参数是函数依赖的值(可选)。若 createHandle 函数中使用到了子组件内部定义的变量,则还需要将该变量作为依赖变量成为 useImperativeHandle 的第3个参数;

其中,第三个参数有3种用法:

1. 空数组

​ 只在子组件首次被渲染时,执行 useImperativeHandle 中的 fn 回调,从而把 return 的对象作为父组件接收到的 ref。例如:

import React, { useState, useImperativeHandle } from 'react'

// 子组件
const Child = React.forwardRef((_, ref) => {
  const [count, setCount] = useState(0)

  const add = (step: number) => {
    setCount((prev) => (prev += step))
  }

  // 向外暴露 count 的值和 reset 函数
  useImperativeHandle(
    ref,
    () => {
      // 这个 console 只执行1次,哪怕 count 值更新了,也不会重新执行
      // 导致的结果是:外界拿到的 count 值,永远是组件首次渲染时的初始值 0
      console.log('执行了 useImperativeHandle 的回调')
      return {
        count,
        reset: () => setCount(0)
      }
    },
    []
  )

  return (
    <>
      <h3>Child 子组件 {count}</h3>
      <button onClick={() => add(-1)}>-1</button>
      <button onClick={() => add(1)}>+1</button>
    </>
  )
})

2. 依赖项数组

​ 子组件首次被渲染时,在依赖项改变时,会执行 useImperativeHandle 中的 fn 回调,从而让父组件通过 ref 能拿到依赖项的新值。例如:

import React, { useState, useImperativeHandle } from 'react'

// 子组件
const Child = React.forwardRef((_, ref) => {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  const add = (step: number) => {
    setCount((prev) => (prev += step))
  }

  // 向外暴露 count 的值和 reset 函数
  useImperativeHandle(
    ref,
    () => {
      // 每当依赖项 count 值变化,都会触发这个回调函数的重新执行
      // 因此,父组件能拿到变化后的最新的 count 值
      console.log('执行了 useImperativeHandle 的回调')
      return {
        count,
        reset: () => setCount(0)
      }
    },
    // 注意:只有 count 值变化,才会触发回调函数的重新执行
    // flag 值的变化,不会导致回调函数的重新执行,因为 flag 没有被声明为依赖项
    [count]
  )

  return (
    <>
      <h3>Child 子组件 {count}</h3>
      <p>flag 的值是:{String(flag)}</p>
      <button onClick={() => add(-1)}>-1</button>
      <button onClick={() => add(1)}>+1</button>
      {/* 点击按钮,切换布尔值 */}
      <button onClick={() => setFlag((boo) => !boo)}>Toggle</button>
    </>
  )
})

3. 省略依赖项数组(省略第三个参数):

​ 此时,组件内任何 state 的变化,都会导致 useImperativeHandle 中的回调的重新执行。示例代码如下:

import React, { useState, useImperativeHandle } from 'react'

// 子组件
const Child = React.forwardRef((_, ref) => {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  const add = (step: number) => {
    setCount((prev) => (prev += step))
  }

  // 向外暴露 count 的值和 reset 函数
  useImperativeHandle(ref, () => {
    // 只要组件内的任何 state 发生变化,都会触发回调函数的重新执行
    console.log('执行了 useImperativeHandle 的回调')
    return {
      count,
      reset: () => setCount(0)
    }
  })

  return (
    <>
      <h3>Child 子组件 {count}</h3>
      <p>flag 的值是:{String(flag)}</p>
      <button onClick={() => add(-1)}>-1</button>
      <button onClick={() => add(1)}>+1</button>
      {/* 点击按钮,切换布尔值 */}
      <button onClick={() => setFlag((boo) => !boo)}>Toggle</button>
    </>
  )
})

5. 使用 ref 时候的陷阱

陷阱1:不要滥用 ref。

​ 你应当仅在你无法通过 prop 来表达 命令式 行为的时候才使用 ref

例如:滚动到指定节点、聚焦某个节点、触发一次动画,以及选择文本等等。

陷阱2:如果可以通过 prop 实现,那就不应该使用 ref

​ 例如,你不应该从一个 Model 组件暴露出 {open, close} 这样的命令式句柄,最好是像  这样,将 isOpen 作为一个 prop。

​ 副作用可以帮你通过 prop 来暴露一些命令式的行为。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前言如释重负,好用的技术就应该越来越简单React Hooks 是 React 16.8 从提案转为正式加入的新特性。这个新特性是个非常棒的设计。 可以说对于React 技术栈的发展具分割线一样的意义。讲师在课程中提到:之前使用 React 作为主要的前端技术,开发一款网页游戏。在整个游戏的各个模块中,Redux ,mobx,以及蚂蚁金服的 ant-design,dva, umi 这些框架或者第三方库都有涉及使用。但是自从了解了Facebook官方提案的 Hooks 特性后,才真正觉得获得了前所未有的解脱。如果你有React开发经验,学习了解 Hooks 后,一定有一种如释重负的轻松感。React 带来了方便也带来了迷茫相信关心 React Hooks 这项新特性的童鞋,很多已经有了一定的 React 开发经验。那么你一定有所体验,React 给我们带来方便的同时,也的确和长久以来的前端开发模式有极大的不同。React 并不需要用继承,而是推荐用嵌套。React 有独特的 jsx 语法。大多数情况 jsx 都使得我们的代码更加简洁了。然而有些时候也给我们带来了一些困扰。 比如数据的传递,逻辑的复用。 react 是一种 mvvm 的设计模式,作为开发者一定要清楚,那些数据是业务数据,那些数据是UI数据。否则你的代码很有可能会陷入混乱局面。大型项目中模块化与功能解耦困难在公司项目中 App 稍大的时候,我们发现状态提升和只通过 props 进行数据传递。很多时候都很难实现我们的需求。这时无论我们是否清楚的了解,但是状态管理也就是 redux mobx 等,轻易地进入到了公司的项目中。我们经过初期的尝试发现状态管理,确实比用纯粹的 React 带来了数据传递上的方便,以及代码组织上的清晰。但前提是你看懂且理解了 redux 大神晦涩的官网文档。 本来 React 被设计用来组件化前端开发。但当我们初期使用状态管理,我们常常会过度的使用状态数据,业务逻辑和ui逻辑没有清楚的分离,最终你的应用代码结果可能是:除了少数几个组件是独立的解耦的,大多数组件都因为状态数据的共享而耦合在了一起,且他们也完全依赖状态管理框架。无法再轻松的转移复用。使用高阶组件,属性渲染,渲染回调等高级特性,确实可以帮我们解决模块或功能的解耦问题。但是这些方法,确实有点超出普通“猿类”的技能。且降低了代码的可读性,对于团队协作,这是很致命的问题。React Hooks 真正开启前端模块化的金钥匙对于以上问题,React Hooks 都有很好的解决方案,官方的设计动机就是解决这些曾经的繁琐,化繁为简。React Hooks 让我们在纯函数中就可以使用 React 的众多特性。而不必使用类。代码扁平,易读。解耦状态相关逻辑,UI逻辑和业务逻辑更好的分离。这些逻辑往往是纯函数,而以前很容易混合在类组件中。通过自定义 Hooks 我们可以把应用中“状态相关”逻辑解耦出来,独立编写到我们自己的hooks 中。从而更加易于复用和独立测试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值