从 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。

相关推荐
张龙6871 小时前
RAG 知识库问答系统实战:分块、混合检索、RRF 融合与重排全链路拆解
前端
用户1306095607231 小时前
第二章学完后,我对 webpack5 前端工程化的一点理解
前端
a1117761 小时前
小鸡下蛋 小游戏 html
前端·开源·html
码哥DFS1 小时前
算法练习day1-备战2027届秋招
前端·javascript·数据结构·算法
张元清1 小时前
React useInfiniteScroll Hook:无限滚动轻松实现(2026)
javascript·react.js
Bigger1 小时前
因为一句「华流才是最屌的」,我做了一个中国风设计的 Skill
前端·人工智能·设计
别怪我很水1 小时前
Vue element admin 浏览器本地存储 localStorage、useStorage
前端·javascript·vue.js
手揽回忆怎么睡1 小时前
Ubuntu 22.04 64位安装MinIO
linux·前端·ubuntu
铁皮饭盒2 小时前
网页端OCR, 加载6mb大小模型, 又快又准, 百度这次真香
前端·后端