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
相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax