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

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

3.21 useScript

import useAsync from "@hooks/useAsync";
export default function useScript(url: string) {
  return useAsync(() => {
    const script = document.createElement("script");
    script.src = url;
    script.async = true;
    return new Promise<void>((resolve, reject) => {
      script.addEventListener("load", () => resolve());
      script.addEventListener("error", () => reject());
      document.body.appendChild(script);
    });
  }, [url]);
}

useScript它具备异步处理脚本加载的能力。通过将脚本的async属性设置为true,确保它不会阻塞应用程序的渲染。特别是在处理较大的脚本或较慢的网络连接时,有很大用处。

使用场景

useScript可以用于各种情景。例如,我们可以加载外部库,如jQuery,从而能够利用其强大的功能,而不会增加捆绑文件的体积。此外,我们还可以加载分析脚本或应用程序动态行为所需的任何其他脚本。

import useScript from "@hooks/useScript";
export default function ScriptComponent() {
  const { loading, error } = useScript(
    "https://code.jquery.com/jquery-3.6.0.min.js"
  );
  if (loading) return <div>资源加载中...</div>;
  if (error) return <div>资源加载失败😡</div>;
  return <div>显示当前视图的宽度{window.$(window).width()}</div>;
}

注意:在使用$处会有一个TS错误。我们需要在项目中弄一个index.d.ts然后需要对$进行定义

declare interface Window {
  $: any;
}

该钩子返回加载状态和错误状态,可以用于相应地显示加载中的旋转图标或错误消息。一旦脚本成功加载,组件将使用jQuery显示当前窗口宽度。


3.22 useStateWithHistory

import { useCallback, useRef, useState, Dispatch, SetStateAction } from "react";
type HistoryAction<T> = {
  history: T[];
  pointer: number;
  back: () => void;
  forward: () => void;
  go: (index: number) => void;
};
type StateWithHistoryReturn<T> = [
  T,
  Dispatch<SetStateAction<T>>,
  HistoryAction<T>
];
function useStateWithHistory<T>(
  defaultValue: T,
  capacity: number = 10
): StateWithHistoryReturn<T> {
  const [value, setValue] = useState<T>(defaultValue);
  const historyRef = useRef<T[]>([value]);
  const pointerRef = useRef<number>(0);
  const set = useCallback(
    (v: SetStateAction<T>) => {
      const resolvedValue =
        typeof v === "function" ? (v as (prevState: T) => T)(value) : v;
      if (historyRef.current[pointerRef.current] !== resolvedValue) {
        if (pointerRef.current < historyRef.current.length - 1) {
          historyRef.current.splice(pointerRef.current + 1);
        }
        historyRef.current.push(resolvedValue);
        while (historyRef.current.length > capacity) {
          historyRef.current.shift();
        }
        pointerRef.current = historyRef.current.length - 1;
      }
      setValue(resolvedValue);
    },
    [capacity, value]
  );
  const back = useCallback(() => {
    if (pointerRef.current <= 0) return;
    pointerRef.current--;
    setValue(historyRef.current[pointerRef.current]);
  }, []);
  const forward = useCallback(() => {
    if (pointerRef.current >= historyRef.current.length - 1) return;
    pointerRef.current++;
    setValue(historyRef.current[pointerRef.current]);
  }, []);
  const go = useCallback((index: number) => {
    if (index < 0 || index > historyRef.current.length - 1) return;
    pointerRef.current = index;
    setValue(historyRef.current[pointerRef.current]);
  }, []);
  const historyAction: HistoryAction<T> = {
    history: historyRef.current,
    pointer: pointerRef.current,
    back,
    forward,
    go,
  };
  return [value, set, historyAction];
}
export default useStateWithHistory;

useStateWithHistory的优势

  • 自动历史跟踪:useStateWithHistory自动跟踪我们设置的值,允许我们在需要时访问完整的历史记录
  • 高效的内存使用:该钩子利用容量参数(支持动态传人),确保历史记录不会无限增长。我们可以定义要保留的历史值的最大数量,防止过多的内存消耗。
  • 时间旅行功能:通过back()forward()go()函数,我们可以轻松地浏览记录的历史。在以前的状态之间来回切换,或直接跳到特定索引,实现强大的撤销/重做或逐步操作功能。

何时使用useStateWithHistory

  • 表单管理:通过提供一种简化处理表单输入的方式,可以跟踪更改,还原以前的值或重做修改,从而简化处理表单输入的过程。
  • 撤销/重做功能:轻松实现应用程序中的撤销/重做功能。跟踪状态更改,允许用户轻松地在其操作之间来回导航。
  • 逐步导航:使用useStateWithHistory构建交互式指南或教程,用户可以在不同步骤之间导航,同时保留其进度。

使用场景

import useStateWithHistory from "@hooks/useStateWithHistory";
export default function StateWithHistoryComponent() {
  const [count, setCount, { history, pointer, back, forward, go }] =
    useStateWithHistory(1);
  return (
    <div>
      <div>当前指针所指位置的数值:{count}</div>
      <div>History的所有值{history.join(", ")}</div>
      <div>指针指向的Index(从0开始):{pointer}</div>
      <button onClick={() => setCount((currentCount) => currentCount * 2)}>
        将之前的数据数值翻倍后,插入到History
      </button>
      <button onClick={() => setCount((currentCount) => currentCount + 1)}>
        将之前的数据数值+1后,插入到History
      </button>
      <button onClick={back}>回退</button>
      <button onClick={forward}>前进</button>
      <button onClick={() => go(2)}>指向第二步</button>
    </div>
  );
}

3.23 useStateWithValidation

import { useState, useCallback } from "react";
export default function useStateWithValidation<T>(
  validationFunc: (value: T) => boolean,
  initialValue: T
): [T, (nextState: T | ((prevState: T) => T)) => void, boolean] {
  const [state, setState] = useState<T>(initialValue);
  const [isValid, setIsValid] = useState(() => validationFunc(state));
  const onChange = useCallback(
    (nextState: T | ((prevState: T) => T)) => {
      const value =
        typeof nextState === "function"
          ? (nextState as (prevState: T) => T)(state)
          : nextState;
      setState(value);
      setIsValid(validationFunc(value));
    },
    [validationFunc, state]
  );
  return [state, onChange, isValid];
}

useStateWithValidation钩子结合了React的useStateuseCallback钩子,它接受两个参数:

  • 一个验证函数(用于确定当前状态是否被视为有效。)
  • 一个初始值

使用场景

我们可以传递适合我们特定需求的任何验证函数。无论是检查字符串的长度,确保数字值在特定范围内,还是执行更复杂的验证,useStateWithValidation都可以满足我们的需求。

import useStateWithValidation from "@hooks/useStateWithValidation";
export default function StateWithValidationComponent() {
  const [username, setUsername, isValid] = useStateWithValidation<string>(
    (name) => name.length > 5,
    "前端柒八九"
  );
  return (
    <>
      <div>输入框内容是否大于5: {isValid.toString()}</div>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
    </>
  );
}

在这个示例中,使用useStateWithValidation钩子来管理用户名的状态。验证函数检查用户名的长度是否大于5个字符,isValid变量反映了当前输入的有效性。


3.24 useTranslation

import { useLocalStorage } from "@hooks/useStorage";
import * as translations from "./translations";
type TranslationFunction = (key: string) => string | undefined;
export default function useTranslation(lang: string, fallbackLang: string) {
  const [language, setLanguage] = useLocalStorage<string>("language", lang);
  const [fallbackLanguage, setFallbackLanguage] = useLocalStorage<string>(
    "fallbackLanguage",
    fallbackLang
  );
  const translate: TranslationFunction = (key) => {
    const keys = key.split(".");
    return (
      getNestedTranslation(language, keys) ??
      getNestedTranslation(fallbackLanguage, keys) ??
      key
    );
  };
  return {
    language,
    setLanguage,
    fallbackLanguage,
    setFallbackLanguage,
    t: translate,
  };
}
function getNestedTranslation(
  language: string,
  keys: string[]
): string | undefined {
  return keys.reduce((obj, key) => {
    return obj?.[key];
  }, translations[language]);
}

我们可以定义一组语言集合(translations文件夹中)

en.json

{
  "hi": "Hello",
  "bye": "Goodbye",
  "nested": {
    "value": "front789"
  }
}

zh.json

{
  "hi": "你好",
  "bye": "再见👋",
  "nested": {
    "value": "前端柒八九"
  }
}

并在index.js中导出

export * as en from "./en.json";
export * as zh from "./zh.json";

它会自动保存用户选择的语言和回退语言,因此用户每次访问我们的应用时都会看到他们喜好的语言内容。

该钩子利用了 useStorage 库的 useLocalStorage 钩子来持久保存语言设置。这确保即使用户刷新页面或导航离开并返回,他们的语言偏好也将得以保留。

当然,市面上也有很多优秀的库。例如react-i18next。这个就看大家的实际情况,酌情使用了。

使用场景

我们将能够访问当前语言、设置语言、回退语言以及设置回退语言的功能。此外,该钩子还提供了一个便捷的翻译函数 t,它以key作为输入并返回相应的翻译值。

无论我们正在构建多语言网站、国际化应用程序,还是仅需要支持 UI 组件的翻译,该钩子都将简化流程并使我们的代码更易维护。

import useTranslation from "@hooks/useTranslation";
export default function TranslationComponent() {
  const { language, setLanguage, fallbackLanguage, setFallbackLanguage, t } =
    useTranslation("zh", "en");
  return (
    <>
      <div>使用{language}</div>
      <div>{t("hi")}</div>
      <div>{t("bye")}</div>
      <div>{t("nested.value")}</div>
      <button onClick={() => setLanguage("zh")}>切换中文</button>
      <button onClick={() => setLanguage("en")}>切换英文</button>
      <div>喜好的语音{fallbackLanguage}</div>
      <button onClick={() => setFallbackLanguage("zh")}>
        切换到喜好的语言
      </button>
    </>
  );
}


3.25 useUpdateEffect

import { useEffect, useRef } from "react";
type EffectHookType = typeof useEffect;
const createUpdateEffect: (effect: EffectHookType) => EffectHookType =
  (effect) => (callback, deps) => {
    const isMounted = useRef(false);
    // 处理刷新
    effect(() => {
      return () => {
        isMounted.current = false;
      };
    }, []);
    effect(() => {
      if (!isMounted.current) {
        isMounted.current = true;
      } else {
        return callback();
      }
    }, deps);
  };
export default createUpdateEffect(useEffect);

useUpdateEffect 钩子旨在仅在初始渲染后执行回调函数。这种行为在我们希望基于状态更改执行操作,同时跳过初始执行时特别有用。通过利用 useRef 钩子,useUpdateEffect 跟踪首次渲染,并在该阶段跳过回调。

使用场景

这个自定义钩子可以在各种场景中使用。例如,我们有一个计数器组件,每当计数更改时需要显示警报,但要排除初始渲染。

import { useState } from "react";
import useUpdateEffect from "@hooks/useUpdateEffect";
export default function UpdateEffectComponent() {
  const [count, setCount] = useState(10);
  useUpdateEffect(() => alert(count), [count]);
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount((c) => c + 1)}>数字+1</button>
    </div>
  );
}

3.26 useWindowSize

import { useState } from "react";
import useEventListener from "@hooks/useEventListener";
type WindowSize = {
  width: number;
  height: number;
};
export default function useWindowSize(): WindowSize {
  const [windowSize, setWindowSize] = useState<WindowSize>({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  useEventListener("resize", () => {
    setWindowSize({ width: window.innerWidth, height: window.innerHeight });
  });
  return windowSize;
}

使用useWindowSize我们就可以访问包含窗口当前宽度和高度的对象,使我们能够专注于构建动态和响应式界面。

此包还包括 useEventListener 钩子,它智能地侦听窗口调整大小事件。每当窗口大小更改时,useWindowSize 更新状态以反映最新的尺寸,触发消耗组件的重新渲染。

使用场景

useWindowSize 钩子可以用于各种场景。在构建适应不同屏幕尺寸的响应式布局时,它特别有用。借助此钩子,我们可以根据可用的窗口空间轻松调整组件的样式、布局或内容。此外,它使我们能够根据窗口尺寸动态渲染或隐藏元素,优化图像加载或执行依赖于窗口尺寸的任何其他行为。

import useWindowSize from "@hooks/useWindowSize";
export default function WindowSizeComponent() {
  const { width, height } = useWindowSize();
  return (
    <div>
      {width} x {height}
    </div>
  );
}

3.27 useDeepCompareEffect

import { useRef, useEffect, DependencyList } from "react";
import isEqual from "lodash/fp/isEqual";
type EffectHookType = typeof useEffect;
type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType;
export const createDeepCompareEffect: CreateUpdateEffect =
  (effect) => (callback, deps) => {
    const ref = useRef<DependencyList>();
    const signalRef = useRef<number>(0);
    if (deps === undefined || !isEqual(deps, ref.current)) {
      ref.current = deps;
      signalRef.current += 1;
    }
    effect(callback, [signalRef.current]);
  };
export default createDeepCompareEffect(useEffect);

在React中管理依赖关系是一件很棘手的事情,尤其是在处理复杂的数据结构或嵌套对象时。为了解决默认useEffect钩子的限制,useDeepCompareEffect确保仅当依赖关系发生深层更改时才触发效果回调,它使用lodashisEqual函数进行准确的比较。

它能够防止不必要的重新渲染。通过在当前依赖项和先前依赖项之间执行深层比较,该钩子智能地确定是否应触发效果,从而在浅层比较无法胜任的情况下实现了性能优化。

使用场景

这个自定义钩子在处理复杂的状态对象时特别有用,比如当你有深层嵌套的数据结构或需要跟踪多个相互关联的状态时。它使你能够定义准确反映你想要跟踪的特定更改的依赖关系,确保只有在绝对必要时才执行效果。

import React, { useEffect, useState, useRef } from "react";
import useDeepCompareEffect from "@hooks/useDeepCompareEffect";
export default function DeepCompareEffectComponent() {
  const [age, setAge] = useState<number>(0);
  const [otherCount, setOtherCount] = useState<number>(0);
  const useEffectCountRef = useRef<HTMLSpanElement>(null);
  const useDeepCompareEffectCountRef = useRef<HTMLSpanElement>(null);
  const person = { age: age, name: "Sergey" };
  useEffect(() => {
    if (useEffectCountRef.current) {
      useEffectCountRef.current.textContent = (
        parseInt(useEffectCountRef.current.textContent || "0") + 1
      ).toString();
    }
  }, [person]);
  useDeepCompareEffect(() => {
    if (useDeepCompareEffectCountRef.current) {
      useDeepCompareEffectCountRef.current.textContent = (
        parseInt(useDeepCompareEffectCountRef.current.textContent || "0") + 1
      ).toString();
    }
  }, [person]);
  return (
    <div>
      <div>
        useEffect被触发的次数: <span ref={useEffectCountRef}>0</span>
      </div>
      <div>
        useDeepCompareEffect被触发的次数:
        <span ref={useDeepCompareEffectCountRef}>0</span>
      </div>
      <div>不相干的值: {otherCount}</div>
      <div>{JSON.stringify(person)}</div>
      <button onClick={() => setAge((currentAge) => currentAge + 1)}>
        修改监听对象中的值
      </button>
      <button onClick={() => setOtherCount((count) => count + 1)}>
        修改和监听对象无关的值
      </button>
    </div>
  );
}

后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关文章
|
3月前
|
自然语言处理 前端开发 JavaScript
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
28 0
|
3月前
|
存储 前端开发 数据可视化
美丽的公主和它的27个React 自定义 Hook(三)
美丽的公主和它的27个React 自定义 Hook(三)
|
3月前
|
存储 JSON 前端开发
美丽的公主和它的27个React 自定义 Hook(二)
美丽的公主和它的27个React 自定义 Hook(二)
|
3月前
|
前端开发 JavaScript API
美丽的公主和它的27个React 自定义 Hook(一)
美丽的公主和它的27个React 自定义 Hook(一)
|
3月前
|
前端开发
react hook 生命周期
react hook 生命周期
26 0
|
前端开发
React Native自定义导航栏
之前我们学习了可触摸组件和页面导航的使用的使用: 从零学React Native之09可触摸组件 … 从零学React Native之03页面导航 … 经过之前的学习, 我们可以完成一个自定义导航栏了, 效果如下: 我们需要创建一个 NaviBar.js 用来显示顶部的导航栏, 还需要四个界面(Page1.js,Page2.js,Page3.js,Page4.js
1526 0
|
2月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
52 0
|
2月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
21 0
|
2月前
|
前端开发
【第31期】一文学会用React Hooks组件编写组件
【第31期】一文学会用React Hooks组件编写组件
24 0
|
2月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
24 0

相关产品

  • 云迁移中心