React自定义Hooks

自定义一些常见的hook,方便在工作中使用,以下内容都是本人工作中的使用经验,不足之处,欢迎指正

useBooleans

ts 复制代码
import { useState } from 'react'

/**
 * @description 切换true/false的公共hook
 * @param {Boolean} initValue 默认值
 */

export interface UseBooleansActions {
  toggle: () => void // 切换值
  set: (value: boolean) => void // 设置值
  setTrue: () => void // 设置为true
  setFalse: () => void // 设置值为false
  reset: () => void // 重置为初始值
}

function useBooleans(initValue = false): [boolean, UseBooleansActions] {
  const [value, setValue] = useState<boolean>(initValue)

  /** 切换值 */
  const toggle = () => {
    setValue((pre) => !pre)
  }

  /** 设置值 */
  const set = (value: boolean) => {
    setValue(value)
  }

  /** 设置为true */
  const setTrue = () => {
    setValue(true)
  }

  /** 设置为false */
  const setFalse = () => {
    setValue(false)
  }

  /** 重置为初始值 */
  const reset = () => {
    setValue(initValue)
  }

  return [value, { toggle, set, setTrue, setFalse, reset }]
}

export default useBooleans

// 使用
const [booleanValue, { toggle, setTrue, setFalse, reset, set }] = useBooleans(true)

useToggle

ts 复制代码
import { useMemo, useState } from 'react'

// 定义操作方法的接口
interface Actions<T, U> {
  toggle: () => void
  set: (value: T | U) => void
  setLeft: () => void // 设置为默认值
  setRight: () => void // 设置为取反值
}

function useToggle<T = boolean>(
  defaultValue?: T,
  reverseValue?: T,
): [T, Actions<T, T>]

function useToggle<T, U>(
  defaultValue: T,
  reverseValue: U,
): [T | U, Actions<T | U, T | U>]

function useToggle<D, R>(
  defaultValue: D = false as unknown as D,
  reverseValue?: R,
) {
  // 状态管理:支持默认值和取反值
  const [state, setState] = useState<D | R>(defaultValue)

  // 计算实际的取反值(若未提供则使用布尔取反)
  const reverseValueOrigin =
    reverseValue === undefined ? !defaultValue : reverseValue

  // 缓存操作方法避免重复创建
  const actions = useMemo(() => {
    // 核心切换逻辑:在默认值和取反值之间切换
    const toggle = () =>
      setState(
        (s) =>
          (s === defaultValue ? reverseValueOrigin : defaultValue) as D | R,
      )
    const set = (value: D | R) => setState(value)
    const setLeft = () => setState(defaultValue)
    const setRight = () => setState(reverseValueOrigin as D | R)

    return { toggle, set, setLeft, setRight }
  }, [defaultValue, reverseValueOrigin])

  return [state, actions]
}

export default useToggle

// 使用
const [toggleValue, { toggle: toggle1, set: set1, setLeft, setRight }] =
    useToggle<'left', 'right'>('left', 'right')

useExcel

基于xlsx的一些常见的导入导出功能封装

ts 复制代码
import { getTypeOf, isAvailableArr } from '@/utils'
import { message } from 'antd'
import type {
  BookType,
  ColInfo,
  Range,
  RowInfo,
  Sheet2JSONOpts,
  WorkBook,
} from 'xlsx'
import * as XLSX from 'xlsx'

// 导出文件配置
export type IExportConfig = {
  name?: string // 导出文件名称
  bookType?: BookType // 导出文件类型
  sheetName?: string // sheet名称
  errorMsg?: string // 错误提示
  headers?: Record<string, string> // 自定义表头,导出的文件里面只有在定义中的字段,并且如果数据为空的话,只生成一个表头。示例:{id: 'ID', name: '链接名称', site_type: '官网类型'}
  merges?: Range[] // 单元格合并
  colInfo?: ColInfo[] // 列属性
  rowInfo?: RowInfo[] // 行属性
}

// 多sheet导出
export type IExtraSheetConfig = {
  name?: string // 导出文件名称
  bookType?: BookType // 导出文件类型
  sheets: ({ json: any[] } & Omit<IExportConfig, 'name' | 'bookType'>)[]
}

/**
 * @desc 自定义导出文件hook
 */
const useExcel = () => {
  /**
   * @desc 一维数组导出Excel文件
   *
   * 此函数将一个一维数组转换为Excel文件并下载
   * 支持自定义表头、单元格合并、列属性和行属性
   *
   * @param {any[]} json - 要导出的数据数组
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.sheetName='Sheet1'] - 工作表名称
   * @param {Record<string, string>} [config.headers] - 自定义表头映射
   * @param {Range[]} [config.merges] - 单元格合并范围
   * @param {ColInfo[]} [config.colInfo] - 列属性配置
   * @param {RowInfo[]} [config.rowInfo] - 行属性配置
   * @returns {void}
   */
  function exportJson2Excel<T = any>(json: T[], config?: IExportConfig) {
    const {
      name = '导出',
      sheetName = 'Sheet1',
      bookType = 'xlsx',
      headers,
      merges,
      colInfo,
      rowInfo,
    } = config || {}
    let lists = [...json]
    if (
      headers &&
      getTypeOf(headers) === 'Object' &&
      isAvailableArr(Object.keys(headers))
    ) {
      // 没有数据的时候用header去生成一个空表头
      if (!isAvailableArr(lists)) {
        const headersField = Object.values(headers)
        const headerObj: Record<string, any> = {}
        headersField.forEach((f) => {
          headerObj[f] = null
        })
        lists = [headerObj as T]
      } else {
        // 有数据的时候根据header去生成
        const headerFields = Object.entries(headers)
        lists = json.map((j) => {
          const obj: Record<string, any> = {}
          for (const [key, value] of headerFields) {
            obj[value] = j[key as keyof T] ?? null
          }
          return obj
        }) as T[]
      }
    }

    const wb = XLSX.utils.book_new()
    const ws = XLSX.utils.json_to_sheet(lists)
    if (merges) {
      ws['!merges'] = merges
    }
    if (colInfo) {
      ws['!cols'] = colInfo
    }
    if (rowInfo) {
      ws['!rows'] = rowInfo
    }
    XLSX.utils.book_append_sheet(wb, ws, sheetName)
    XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
  }

  /**
   * @desc 一维数组多sheet导出Excel文件
   *
   * 此函数将多个一维数组分别放在不同的工作表中导出为一个Excel文件
   * 支持自定义表头、单元格合并、列属性和行属性
   *
   * @param {IExtraSheetConfig} params - 多sheet导出配置项
   * @param {string} [params.name='导出'] - 导出文件名称
   * @param {BookType} [params.bookType='xlsx'] - 导出文件类型
   * @param {Array} params.sheets - 工作表配置数组
   * @param {any[]} params.sheets[].json - 要导出的数据数组
   * @param {string} [params.sheets[].sheetName] - 工作表名称
   * @param {Record<string, string>} [params.sheets[].headers] - 自定义表头映射
   * @param {Range[]} [params.sheets[].merges] - 单元格合并范围
   * @param {ColInfo[]} [params.sheets[].colInfo] - 列属性配置
   * @param {RowInfo[]} [params.sheets[].rowInfo] - 行属性配置
   * @returns {void}
   */
  function exportJson2ExcelSheets<T = any>(params: IExtraSheetConfig) {
    const { name = '导出', bookType = 'xlsx', sheets } = params || {}
    const wb = XLSX.utils.book_new()
    if (isAvailableArr(sheets)) {
      sheets?.forEach((s) => {
        const { json, headers, merges, colInfo, rowInfo, sheetName } = s || {}
        let lists = [...json]
        if (
          headers &&
          getTypeOf(headers) === 'Object' &&
          isAvailableArr(Object.keys(headers))
        ) {
          // 没有数据的时候用header去生成一个空表头
          if (!isAvailableArr(lists)) {
            const headersField = Object.values(headers)
            const headerObj: Record<string, any> = {}
            headersField.forEach((f) => {
              headerObj[f] = null
            })
            lists = [headerObj as T]
          } else {
            // 有数据的时候根据header去生成
            const headerFields = Object.entries(headers)
            lists = json.map((j) => {
              const obj: Record<string, any> = {}
              for (const [key, value] of headerFields) {
                obj[value] = j[key] ?? null
              }
              return obj
            }) as T[]
          }
        }

        const ws = XLSX.utils.json_to_sheet(lists)
        if (merges) {
          ws['!merges'] = merges
        }
        if (colInfo) {
          ws['!cols'] = colInfo
        }
        if (rowInfo) {
          ws['!rows'] = rowInfo
        }
        XLSX.utils.book_append_sheet(wb, ws, sheetName)
      })
    } else {
      const ws = XLSX.utils.json_to_sheet([])
      XLSX.utils.book_append_sheet(wb, ws)
    }

    XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
  }

  /**
   * @desc 二维数组导出Excel文件
   *
   * 此函数将一个二维数组转换为Excel文件并下载
   * 支持单元格合并、列属性和行属性
   *
   * @param {any[][]} aoas - 要导出的二维数组
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.sheetName='Sheet1'] - 工作表名称
   * @param {Range[]} [config.merges] - 单元格合并范围
   * @param {ColInfo[]} [config.colInfo] - 列属性配置
   * @param {RowInfo[]} [config.rowInfo] - 行属性配置
   * @returns {void}
   */
  function exportAoa2Excel<T = any>(aoas: T[][], config?: IExportConfig) {
    const {
      name = '导出',
      sheetName = 'Sheet1',
      bookType = 'xlsx',
      merges,
      colInfo,
      rowInfo,
    } = config || {}

    const wb = XLSX.utils.book_new()
    const ws = XLSX.utils.aoa_to_sheet(aoas)
    if (merges) {
      ws['!merges'] = merges
    }
    if (colInfo) {
      ws['!cols'] = colInfo
    }
    if (rowInfo) {
      ws['!rows'] = rowInfo
    }

    XLSX.utils.book_append_sheet(wb, ws, sheetName)
    XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
  }

  /**
   * @desc 从本地文件读取Excel工作簿
   *
   * 此函数从File或Blob对象读取Excel文件并返回工作簿对象
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @returns {Promise<WorkBook | false>} 返回工作簿对象或false(读取失败时)
   */
  function readWorkbookFromLocalFile(
    file: File | Blob,
  ): Promise<WorkBook | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })
        resolve(workbook)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 从本地Excel文件读取数据为JSON格式
   *
   * 此函数从File或Blob对象读取Excel文件并返回第一个工作表的数据为JSON数组
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @param {Sheet2JSONOpts} [options] - 工作表转JSON的选项
   * @returns {Promise<any[] | false>} 返回JSON数组或false(读取失败时)
   */
  function readFileToJson<T = any>(
    file: File | Blob,
    options?: Sheet2JSONOpts,
  ): Promise<T[] | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })
        const json = XLSX.utils.sheet_to_json<T>(
          workbook.Sheets[workbook.SheetNames[0]],
          options,
        )
        resolve(json)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 从本地Excel文件读取多个工作表的数据为JSON格式
   *
   * 此函数从File或Blob对象读取Excel文件并返回所有工作表的数据为JSON对象
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @param {Sheet2JSONOpts} [options] - 工作表转JSON的选项
   * @returns {Promise<Record<string, any[]> | false>} 返回包含所有工作表数据的对象或false(读取失败时)
   */
  function readFileToJsons(
    file: File | Blob,
    options?: Sheet2JSONOpts,
  ): Promise<Record<string, any[]> | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = e.target?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })

        const sheetNames = workbook.SheetNames
        if (!sheetNames?.length) {
          resolve(false)
        }

        const jsons = sheetNames.reduce(
          (pre: Record<string, any[]>, cur: string) => {
            const json = XLSX.utils.sheet_to_json(workbook.Sheets[cur], options)
            pre[cur] = json
            return pre
          },
          {},
        )

        resolve(jsons)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 文件流导出Excel文件
   *
   * 此函数从File或Blob对象读取Excel文件流并导出为Excel文件
   *
   * @param {File | Blob} file - 要导出的Excel文件流
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.errorMsg='下载失败'] - 错误提示信息
   * @returns {Promise<void>}
   */
  async function exportBuffer2Excel(file: File | Blob, config?: IExportConfig) {
    const {
      name = '导出',
      bookType = 'xlsx',
      errorMsg = '下载失败',
    } = config || {}
    const wb: WorkBook | false = await readWorkbookFromLocalFile(file)
    if (wb) {
      XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
    } else {
      message.error(errorMsg)
    }
  }

  /**
   * @desc 从URL导出Excel文件
   *
   * 此函数从URL获取Excel文件并导出
   *
   * @param {string} url - Excel文件的URL地址
   * @param {IExportConfig} config - 导出配置项
   * @param {string} [config.name='导出'] - 导出文件名称
   * @param {BookType} [config.bookType='xlsx'] - 导出文件类型
   * @param {string} [config.errorMsg='下载失败'] - 错误提示信息
   * @returns {Promise<void>}
   */
  async function exportUrl2Excel(url: string, config?: IExportConfig) {
    fetch(url)
      .then((response) => response.blob())
      .then((blob) => {
        exportBuffer2Excel(new Blob([blob]), config)
      })
      .catch((error) => {
        throw error
      })
  }

  /**
   * @desc 从工作表中读取指定列的数据
   *
   * 此函数从Excel文件的指定工作表中读取指定列的数据
   *
   * @param {File | Blob} file - Excel文件
   * @param {number} [sheetIndex=0] - 工作表索引
   * @param {string[]} [columns=['A']] - 要读取的列数组(如['A', 'B'])
   * @param {Record<string, string>} [fieldsMap={}] - 列名到字段名的映射
   * @returns {Promise<any[] | false>} 返回包含指定列数据的数组或false(读取失败时)
   */
  function readColumnFromSheet(
    file: File | Blob,
    sheetIndex: number = 0,
    columns: string[] = ['A'],
    fieldsMap: Record<string, string> = {},
  ): Promise<any[] | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, {
          type: 'binary',
          raw: true,
          cellNF: true,
        })

        const sheetName = workbook.SheetNames[sheetIndex]
        const worksheet = workbook.Sheets[sheetName]

        // 获取 A 列的所有数据,跳过第一行(标题行)
        const columnData = []
        const range = XLSX.utils.decode_range(worksheet['!ref'] as string)

        // 从第2行开始(索引为1,跳过标题行)
        for (let row = range.s.r + 1; row <= range.e.r; row++) {
          const res: Record<string, any> = {}
          columns.forEach((col: string) => {
            const columnIndex = XLSX.utils.decode_col(col) // 将列字母转换为索引
            const cellAddress = XLSX.utils.encode_cell({
              r: row,
              c: columnIndex,
            }) // A列是第0列
            const cell = worksheet[cellAddress]
            const field = fieldsMap[col] ?? col
            res[field] = cell ? cell.v : ''
          })
          columnData.push(res)
        }

        resolve(columnData)
      }
      reader.onerror = function () {
        resolve(false)
      }
    })
  }

  /**
   * @desc 从工作表中获取指定列的数据
   *
   * 此函数从Excel工作表中获取指定列的所有数据(跳过标题行)
   *
   * @param {XLSX.WorkSheet} worksheet - Excel工作表对象
   * @param {string} column - 要获取数据的列(如'A'、'B')
   * @returns {any[]} 返回指定列的数据数组
   */
  function getColumnData(worksheet: XLSX.WorkSheet, column: string) {
    const columnData = []
    const range = XLSX.utils.decode_range(worksheet['!ref'] as string)
    const columnIndex = XLSX.utils.decode_col(column) // 将列字母转换为索引

    for (let row = range.s.r + 1; row <= range.e.r; row++) {
      const cellAddress = XLSX.utils.encode_cell({ r: row, c: columnIndex })
      const cell = worksheet[cellAddress]
      columnData.push(cell ? cell.v : null)
    }

    return columnData
  }

  /**
   * @desc 从本地文件读取Excel工作簿
   *
   * 此函数从File或Blob对象读取Excel文件并返回工作簿对象
   *
   * @param {File | Blob} file - 要读取的Excel文件
   * @param {XLSX.ParsingOptions} [options] - 解析选项
   * @returns {Promise<WorkBook | false>} 返回工作簿对象或false(读取失败时)
   */
  function readWorkbookFromFile(
    file: File | Blob,
    options: XLSX.ParsingOptions = {
      type: 'binary',
      raw: true,
      cellNF: true,
    },
  ): Promise<WorkBook | false> {
    return new Promise((resolve) => {
      const reader = new FileReader()
      reader.readAsBinaryString(file)
      reader.onload = function (e) {
        const data = (e.target as any)?.result
        const workbook = XLSX.read(data, options)
        resolve(workbook)
      }
    })
  }

  /**
   * @desc 从URL读取Excel工作簿
   *
   * 此函数从URL读取Excel文件并返回工作簿对象
   *
   * @param {string} url - Excel文件的URL地址
   * @param {XLSX.ParsingOptions} [options] - 解析选项
   * @returns {WorkBook} 返回工作簿对象
   */
  function readWorkbookFromUrl(
    url: string,
    options: XLSX.ParsingOptions = {
      type: 'string',
      raw: true,
      cellNF: true,
    },
  ): WorkBook {
    const workbook = XLSX.readFile(url, options)
    return workbook
  }

  /**
   * @desc 从URL读取Excel工作簿
   *
   * 此函数从URL读取Excel文件并返回工作簿对象
   *
   * @param {string} url - Excel文件的URL地址
   * @param {XLSX.ParsingOptions} [options] - 解析选项
   * @returns {WorkBook} 返回工作簿对象
   */
  async function readWorkbookFromUrl1(url: string): Promise<WorkBook | false> {
    try {
      const res = await fetch(url)
      const blob = await res.blob()
      const wb = readWorkbookFromFile(new Blob([blob]))
      return Promise.resolve(wb)
    } catch  {
      return Promise.resolve(false)
    }
  }

  /**
   * @desc 保存工作簿为Excel文件
   *
   * 此函数将工作簿对象保存为Excel文件
   *
   * @param {WorkBook} workbook - 要保存的工作簿对象
   * @param {string} fileName - 文件名
   * @param {XLSX.WritingOptions} [options] - 写入选项
   * @returns {void}
   */
  function saveWbToExcel(
    workbook: WorkBook,
    fileName: string,
    options?: XLSX.WritingOptions,
  ) {
    XLSX.writeFile(workbook, fileName, options)
  }

  return {
    exportJson2Excel,
    exportAoa2Excel,
    exportBuffer2Excel,
    exportJson2ExcelSheets,
    exportUrl2Excel,
    readWorkbookFromLocalFile,
    readFileToJson,
    readFileToJsons,
    readColumnFromSheet,
    getColumnData,
    readWorkbookFromFile,
    readWorkbookFromUrl,
    readWorkbookFromUrl1,
    saveWbToExcel,
  }
}

export default useExcel

// 使用
const {
    exportJson2Excel,
    readFileToJson,
    readFileToJsons,
    readColumnFromSheet,
    readWorkbookFromUrl,
    readWorkbookFromUrl1,
    readWorkbookFromFile,
    saveWbToExcel,
  } = useExcel()

useSet

ts 复制代码
import { useMemo, useState } from 'react'

// 定义操作方法的接口
interface Actions<T> {
  add: (key: T) => void // 新增一条数据
  set: (map: Set<T>) => void // 设置全部数据
  remove: (key: T) => void // 删除某条数据
  clear: () => void // 清空数据
  reset: () => void // 重置为默认值
}

function useSet<T>(defaultValue: Set<T> = new Set()): [Set<T>, Actions<T>] {
  const [sets, setSets] = useState<Set<T>>(defaultValue)

  // 缓存操作方法避免重复创建
  const actions = useMemo(() => {
    // 新增数据
    const add = (value: T) => {
      const newSet = new Set(sets)
      newSet.add(value)
      setSets(newSet)
    }

    // 设置全部数据
    const set = (sets: Set<T>) => {
      setSets(sets)
    }

    // 删除数据
    const remove = (key: T) => {
      const newSet = new Set(sets)
      newSet.delete(key)
      setSets(newSet)
    }

    // 清空数据
    const clear = () => {
      const newSet = new Set<T>()
      setSets(newSet)
    }

    // 重置为默认值
    const reset = () => {
      setSets(defaultValue)
    }

    return { add, set, remove, clear, reset }
  }, [defaultValue, sets])

  return [sets, actions] as const
}

export default useSet

useMap

ts 复制代码
import { useMemo, useState } from 'react'

// 定义操作方法的接口
interface Actions<T, U> {
  add: (key: T, value: U) => void // 新增一条数据
  get: (key: T) => U | undefined // 获取一条数据
  set: (map: Map<T, U>) => void // 设置全部数据
  remove: (key: T) => void // 删除某条数据
  clear: () => void // 清空数据
  reset: () => void // 重置为默认值
}

function useMap<T, U>(
  defaultValue: Map<T, U> = new Map(),
): [Map<T, U>, Actions<T, U>] {
  const [map, setMap] = useState<Map<T, U>>(defaultValue)

  // 缓存操作方法避免重复创建
  const actions = useMemo(() => {
    // 新增数据
    const add = (key: T, value: U) => {
      const newMap = new Map(map)
      newMap.set(key, value)
      setMap(newMap)
    }

    // 获取数据
    const get = (key: T) => {
      return map.get(key)
    }

    // 设置全部数据
    const set = (map: Map<T, U>) => {
      setMap(map)
    }

    // 删除数据
    const remove = (key: T) => {
      const newMap = new Map(map)
      newMap.delete(key)
      setMap(newMap)
    }

    // 清空数据
    const clear = () => {
      const newMap = new Map(map)
      newMap.clear()
      setMap(newMap)
    }

    // 重置为默认值
    const reset = () => {
      setMap(defaultValue)
    }

    return { add, get, set, remove, clear, reset }
  }, [defaultValue, map])

  return [map, actions] as const
}

export default useMap

usePrevious

ts 复制代码
import { useRef } from 'react'

type ShouldUpdateFn<T> = (prev: T | undefined, next: T) => boolean
const shouldUpdate = <T>(prev: T | undefined, next: T) => !Object.is(prev, next)
export default function usePrevious<T>(
  state: T,
  shouldUp: ShouldUpdateFn<T> = shouldUpdate,
): T | undefined {
  const curRef = useRef<T>() // 当前值
  const preRef = useRef<T>() // 上一个值

  if (shouldUp(curRef.current, state)) {
    preRef.current = curRef.current
    curRef.current = state
  }

  return preRef.current
}

useLatest

ts 复制代码
import { RefObject, useRef } from 'react'

// 获取某个state的最新值的hook
export default function useLatest<S>(value: S): RefObject<S> {
  // useRef 保存能保证每次获取到的都是最新的值
  const curRef = useRef<S>(value)
  curRef.current = value

  return curRef
}

useSafeState

ts 复制代码
import { useUnmount } from 'ahooks'
import { Dispatch, SetStateAction, useRef, useState } from 'react'

// 主要是实现 setRafState 方法,在外部调用 setRafState 方法时,会取消上一次的 setState 回调函数,并执行 requestAnimationFrame 来控制 setState 的执行时机
export default function useSafeState<S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>] {
  const [state, setState] = useState(initialState)
  const reqId = useRef(0)
  const setSafeState = (state: SetStateAction<S>) => {
    cancelAnimationFrame(reqId.current)

    reqId.current = requestAnimationFrame(() => {
      // 在回调执行真正的 setState
      setState(state)
    })
  }

  useUnmount(() => {
    cancelAnimationFrame(reqId.current)
  })

  return [state, setSafeState] as const
}

useSetState

ts 复制代码
import { getTypeOf } from '@/utils'
import { SetStateAction, useCallback, useRef, useState } from 'react'

// 类似于以前的setState,有一个回调函数,回调函数的参数是最新的值
export default function useSetState<S>(
  initialState: S | (() => S),
): [S, (state: SetStateAction<S>, cb?: (state: S) => void) => void] {
  const [state, set] = useState(initialState)
  const curRef = useRef<S>(state)
  curRef.current = state

  const setState = useCallback(
    (state: SetStateAction<S>, cb?: (state: S) => void) => {
      set((pre) => {
        const newState =
          getTypeOf(state) === 'Function'
            ? (state as (preState: S) => S)(pre)
            : (state as S)
        curRef.current = newState
        return newState
      })
      cb?.(curRef.current)
    },
    [],
  )

  return [state, setState] as const
}

useMergeStates

ts 复制代码
import { getTypeOf } from '@/utils'
import { Dispatch, SetStateAction, useCallback, useState } from 'react'

// 合并state属性,使用的时候不用整体重新赋值
export default function useMergeStates<S extends Record<string, any>>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<Partial<S>>>] {
  const [state, set] = useState(initialState)

  const setState = useCallback((patch: SetStateAction<Partial<S>>) => {
    set((pre) => {
      const newState =
        getTypeOf(patch) === 'Function'
          ? (patch as (preState: Partial<S>) => S)(pre)
          : (patch as S)

      return newState ? { ...pre, ...newState } : pre
    })
  }, [])

  return [state, setState] as const
}

useGetState

ts 复制代码
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'

// 在useState的基础上增加获取方法
export default function useGetState<S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, () => S] {
  const [state, setState] = useState(initialState)
  const curRef = useRef(state)
  curRef.current = state

  const getState = useCallback(() => curRef.current, [])

  return [state, setState, getState] as const
}

useReactive

ts 复制代码
import { useRef } from 'react'
import useToggle from './useToggle'

// 响应式对象
export default function useReactive<S extends object>(initialState: S): S {
  const [, { toggle }] = useToggle(false)
  const curRef = useRef(initialState)

  const proxy = new Proxy(curRef.current, {
    set(target, key, value) {
      const ret = Reflect.set(target, key, value)
      toggle() // 属性赋值时触发回调
      return ret
    },
    get(target, key) {
      const ret = Reflect.get(target, key)
      return ret
    },
    defineProperty(target, key) {
      const ret = Reflect.deleteProperty(target, key)
      toggle() // 属性删除时触发回调
      return ret
    },
  })

  return proxy
}

useOptions

ts 复制代码
import { DefaultOptionType } from 'antd/lib/select'
import { useEffect, useState } from 'react'

// 账号列表项
export type UseOptionsProps<T = DefaultOptionType, R = any> = {
  labelField?: keyof R // 要取的作为label的字段
  labelFormat?: (record: R) => any // label字段自定义格式化函数
  valueField?: keyof R // 要取的作为value的字段
  valueFormat?: (record: R) => any // value字段自定义格式化函数
  resField?: string[] // 要取的返回值字段(比如res、list、resutl等)
  dataFn: (...args: any) => Promise<any> // 要请求的接口
  fnParams?: any // 请求的接口参数
  definedFormat?: (data: R[]) => T[] // 自定义格式化数据函数
}

/**
 * @desc 自定义获取任意接口并格式化为下拉options的hook
 * @param  T 最终返回的数据的类型
 * @param  R 接口返回的数据的类型
 */
function useOptions<T = DefaultOptionType, R = any>(
  props: UseOptionsProps<T, R>,
): [T[], R[], () => void] {
  const {
    labelField,
    valueField,
    labelFormat,
    valueFormat,
    fnParams,
    dataFn,
    resField,
    definedFormat,
  } = props || {}
  const [options, setOptions] = useState<T[]>([]) // 下拉选项
  const [datas, setDatas] = useState<R[]>([]) // 原始数据

  /** 格式化为通用下拉选项结构 */
  const formatToCommonOptions = (list: R[]): T[] => {
    return list?.map((item: R) => {
      const label = labelFormat
        ? labelFormat?.(item)
        : labelField
        ? item?.[labelField]
        : item?.['name' as keyof R] || '' // label取值
      const value = valueFormat
        ? valueFormat?.(item)
        : valueField
        ? item?.[valueField]
        : item?.['id' as keyof R] || '' // value取值

      return { label, value } as T
    })
  }

  // 获取数据
  const getDatas = async () => {
    try {
      const res = fnParams ? await dataFn(fnParams) : await dataFn()

      const list: R[] =
        resField && resField?.length
          ? resField.reduce((pre, cur) => pre?.[cur], res)
          : res
      if (list && Array.isArray(list)) {
        setDatas(list) // 保存原始数据
        setOptions(
          definedFormat ? definedFormat(list) : formatToCommonOptions(list),
        ) // 保存格式化数据
      }
    } catch {
      //
    }
  }

  useEffect(() => {
    getDatas()
  }, [])

  return [options, datas, getDatas]
}

export default useOptions

useSse

ts 复制代码
import { useEffect, useRef, useState } from 'react'

// 定时器的类型
type TimerType = ReturnType<typeof setTimeout> | undefined

/**
 * @description SSE暴露的方法
 */
export interface SSEMethods {
  close: () => void
  reconnect: () => void
}

/**
 * SSE连接状态枚举
 */
export enum SSEReadyState {
  CONNECTING, // 连接中
  OPEN, // 已连接
  CLOSED, // 已关闭
}

/**
 * @description 初始化参数
 */
export type EventSourceInit = {
  withCredentials?: boolean
  headers?: Record<string, string>
  payload?: string
}

/**
 * @description SSE连接需要的参数
 * @param T 当前SSE连接的数据结构
 */
export interface SSEProps {
  events?: string[] // 事件名称
  retryInterval?: number // 重试间隔
  heartbeatTimeout?: number // 心跳超时时间
  init?: EventSourceInit // 初始化参数
  onMessage?: (event: MessageEvent) => void // 消息回调
  autoConnect?: boolean // 是否自动连接
  maxRetries?: number // 最大重试次数
}

/**
 * @description SSE连接返回结构
 * @param T 当前SSE连接的数据结构
 */
export interface SSEReturn<T> {
  data: T | null // 当前SSE连接的数据结构
  error: Error | null // 错误
  readyState: SSEReadyState // 连接状态
  methods: SSEMethods // 暴露的方法
  retryCount: number // 当前重试次数
}

/**
 * @description SSE连接状态枚举
 */
export const READY_STATE_MAP = {
  [SSEReadyState.CONNECTING]: '连接中...',
  [SSEReadyState.OPEN]: '已连接',
  [SSEReadyState.CLOSED]: '已关闭',
}

/**
 * @description SSE连接状态枚举
 */
export const READY_STATE_TYPE_MAP = {
  [SSEReadyState.CONNECTING]: 'warning',
  [SSEReadyState.OPEN]: 'success',
  [SSEReadyState.CLOSED]: 'error',
}

/**
 * @description 通用的SSE连接HOOK
 * @param {SSEProps} 连接参数
 */
// 连接地址
const useSse = <T,>(url: string, options: SSEProps): SSEReturn<T> => {
  const {
    autoConnect,
    maxRetries,
    retryInterval = 5 * 1000,
    heartbeatTimeout,
    init,
    events,
    onMessage,
  } = options || {}

  const [readyState, setReadyState] = useState<SSEReadyState>(
    SSEReadyState.CONNECTING,
  ) // 当前连接状态
  const [data, setData] = useState<T | null>(null) // 当前SSE数据
  const [error, setError] = useState<Error | null>(null) // 错误
  const sseRef = useRef<EventSource | null>(null) // SSE实例
  const retryTimer = useRef<TimerType | null>(null) // 重试的定时器
  const heartbeatTimer = useRef<TimerType | null>(null) // 心跳的定时器
  const retryCount = useRef<number>(0) // 当前重试次数

  // 初始化SSE
  const initSSE = () => {
    try {
      const sseInstance = new EventSource(url, init)
      sseRef.current = sseInstance

      // sse连接打开事件
      sseInstance.onopen = () => {
        console.log('SSE连接已打开')
        setReadyState(SSEReadyState.OPEN)
        retryCount.current = 0 // 重置重试次数
        startHeatbeat() // 开始心跳
        bindEvents()
      }

      sseInstance.onerror = (e) => {
        setError(new Error(`SSE Error: ${e.type}`))
        setReadyState(SSEReadyState.CLOSED)
        if (autoConnect !== false) reconnect()
      }

      sseInstance.onmessage = handleMessage
    } catch (error) {
      setError(error as Error)
      setReadyState(SSEReadyState.CLOSED)
      if (autoConnect !== false) reconnect()
    }
  }

  // 消息处理事件
  const handleMessage = (event: MessageEvent) => {
    const parsedData = JSON.parse(event.data) as T
    setData(parsedData)
    onMessage?.(event)
  }

  // 重置重试的定时器
  const resetRetryTimer = () => {
    if (retryTimer.current) {
      clearTimeout(retryTimer.current)
      retryTimer.current = null
    }
  }

  // 重置心跳的定时器
  const resetHeartbeatTimer = () => {
    if (heartbeatTimer.current) {
      clearTimeout(heartbeatTimer.current)
      heartbeatTimer.current = null
    }
  }

  // 重连SSE
  const reconnect = () => {
    // 超过最大重试次数就不重试了
    if (maxRetries && retryCount.current >= maxRetries) {
      setReadyState(SSEReadyState.CLOSED)
      return
    }

    resetRetryTimer()
    resetHeartbeatTimer()

    retryTimer.current = setTimeout(() => {
      setReadyState(SSEReadyState.CONNECTING)
      retryCount.current += 1 // 重试次数+1
      initSSE()
    }, retryInterval * (retryCount.current + 1))
  }

  // 开始心跳
  const startHeatbeat = () => {
    if (heartbeatTimeout) {
      resetHeartbeatTimer()
      heartbeatTimer.current = setTimeout(() => {
        setReadyState(SSEReadyState.CLOSED)
        disconnectSSE()
        initSSE()
      }, heartbeatTimeout)
    }
  }

  // 绑定自定义事件
  const bindEvents = () => {
    if (sseRef && events?.length) {
      events?.forEach((eventName) => {
        ;(sseRef as unknown as EventSource).addEventListener(
          eventName,
          handleMessage,
        )
      })
    }
  }

  // 解绑自定义事件
  const unbindEvents = () => {
    if (sseRef && events?.length) {
      events?.forEach((eventName) => {
        ;(sseRef as unknown as EventSource).removeEventListener(
          eventName,
          handleMessage,
        )
      })
    }
  }

  useEffect(() => {
    // 如果是自动连接
    if (autoConnect !== false) {
      initSSE()
    }
    return close
  }, [])

  // 断开SSE连接
  const disconnectSSE = () => {
    const sseInstance = sseRef.current
    if (sseInstance) {
      sseInstance.close()
      sseRef.current = null
    }
  }

  // 关闭
  const close = () => {
    disconnectSSE()
    setReadyState(SSEReadyState.CLOSED)
    resetRetryTimer()
    resetHeartbeatTimer()
    retryCount.current = 0
    setData(null)
    unbindEvents()
  }

  const methods: SSEMethods = {
    close,
    reconnect: () => {
      close()
      initSSE()
    },
  }

  return { data, readyState, retryCount: retryCount.current, error, methods }
}

export default useSse

useWebSocket

ts 复制代码
import { HeartBeatType } from '@/enums/common'
import { useCallback, useMemo, useRef, useState } from 'react'

// 心跳类型枚举
enum HeartBeatType {
  Ping = 'heartBeatPing', // 心跳发送的消息
  Pong = 'heartBeatPong', // 心跳接收的消息
}

type TimerType = ReturnType<typeof setTimeout> | undefined // 定时器的类型

// useWebSocket的参数类型
export interface IWebSocketProps {
  url?: string // socket连接地址
  heartBeatInterval?: number // 心跳检测间隔
  heartBeatData?: HeartBeatType // 心跳检测发送数据
  heartBeatSendData?: any // 心跳检测实际发送数据
  maxReconnectAttempts?: number // 最大重连次数
  reconnectInterval?: number // 重连间隔
}

// 发送数据的类型
export type SendData = string | ArrayBufferLike | Blob | ArrayBufferView

/**
 * @description WebSocket hook封装
 */
export default function useWebSocket(options: IWebSocketProps) {
  const {
    url,
    heartBeatInterval = 60 * 1000,
    heartBeatData = HeartBeatType.Ping,
    heartBeatSendData,
    maxReconnectAttempts = 10,
    reconnectInterval = 5 * 1000,
  } = options

  const socket = useRef<WebSocket>() // socket实例
  const [lastMessage, setLastMessage] = useState<MessageEvent>() // socket消息
  const reconnectAttempts = useRef<number>(0) // 已经尝试重连了的次数
  const error = useRef<Event>() // error
  const curUrlRef = useRef<string>() // curUrlRef

  let heartBeatTimer: TimerType = undefined // 心跳检测的定时器
  let reconnectTimer: TimerType = undefined // 重连的定时器

  // 是否已连接
  const isConnected = useMemo(
    () => socket.current?.readyState === WebSocket.OPEN,
    [socket],
  )

  // 连接
  const connect = useCallback(
    (curUrl: string) => {
      // 如果已存在 WebSocket 实例,则先关闭它
      if (socket) {
        socket.current?.close()
      }

      if (curUrl && !curUrlRef.current) {
        curUrlRef.current = curUrl
      }

      const socketInstance = new WebSocket(curUrl || url!)
      socket.current = socketInstance
      socketInstance.onopen = onopen
      socketInstance.onmessage = onmessage
      socketInstance.onclose = onclose
      socketInstance.onerror = onerror
    },
    [url],
  )

  // 接收消息
  const onmessage = (data: any) => {
    setLastMessage(data)
  }

  // 连接成功
  const onopen = () => {
    console.log('连接成功')
    // 重置重连次数
    reconnectAttempts.current = 0
    // 开始心跳检测
    checkHealthStart()
  }

  // 连接断开
  const onclose = () => {
    console.log('连接断开')
    // 结束心跳检测
    checkHealthEnd()
  }

  // 连接出错
  const onerror = (e: Event) => {
    console.log('连接出错')
    error.current = e
    // 结束心跳检测
    checkHealthEnd()
    // 重连
    reconnect()
  }

  // 断开连接
  const disconnect = () => {
    console.log('连接断开')
    socket.current?.close()
    // 结束心跳检测
    checkHealthEnd()
  }

  // 发送消息
  const sendMessage = (data: SendData) => {
    console.log('即将发送的数据', data)
    if (socket.current?.readyState === WebSocket.OPEN) {
      console.log('真实发送的数据', data)
      socket.current?.send(data)
    }
  }

  // 心跳检测开始
  const checkHealthStart = () => {
    console.log('心跳检测开始')
    checkHealthEnd()
    heartBeatTimer = setTimeout(() => {
      if (socket.current?.readyState === WebSocket.OPEN) {
        sendMessage(
          heartBeatSendData ||
            JSON.stringify({
              type: 'ping',
              data: heartBeatData,
            }),
        )
        checkHealthStart()
      }
    }, heartBeatInterval)
  }

  // 心跳检测结束
  const checkHealthEnd = () => {
    console.log('心跳检测结束')
    clearTimeout(heartBeatTimer)
    heartBeatTimer = undefined
  }

  // 重连
  const reconnect = () => {
    console.log('开始重连')

    // 没有连接并且没有达到重连次数
    if (
      socket.current?.readyState !== WebSocket.OPEN &&
      reconnectAttempts.current <= maxReconnectAttempts
    ) {
      // 使用递增的延迟来避免频繁重连
      reconnectTimer = setTimeout(() => {
        connect(curUrlRef.current || url!)
      }, reconnectInterval * reconnectAttempts.current + 1)
      // 增加重连尝试次数
      reconnectAttempts.current += 1
    } else {
      clearTimeout(reconnectTimer)
      reconnectTimer = undefined
    }
  }

  // 取消重连
  const cancelReconnect = () => {
    clearTimeout(reconnectTimer)
    reconnectTimer = undefined
  }

  return {
    socket,
    lastMessage,
    isConnected,
    error,
    connect,
    disconnect,
    reconnect,
    checkHealthStart,
    sendMessage,
    cancelReconnect,
  }
}

useLocalforage

ts 复制代码
import localforage from 'localforage'

/**
 * @desc localForage 是一个 JavaScript 库,通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。
  localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。
  localForage 提供回调 API 同时也支持 ES6 Promises API,你可以自行选择。
  * @return getItem 获取某个key的值
  * @return setItem 设置某个key的值
  * @return removeItem 移除某个key的值
  * @return clearItems 移除所有key的值,此方法将会删除离线仓库中的所有值。谨慎使用此方法。
  * @return itemsLength 获取离线仓库中的 key 的数量(即数据仓库的"长度")
  * @return key 根据 key 的索引获取其名
  * @return keys 获取数据仓库中所有的 key,包含所有 key 名的数组
 */
const useLocalforage = <T = any>(): [
  getItem: (key: string) => Promise<T | null>, // 获取某个key的值
  setItem: (key: string, value: T) => Promise<T>, // 设置某个key的值
  removeItem: (key: string) => Promise<boolean>, // 移除某个key的值
  clearItems: () => Promise<boolean>, // 移除所有key的值,此方法将会删除离线仓库中的所有值。谨慎使用此方法。
  itemsLength: () => Promise<number>, // 获取离线仓库中的 key 的数量(即数据仓库的"长度")
  key: (index: number) => Promise<string | null>, // 根据 key 的索引获取其名
  keys: () => Promise<string[]>, // 获取数据仓库中所有的 key,包含所有 key 名的数组
] => {
  /**
   * @desc 获取某个key的值
   * @param {string} key 要获取的key
   * @return {Promise<T | null>} 返回一个Promise,值的类型是T或者是null
   */
  const getItem = (key: string): Promise<T | null> => {
    return localforage
      .getItem<T>(key)
      .then((value) => Promise.resolve(value))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 设置某个key的值
   * @param {string} key 要设置的key
   * @param {T} value 要设置的key的值
   * @return {Promise<T>} 返回一个Promise,值的类型是T
   */
  const setItem = (key: string, value: T): Promise<T> => {
    return localforage
      .setItem<T>(key, value)
      .then((value) => Promise.resolve(value))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 从离线仓库中删除 key 对应的值
   * @param {string} key 要删除的key
   * @return {Promise<Boolean>} 返回一个Promise,值的类型是Boolean,为了保证不阻塞代码执行,移除时报错会返回Promise<false>,各位大佬可以根据返回值来判断是否移除成功
   */
  const removeItem = (key: string): Promise<boolean> => {
    return localforage
      .removeItem(key)
      .then(() => Promise.resolve(true))
      .catch(() => Promise.resolve(false))
  }

  /**
   * @desc 从数据库中删除所有的 key,重置数据库.将会删除离线仓库中的所有值。谨慎使用此方法
   * @return {Promise<Boolean>} 返回一个Promise,值的类型是Boolean,为了保证不阻塞代码执行,移除时报错会返回Promise<false>,各位大佬可以根据返回值来判断是否移除成功
   */
  const clearItems = (): Promise<boolean> => {
    return localforage
      .clear()
      .then(() => Promise.resolve(true))
      .catch(() => Promise.resolve(false))
  }

  /**
   * @desc 获取离线仓库中的 key 的数量(即数据仓库的"长度")
   * @return {Promise<number>} 返回一个Promise,值的类型是number
   */
  const itemsLength = (): Promise<number> => {
    return localforage
      .length()
      .then((numberOfKeys) => Promise.resolve(numberOfKeys))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 根据 key 的索引获取其名
   * @param {number} index 要删除的key
   * @return {Promise<string>} 返回一个Promise,key名,值的类型是string
   */
  const key = (index: number): Promise<string | null> => {
    return localforage
      .key(index)
      .then((keyName) => Promise.resolve(keyName))
      .catch((err) => Promise.reject(err))
  }

  /**
   * @desc 获取数据仓库中所有的key名数组
   * @return {Promise<string[]>} 返回一个Promise,key名数组,值的类型是string[]
   */
  const keys = (): Promise<string[]> => {
    return localforage
      .keys()
      .then((keys) => Promise.resolve(keys))
      .catch((err) => Promise.reject(err))
  }

  return [getItem, setItem, removeItem, clearItems, itemsLength, key, keys]
}

export default useLocalforage
相关推荐
heyCHEEMS2 小时前
Uni-app 性能天坑:为什么 v-if 删不掉 DOM 节点
前端
马致良2 小时前
三年前写的一个代码工具,至今已被 AI Coding 完全取代。
前端·ai编程
橙某人2 小时前
LogicFlow 交互新体验:让锚点"活"起来,鼠标跟随动效实战!🧲
前端·javascript·vue.js
借个火er2 小时前
依赖注入系统
前端
借个火er2 小时前
项目介绍与环境搭建
前端
gustt2 小时前
React 跨层级组件通信:从 Props Drilling 到 useContext 的实战剖析
前端·react.js
程序猿的程2 小时前
Stock写给前端的股票行情 SDK: stock-sdk,终于不用再求后端帮忙了
前端·javascript·node.js
用户新2 小时前
V8引擎 精品漫游指南 -解析篇 语法解析 AST 作用域 闭包 字节码 优化 一文通关
前端·javascript
黑土豆2 小时前
2025,我不再写代码,我在当代码的“审核员”
前端·vue.js·ai编程