面向 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 没这么严格,需注意):
- 只在顶层调用 Hook :不能在
if、for、嵌套函数里调用useState/useEffect等。 - 只在 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 用户上手无缝。
有了它们,useLocalStorage、useDebounce、useRequest 这些常用逻辑不用自己写。就像你在 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。