美丽的公主和它的27个React 自定义 Hook(一)

简介: 美丽的公主和它的27个React 自定义 Hook(一)

希望是厄运的忠实的姐妹。——普希金

大家好,我是柒八九

前言

在上一篇git 原理中我们在前置知识点中随口提到了Hook。其中,就有我们比较熟悉的React Hook

image.png

而针对React Hook而言,除了那些让人眼花缭乱的内置hook。其实,它最大的魅力还是自定义hook

所以,今天我们就来讲几个,我们平时开发中可能会用到的自定义hook。(文章内容可能有些长,请大家耐心观看,也可以先收藏后享用哦 😊)

当然,其实业界已经有很好的开源库,功能也强大的很多。(例如:ahooks)。但是它有一些让人诟病的问题,首先,有些功能其实我们在开发中不经常使用,并且引入了第三方库,反而使我们项目变得臃肿;其次,在开发中,我有一个比较执拗的做法,也就是别人的永远都是别人的。只有自己真正懂了,才是自己的。所以,大部分的工具库,我都选择手搓。(当然,也还没到了固执己见的地步,有些合适的库还是会用的)

所以,今天这篇文章,就给大家罗列一些在开发中,可能会用到并且能帮助到大家的自定义Hook

还有之前我们也有React相关的文章,大家可以自行获取:

  1. React_Fiber机制(上)
  2. React_Fiber机制(下)
  3. React 元素 VS 组件
  4. React-全局状态管理的群魔乱舞
  5. 构建面向未来的前端架构
  6. React 18 如何提升应用性能
  7. React Server Components手把手教学
  8. React 并发原理
  9. 在React项目中使用CSS Module
  10. React Memo不是你优化的第一选择

好了,天不早了,干点正事哇。


我们能所学到的知识点

  1. 前置知识点
  2. React Hook 解析
  3. React 自定义 Hook

1. 前置知识点

前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略


同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用

React 内置Hook

以下是React提供的一些标准内置Hooks。你能相信,现在有15个之多,如果大家有需要,到时候也可以写一篇关于内置hook的文章。

如果想看更详细的解释可以移步官网

image.png


2. React Hook 解析

追根溯源

在考虑使用Hooks之前,首先要考虑原生JavaScript函数。

JavaScript编程语言中,函数是可重用的代码逻辑,用于执行重复的任务。函数是可组合的,这意味着你可以在另一个函数中调用一个函数并使用其输出

在下图中,someFunction()函数组合(使用)了函数a()b()。函数b()使用了函数c()

image.png

毫无疑问,React中的函数组件实际上就是普通的JavaScript函数!因此,如果函数具有组合性,React组件也可以具有组合性。这意味着我们可以像下面的图像所示,将一个或多个组件组合(使用)到另一个组件中

image.png

有状态组件 vs 无状态组件

React中,组件可以是有状态(stateful)或无状态(stateless)的。

  • 一个有状态组件声明并管理本地状态。
  • 一个无状态组件是一个纯函数,它没有本地状态和需要管理的副作用。

一个纯函数是一个没有副作用的函数。这意味着一个函数对于相同的输入始终返回相同的输出。

如果我们从函数组件中移除有状态和副作用逻辑,我们就得到了一个无状态组件。此外,有状态和副作用逻辑可以在应用程序的其他地方进行重复使用。因此,尽量将它们与组件隔离开来是有意义的。

image.png

React Hooks 和 有状态逻辑

通过React Hooks,我们可以将状态逻辑副作用从函数组件中隔离出来。

HooksJavaScript函数,通过将它们与组件隔离开来来管理状态行为和副作用

因此,现在我们可以将所有状态逻辑隔离到Hooks中,并将它们用于组件中(因为Hooks本身也是函数,所以可以组合它们)。

image.png

状态逻辑

它可以是任何需要在本地声明和管理状态变量的内容。

例如,用于获取数据并将数据管理在本地变量中的逻辑是有状态的。我们可能还希望在多个组件中重复使用获取数据的逻辑。

以前,状态逻辑只能在类组件中使用生命周期方法来实现。但是,有了React Hooks,开发人员现在可以在函数组件中直接利用状态和其他React功能。

Hooks提供了一种轻松地在多个组件之间重复使用有状态逻辑的方式,提高了代码的可重用性并减少了复杂性。它们使开发人员能够将复杂的组件拆分成更小、更易管理的部分,从而产生更清晰和更易维护的代码。

useStateuseEffect这样的Hooks允许开发人员轻松地管理组件状态并处理副作用。由于其简单性和灵活性,React Hooks已成为构建现代、高效和可扩展的React应用程序的必备工具。


3. React 自定义 Hook

React自定义Hooks可重复使用的函数,允许开发人员以可重复使用的方式抽象和封装复杂的逻辑,用于共享非可视逻辑的Hooks模式

自定义Hook是通过组合现有的React Hooks或其他自定义Hooks来创建的。

它们允许开发人员从组件中提取通用逻辑,并在应用程序的不同部分之间共享它。自定义Hooks遵循使用use前缀的命名约定,这允许它们利用ReactHooks规则的优势。

通过创建自定义Hooks,开发人员可以模块化和组织他们的代码,使其更易读、易维护和易测试。

这些Hooks可以封装任何类型的逻辑,如API调用、表单处理、状态管理,甚至是抽象外部库。

我们采用Vite构建一个React-TS版本的项目。(yarn create vite my-vue-app --template react-ts

并且在src文件下,新增hooks文件夹,以存储下面我们定义的自定义hook。然后我们通过配置alias可以在组件中随意引入。即import xx from @hooks/xxx

前面我们讲过自定义Hooks是通过组合现有的React Hooks或其他自定义Hooks来创建的,所以下文中会有自定义hook的嵌套现象,大家在阅读的时候,需要甄别代码。(推荐大家还是自己弄一个小项目,自己实践一下)。

还有一点,由于篇幅所限,下面的hook不做过多的解读。我们用了ts,想必通过直接阅读代码,也能比较清晰的了解代码含义和限制。

3.1 useArray

import { useState, Dispatch, SetStateAction } from "react";
export type ArrayReturnType<T> {
  array: T[];
  set: Dispatch<SetStateAction<T[]>>;
  push: (element: T) => void;
  filter: (callback: (value: T, index: number, array: T[]) => boolean) => void;
  update: (index: number, newElement: T) => void;
  remove: (index: number) => void;
  clear: () => void;
}
export default function useArray<T>(defaultValue: T[]): ArrayReturnType<T> {
  const [array, setArray] = useState<T[]>(defaultValue);
  function push(element: T) {
    setArray((a) => [...a, element]);
  }
  function filter(callback: (value: T, index: number, array: T[]) => boolean) {
    setArray((a) => a.filter(callback));
  }
  function update(index: number, newElement: T) {
    setArray((a) => [
      ...a.slice(0, index),
      newElement,
      ...a.slice(index + 1, a.length),
    ]);
  }
  function remove(index: number) {
    setArray((a) => [...a.slice(0, index), ...a.slice(index + 1, a.length)]);
  }
  function clear() {
    setArray([]);
  }
  return { array, set: setArray, push, filter, update, remove, clear };
}

useArrayhook利用ReactuseStatehook来初始化和管理数组状态。它返回一个带有以下函数的对象:

  • push(element): 将指定的元素添加到数组中。
  • filter(callback): 根据提供的回调函数对数组进行筛选,删除不满足条件的元素。
  • update(index, newElement): 用newElement替换指定索引处的元素。
  • remove(index): 从数组中移除指定索引处的元素。
  • clear(): 清空数组,将其设置为空数组。

使用useArray钩子,我们可以轻松地向数组中添加、更新、移除、筛选和清除元素,而无需处理复杂的逻辑。

import React from "react";
import useArray, { ArrayReturnType } from "@hooks/useArray";
// 在组件中使用(这里的使用方式不在赘述)
 const { array, set, push, remove, filter, update, clear }: ArrayReturnType<number> = useArray([
    1, 2, 3, 4, 5, 6,
  ]); 
// 在组件中定义回掉函数,处理相关逻辑

3.2 useAsync

import { useCallback, useEffect, useState } from "react";
export type AsyncReturn<T> = {
  loading: boolean;
  error?: Error | null;
  value?: T;
};
export default function useAsync<T>(
  callback: () => Promise<T>,
  dependencies: unknown[] = []
): AsyncReturn<T> {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error>();
  const [value, setValue] = useState<T | undefined>();
  const callbackMemoized = useCallback(() => {
    setLoading(true);
    setError(undefined);
    setValue(undefined);
    callback()
      .then((result) => setValue(result))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [...dependencies]);
  useEffect(() => {
    callbackMemoized();
  }, [callbackMemoized]);
  return { loading, error, value };
}

useAsync钩子接受一个执行异步操作的回调函数以及一个可选的依赖数组。它返回一个带有三个属性的对象:

  1. loading属性指示操作是否正在进行中
  2. error属性保存在过程中遇到的任何错误消息
  3. value属性包含异步操作的解析值

useAsync使用useCallback记忆回调函数。这确保只有在依赖项发生变化时才会重新创建回调,防止不必要的重新渲染,并优化性能。此外,该钩子使用useStateuseEffect钩子来管理加载状态,并在必要时调用记忆化的回调函数。

使用场景

无论我们是从API获取数据、执行计算还是处理表单提交,这个自定义钩子都简化了在React组件中管理异步操作

import React from "react";
import useAsync, { AsyncReturn } from "@hooks/useAsync";
export default function AsyncComponent() {
  const { loading, error, value }: AsyncReturn<string> = useAsync(() => {
    return new Promise<string>((resolve, reject) => {
      // 这里可以替换成正式场景
      const success = false;
      setTimeout(() => {
        success ? resolve("成功了") : reject("失败了");
      }, 1000);
    });
  });
  return (
    <div>
      <div>Loading: {loading.toString()}</div>
      <div>{error}</div>
      <div>{value}</div>
    </div>
  );
}

3.3 useEventListener

import { RefObject, useEffect, useRef } from "react";
type EventCallback = (e: Event) => void;
export default function useEventListener(
  eventType: string,
  callback: EventCallback,
  element: RefObject<HTMLElement> | EventTarget | null = window
) {
  const callbackRef = useRef<EventCallback | null>(null);
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  useEffect(() => {
    if (element == null) return;
    if (
      !(element instanceof EventTarget) &&
      (element as RefObject<HTMLElement>).current == null
    )
      return;
    const handler = (e: Event) => {
      if (callbackRef.current) {
        callbackRef.current(e);
      }
    };
    if ((element as RefObject<HTMLElement>).current) {
      (element as RefObject<HTMLElement>).current?.addEventListener(
        eventType,
        handler
      );
    } else {
      (element as EventTarget).addEventListener(eventType, handler);
    }
    return () => {
      if ((element as RefObject<HTMLElement>).current) {
        (element as RefObject<HTMLElement>).current?.removeEventListener(
          eventType,
          handler
        );
      } else {
        (element as EventTarget).removeEventListener(eventType, handler);
      }
    };
  }, [eventType, element]);
}

使用useEventListener我们可以指定事件类型回调函数,甚至要附加事件侦听器的元素(可以是ref也可以是dom)。这允许我们根据特定需求定制事件处理,提高了代码的可重用性。

该钩子还利用useRef钩子维护对回调函数的稳定引用。这确保了在组件的生命周期中即使回调函数发生变化,也使用最新版本的回调。这种动态行为使我们能够精确处理事件并响应应用程序状态的变化。

使用场景

useEventListener钩子可以在各种情况下使用。无论我们需要捕获键盘事件监听滚动事件或与用户输入交互,这个钩子都可以胜任。

import { useState } from "react";
import useEventListener from "@hooks/useEventListener";
export default function EventListenerComponent() {
  const [key, setKey] = useState<string>("");
  useEventListener("keydown", (e: Event) => {
    if (e instanceof KeyboardEvent) {
      setKey(e.key);
    }
  });
  return <div> {key} </div>;
}

上面示例中,useEventListener利用这个钩子来跟踪用户按下的最后一个键。


3.4 useClickOutside

// 复用了上面的useEventListener钩子
import useEventListener from "@hooks/useEventListener";
import React from "react";
export default function useClickOutside(
  ref: React.RefObject<HTMLElement>,
  cb: (e: MouseEvent) => void,
  triggerRef?: React.RefObject<HTMLElement>
) {
  useEventListener(
    "click",
    (e) => {
      if (
        ref.current == null ||
        ref.current.contains(e.target as Node) ||
        triggerRef.current?.contains(e.target as Node)
      )
        return;
      cb(e as unknown as MouseEvent);
    },
    document
  );
}

useClickOutside钩子简化了检测点击事件是否发生在指定组件之外的过程。通过利用useEventListener钩子,它document级别监听点击事件,允许我们在发生在提供的组件引用之外的点击时触发回调函数。

只需将钩子导入到我们的组件中,并传递所需组件的引用回调函数,还有一个可选项-triggerRef

使用场景

useClickOutside的潜在应用场景是无限的。在实现唤起弹窗下拉菜单或任何在用户与其之外的任何元素交互时应该关闭的元素时,它特别有用。

下面示例中,我们特意将button放置在Modal之外,想必这也符合大家平时开发的模式。(所以,我们单独处理button的点击,也就是需要有一个triggerRef)。其实,我们完全可以将button放置在modal内部,做一个主动唤起的处理。(这在之前的文章中有介绍过,这里就不做展示了)

import { useRef, useState } from "react";
import useClickOutside from "@hooks/useClickOutside";
export default function ClickOutsideComponent() {
  const [open, setOpen] = useState<boolean>(false);
  const modalRef: React.RefObject<HTMLDivElement> = useRef(null);
  const triggerRef: React.RefObject<HTMLButtonElement> = useRef(null);
  useClickOutside(
    modalRef,
    () => {
      if (open) setOpen(false);
    },
    triggerRef
  );
  return (
    <>
      <button onClick={() => setOpen(true)} ref={triggerRef}>
        打开弹窗
      </button>
      <div
        ref={modalRef}
        style={{
          display: open ? "block" : "none",
          backgroundColor: "blue",
          color: "white",
          width: "100px",
          height: "100px",
          position: "absolute",
          top: "calc(50% - 50px)",
          left: "calc(50% - 50px)",
        }}
      >
        <span>我是一个萌萌哒的弹窗</span>
      </div>
    </>
  );
}

上面的情况,利用该钩子来切换弹窗的可见性。

  • 点击button时候,弹窗开启,将open状态设置为true
  • 当用户在弹窗外点击(排除button)时,提供的回调函数将open状态设置为false,关闭窗口。
相关文章
|
15天前
|
前端开发 JavaScript 定位技术
Docusaurus框架——react+antd+echarts自定义mdx生成图表代码解释文档
Docusaurus框架——react+antd+echarts自定义mdx生成图表代码解释文档
28 0
|
16天前
|
前端开发 JavaScript CDN
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏(一)
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏
30 0
|
28天前
|
前端开发 JavaScript
【边做边学】React Hooks (二)——useEffect Hook
【边做边学】React Hooks (二)——useEffect Hook
|
1月前
|
前端开发 JavaScript
React中useEffect Hook使用纠错
React中useEffect Hook使用纠错
17 0
|
5月前
|
自然语言处理 前端开发 JavaScript
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
52 0
|
5月前
|
自然语言处理 前端开发 JavaScript
美丽的公主和它的27个React 自定义 Hook(四)
美丽的公主和它的27个React 自定义 Hook(四)
|
5月前
|
存储 前端开发 数据可视化
美丽的公主和它的27个React 自定义 Hook(三)
美丽的公主和它的27个React 自定义 Hook(三)
|
18天前
|
前端开发 测试技术 开发工具
探索前端框架React Hooks的优势与应用
本文将深入探讨前端框架React Hooks的优势与应用。通过分析React Hooks的特性以及实际应用案例,帮助读者更好地理解和运用这一现代化的前端开发工具。
|
11天前
|
开发框架 Dart 前端开发
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
【4月更文挑战第30天】对比 Flutter(Dart,强类型,Google支持,快速热重载,高性能渲染)与 React Native(JavaScript,庞大生态,热重载,依赖原生渲染),文章讨论了开发语言、生态系统、性能、开发体验、学习曲线、社区支持及项目选择因素。两者各有优势,选择取决于项目需求、团队技能和长期维护考虑。参考文献包括官方文档和性能比较文章。
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
|
10天前
|
前端开发 JavaScript 开发者
【专栏:HTML与CSS前端技术趋势篇】前端框架(React/Vue/Angular)与HTML/CSS的结合使用
【4月更文挑战第30天】前端框架React、Vue和Angular助力UI开发,通过组件化、状态管理和虚拟DOM提升效率。这些框架与HTML/CSS结合,使用模板语法、样式管理及组件化思想。未来趋势包括框架简化、Web组件标准采用和CSS在框架中角色的演变。开发者需紧跟技术发展,掌握新工具,提升开发效能。