Vue3 自定义Hooks实战:10个常用场景

更新日期: 2026-05-11 阅读: 396 标签: Hooks

Vue 3的组合式API让代码复用变得简单很多。自定义Hooks就是其中比较实用的技巧。下面整理了10个开发中经常用到的Hooks,可以直接复制到项目里用。


一、useRequest:请求封装

封装请求的loading、error、data状态,支持手动和自动触发。

// composables/useRequest.ts
import { ref, shallowRef } from 'vue'

export function useRequest<T>(fetcher: () => Promise<T>, options?: { immediate?: boolean }) {
  const data = shallowRef<T | undefined>()
  const loading = ref(false)
  const error = ref<Error | null>(null)

  const run = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
      return data.value
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }

  if (options?.immediate !== false) {
    run()
  }

  return { data, loading, error, run }
}

使用示例:

<script setup lang="ts">
const { data, loading, error, run } = useRequest(() =>
  fetch('/api/user').then(r => r.json())
)
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <div v-else>{{ data }}</div>
</template>

二、useDebounce:防抖

防抖的意思是,连续触发时只执行最后一次。比如搜索框输入,用户停下来了才去请求。

// composables/useDebounce.ts
export function useDebounceFn<T extends (...args: unknown[]) => unknown>(
  fn: T,
  delay = 300
) {
  let timer: ReturnType<typeof setTimeout>
  return (...args: Parameters<T>) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
}

使用示例:

<script setup>
const search = (keyword: string) => {
  console.log('搜索:', keyword)
}
const debouncedSearch = useDebounceFn(search, 500)
</script>

三、useThrottle:节流

节流的意思是,固定时间内只执行一次。比如滚动事件,每隔一段时间触发一次。

// composables/useThrottle.ts
export function useThrottleFn<T extends (...args: unknown[]) => unknown>(
  fn: T,
  interval = 300
) {
  let last = 0
  return (...args: Parameters<T>) => {
    const now = Date.now()
    if (now - last >= interval) {
      last = now
      fn(...args)
    }
  }
}

四、useLocalStorage:本地存储

把数据自动同步到localStorage,刷新页面后数据还在。

// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const data = ref<T>(
    (() => {
      try {
        const raw = localStorage.getItem(key)
        return raw ? JSON.parse(raw) : defaultValue
      } catch {
        return defaultValue
      }
    })()
  )

  watch(data, (v) => {
    localStorage.setItem(key, JSON.stringify(v))
  }, { deep: true })

  return data
}

使用示例:

<script setup>
const settings = useLocalStorage('user-settings', { theme: 'light', fontSize: 14 })
// 修改settings,会自动存到localStorage
</script>

五、useClickOutside:点击外部关闭

做下拉框、弹窗、菜单时经常需要:点击元素外面就关闭它。

// composables/useClickOutside.ts
import { onMounted, onUnmounted } from 'vue'

export function useClickOutside(
  target: () => HTMLElement | null,
  callback: () => void
) {
  const handler = (e: MouseEvent) => {
    const el = target()
    if (el && !el.contains(e.target as Node)) {
      callback()
    }
  }

  onMounted(() => document.addEventListener('click', handler))
  onUnmounted(() => document.removeEventListener('click', handler))
}

使用示例:

<script setup>
import { ref } from 'vue'
const dropdownRef = ref(null)
const showMenu = ref(false)

useClickOutside(() => dropdownRef.value, () => {
  showMenu.value = false
})
</script>

<template>
  <div ref="dropdownRef" v-if="showMenu">下拉菜单</div>
</template>

六、useInfiniteScroll:无限滚动

滚动到页面底部时自动加载更多数据。

// composables/useInfiniteScroll.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useInfiniteScroll(
  loadMore: () => Promise<void>,
  options?: { root?: Element | null; threshold?: number }
) {
  const loading = ref(false)
  const sentinel = ref<HTMLElement | null>(null)

  const observer = new IntersectionObserver(
    async (entries) => {
      if (loading.value || !entries[0]?.isIntersecting) return
      loading.value = true
      try {
        await loadMore()
      } finally {
        loading.value = false
      }
    },
    { root: options?.root ?? null, threshold: options?.threshold ?? 0 }
  )

  onMounted(() => {
    if (sentinel.value) observer.observe(sentinel.value)
  })
  onUnmounted(() => observer.disconnect())

  return { sentinel, loading }
}

七、useClipboard:剪贴板

复制文本到剪贴板,带成功失败的提示状态。

// composables/useClipboard.ts
import { ref } from 'vue'

export function useClipboard() {
  const copied = ref(false)

  const copy = async (text: string) => {
    try {
      await navigator.clipboard.writeText(text)
      copied.value = true
      setTimeout(() => {
        copied.value = false
      }, 2000)
      return true
    } catch {
      return false
    }
  }

  return { copied, copy }
}

八、useDarkMode:暗色模式

切换网站的主题色,并把用户的选择存下来。

// composables/useDarkMode.ts
import { ref, watch } from 'vue'

export function useDarkMode() {
  const isDark = ref(
    typeof document !== 'undefined' &&
      document.documentElement.classList.contains('dark')
  )

  watch(isDark, (v) => {
    const root = document.documentElement
    if (v) {
      root.classList.add('dark')
    } else {
      root.classList.remove('dark')
    }
    localStorage.setItem('theme', v ? 'dark' : 'light')
  }, { immediate: true })

  const toggle = () => {
    isDark.value = !isDark.value
  }

  return { isDark, toggle }
}

使用时需要在全局CSS中定义暗色模式的样式。


九、useCounter:计数器

简单的数字加减,带最小值和最大值的限制。

// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initial = 0, options?: { min?: number; max?: number }) {
  const count = ref(initial)

  const inc = () => {
    if (options?.max !== undefined && count.value >= options.max) return
    count.value++
  }

  const dec = () => {
    if (options?.min !== undefined && count.value <= options.min) return
    count.value--
  }

  const reset = () => {
    count.value = initial
  }

  return {
    count: computed(() => count.value),
    inc,
    dec,
    reset,
  }
}

十、useForm:表单校验

管理表单数据和校验错误。

// composables/useForm.ts
import { reactive, toRefs } from 'vue'

export function useForm<T extends Record<string, unknown>>(
  initial: T,
  validate?: (values: T) => Record<keyof T, string | undefined>
) {
  const form = reactive({ ...initial }) as T
  const errors = reactive({} as Record<keyof T, string>)

  const validateForm = () => {
    if (!validate) return true
    const res = validate(form)
    let valid = true
    for (const k of Object.keys(res) as (keyof T)[]) {
      errors[k] = res[k] ?? ''
      if (res[k]) valid = false
    }
    return valid
  }

  const reset = () => {
    Object.assign(form, initial)
    Object.keys(errors).forEach((k) => {
      errors[k as keyof T] = ''
    })
  }

  return { ...toRefs(form), errors, validateForm, reset }
}

常用场景速查

Hook使用场景
useRequest接口请求,管理loading和error
useDebounce搜索输入、窗口resize
useThrottle滚动事件、鼠标移动
useLocalStorage用户设置、浏览记录
useClickOutside下拉框、弹窗、右键菜单
useInfiniteScroll商品列表、文章列表
useClipboard复制链接、复制代码
useDarkMode深色浅色主题切换
useCounter购物车数量、分页页码
useForm登录注册、信息填写

自定义Hooks的核心是把业务逻辑从组件里抽出来。这样代码更干净,也好复用。写组件的时候如果发现逻辑有点复杂,就可以考虑拆成Hook。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

相关推荐

在hooks中使用Mobx

创建store;注入store,这样既可以在class中使用,也可以在hooks中使用了;在class中使用;在hooks中使用

使用react hooks实现自己的context-redux

我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理

React Hooks与setInterval

Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?

React将引入Hooks,你怎么看?

React将引入Hooks,你怎么看?

近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?

useEffect Hook 是如何工作的?

使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。

精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。

React Hooks 你真的用对了吗?

从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考

React-Hooks

以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。

用 React Hooks 做一个搜索栏

以下是我们将要构建的搜索框的动图。这是一个简单的搜索框,我们可以用它来搜索联系人列表。我们将使用函数式组件,而不是基于类的组件来实现它。

react hooks系列之useRef

react hooks是 react 16.8 引入的特性,这里我们通过对react-hook-form进行分析来了解成熟的库是如何使用hook的。这将是一个系列,首先推荐 useRef

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!