从 Vue3 到 React · 快速上手系列第 9 篇:自定义 Hook(对标 Composables)

面向 Vue3 开发者的 React 快速上手系列

这是让 Vue 开发者最有"亲切感"的一篇。React 的自定义 Hook 和 Vue 的 Composables 是几乎同一个概念------甚至连命名规范(use 前缀)都一样!掌握它,你就掌握了 React 逻辑复用的精髓。


一、它们本质是同一个东西

Vue3 Composable React 自定义 Hook
是什么 封装响应式逻辑的函数 封装状态逻辑的函数
命名 useXxx useXxx
内部可用 ref/computed/watch... useState/useEffect...
目的 逻辑复用、关注点分离 逻辑复用、关注点分离

一句话:自定义 Hook 就是"把用到其他 Hook 的逻辑抽成一个 useXxx 函数",和你写 Composable 的思路完全一致。


二、第一个自定义 Hook:并列对比

来写一个"计数器"逻辑。

Vue3 Composable:

js 复制代码
// useCounter.js
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initial
  return { count, increment, decrement, reset }
}

React 自定义 Hook:

js 复制代码
// useCounter.js
import { useState } from 'react'

export function useCounter(initial = 0) {
  const [count, setCount] = useState(initial)
  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initial)
  return { count, increment, decrement, reset }
}

使用起来也几乎一样:

jsx 复制代码
// React 组件里
function Counter() {
  const { count, increment, decrement, reset } = useCounter(10)
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

看到没?结构一模一样。你写 Composable 的所有直觉都能平移。


三、自定义 Hook 的规则("Hook 规则")

有两条铁律,违反会直接报错(Vue Composable 没这么严格,需注意):

  1. 只在顶层调用 Hook :不能在 iffor、嵌套函数里调用 useState/useEffect 等。
  2. 只在 React 函数里调用:只能在组件或其他自定义 Hook 里用。
jsx 复制代码
// ❌ 错误:条件里调用
function Bad({ show }) {
  if (show) {
    const [x, setX] = useState(0)   // 报错!
  }
}

// ✅ 正确:顶层调用,条件写在内部
function Good({ show }) {
  const [x, setX] = useState(0)
  if (show) { /* 用 x */ }
}

原因回到第 1 篇:React 靠调用顺序 来对应每个 Hook 的内部存储,条件调用会打乱顺序。这也是为什么必须装 eslint-plugin-react-hooks------它会自动帮你抓违规。

Vue 的 Composable 因为 setup 只运行一次,没有这个顺序约束,可以更随意。这是两者少数的实质差异之一。


四、几个实用的自定义 Hook 示例

1. useToggle(布尔开关)

js 复制代码
import { useState, useCallback } from 'react'

export function useToggle(initial = false) {
  const [value, setValue] = useState(initial)
  const toggle = useCallback(() => setValue(v => !v), [])
  return [value, toggle]
}

// 用法
const [open, toggleOpen] = useToggle()

2. useLocalStorage(持久化状态,对标 VueUse 的 useStorage)

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

export function useLocalStorage(key, defaultValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key)
    return saved ? JSON.parse(saved) : defaultValue
  })

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])

  return [value, setValue]
}

// 用法:和 useState 一样,但会自动持久化
const [theme, setTheme] = useLocalStorage('theme', 'light')

3. useFetch(数据请求)

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

export function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let cancelled = false
    setLoading(true)
    fetch(url)
      .then(res => res.json())
      .then(json => { if (!cancelled) setData(json) })
      .catch(err => { if (!cancelled) setError(err) })
      .finally(() => { if (!cancelled) setLoading(false) })
    return () => { cancelled = true }
  }, [url])

  return { data, loading, error }
}

// 用法
function UserList() {
  const { data, loading, error } = useFetch('/api/users')
  if (loading) return <Spinner />
  if (error) return <ErrorTip />
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

4. useWindowSize(响应窗口尺寸)

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

export function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })
  useEffect(() => {
    const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight })
    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])
  return size
}

五、组合 Hook:Hook 里调用 Hook

自定义 Hook 可以调用其他 Hook(包括别的自定义 Hook),层层组合,和 Composable 组合思路一致:

js 复制代码
export function useUser(userId) {
  const { data, loading } = useFetch(`/api/users/${userId}`)  // 复用 useFetch
  const [isFollowing, toggleFollow] = useToggle(false)         // 复用 useToggle
  return { user: data, loading, isFollowing, toggleFollow }
}

六、开箱即用的 Hook 库:ReactUse / ahooks

Vue 有 VueUse(200+ composables),React 也有对应的:

  • ahooks:阿里出品,中文文档友好,国内项目常用,涵盖请求、防抖节流、DOM、生命周期等大量 Hook。
  • ReactUse:直接对标 VueUse 设计,API 命名都刻意向 VueUse 靠拢,VueUse 用户上手无缝。

有了它们,useLocalStorageuseDebounceuseRequest 这些常用逻辑不用自己写。就像你在 Vue 里直接 import { useStorage } from '@vueuse/core' 一样。

js 复制代码
// ahooks 示例
import { useRequest, useLocalStorageState, useToggle } from 'ahooks'

const { data, loading } = useRequest(fetchUsers)

七、Vue 开发者的心智迁移总结

Composable 习惯 React 自定义 Hook
useXxx.js 文件放逻辑 一样
返回 { 状态, 方法 } 一样(或返回数组,如 useState 风格)
内部用 ref/computed 内部用 useState/useMemo
可随意在 setup 任意位置调用 必须顶层调用(Hook 规则)
VueUse 现成工具 ahooks / ReactUse

小结

  • 自定义 Hook ≈ Composable,连 use 前缀命名都相同,是 React 逻辑复用的核心。
  • 写法直觉可从 Composable 直接平移:抽成 useXxx 函数,内部用其他 Hook,返回状态和方法。
  • 两条铁律:Hook 只在顶层调用、只在 React 函数里调用(装 ESLint 插件保驾护航)。
  • Hook 可组合、可嵌套调用其他 Hook。
  • 现成生态:ahooks / ReactUse 对标 VueUse,别重复造轮子。

下一篇讲路由------React Router 对标 Vue Router。

相关推荐
李姆斯12 分钟前
为啥Agent在coding表现这么好,但是在别的领域就是差的不少?
前端·agent·ai编程
鱼与宇3 小时前
前端Web(html+css+js+vue3)
前端
孙克旭_3 小时前
Docker 镜像构建|Vue+SpringBoot+Go 多语言项目 Dockerfile 案例
linux·运维·vue.js·spring boot·docker·dockerfile
雪芽蓝域zzs4 小时前
(六)打包优化 + Nginx 部署完整配置 + 项目收尾
前端·vue.js
研☆香4 小时前
聊一聊前端的常见字 关键字
开发语言·前端·javascript
东风破_5 小时前
JWT 1:从一个登录请求开始,理解 React 项目里的 API 层与 Mock
前端·后端
东风破_5 小时前
JWT 3:为什么 Token 要放进 Authorization?Axios 拦截器到底解决了什么?
前端·后端
东风破_5 小时前
JWT 5:路由守卫是什么?把整个 JWT 登录鉴权流程串起来
前端·后端
东风破_5 小时前
JWT 2:HTTP 是无状态的,为什么登录成功后还要给 Token?
前端·后端
东风破_5 小时前
JWT 4:Zustand 到底解决了什么?为什么登录状态要放进 Store?
前端·后端