⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

3.2.5 封装你自己的积木——自定义 Hooks

一句话破题

自定义 Hook 是以 use 开头的函数,它让你将有状态逻辑提取出来,在多个组件间复用。

核心价值

当你发现多个组件有相似的状态逻辑时,不要复制粘贴——把它提取成自定义 Hook。这不仅减少重复代码,还让逻辑更易测试和维护。

自定义 Hook 基础

tsx
// hooks/useCounter.ts
import { useState, useCallback } from 'react'

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

// 使用
function Counter() {
  const { count, increment, decrement } = useCounter(10)
  
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

常用自定义 Hooks 模式

1. useToggle - 布尔切换

tsx
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)
  
  const toggle = useCallback(() => setValue(v => !v), [])
  const setTrue = useCallback(() => setValue(true), [])
  const setFalse = useCallback(() => setValue(false), [])
  
  return { value, toggle, setTrue, setFalse }
}

// 使用
const { value: isOpen, toggle } = useToggle()

2. useLocalStorage - 持久化存储

tsx
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })
  
  const setValue = useCallback((value: T | ((val: T) => T)) => {
    setStoredValue(prev => {
      const valueToStore = value instanceof Function ? value(prev) : value
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
      return valueToStore
    })
  }, [key])
  
  return [storedValue, setValue] as const
}

3. useFetch - 数据获取

tsx
interface UseFetchResult<T> {
  data: T | null
  loading: boolean
  error: Error | null
  refetch: () => void
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)
  
  const fetchData = useCallback(async () => {
    setLoading(true)
    setError(null)
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error('Fetch failed')
      const json = await res.json()
      setData(json)
    } catch (e) {
      setError(e as Error)
    } finally {
      setLoading(false)
    }
  }, [url])
  
  useEffect(() => {
    fetchData()
  }, [fetchData])
  
  return { data, loading, error, refetch: fetchData }
}

4. useDebounce - 防抖

tsx
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value)
  
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])
  
  return debouncedValue
}

// 使用:搜索输入防抖
function Search() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)
  
  useEffect(() => {
    if (debouncedQuery) {
      // 发起搜索请求
    }
  }, [debouncedQuery])
}

命名规范

  • 必须以 use 开头:这是 React 识别 Hook 的方式
  • 描述功能而非实现useAuth 而非 useAuthState
  • 返回值要一致:对象用于多返回值,元组用于类似 useState 的场景

何时提取自定义 Hook

  1. 逻辑重复:两个以上组件有相同的状态逻辑
  2. 逻辑复杂:组件内 Hook 调用超过 5-6 个
  3. 关注点分离:业务逻辑和 UI 逻辑混在一起
  4. 便于测试:需要单独测试某段逻辑

AI 协作指南

核心意图:让 AI 帮你识别和提取可复用的逻辑。

需求定义公式

  • 功能描述:我有 [相似逻辑] 在多个组件中重复
  • 交互方式:这段逻辑需要 [输入参数]
  • 预期效果:Hook 返回 [哪些数据和方法]

关键术语useCallbackuseMemo、泛型、返回类型

交互策略

  1. 把重复的代码展示给 AI
  2. 让它分析可提取的公共逻辑
  3. 设计 Hook 的输入输出接口
  4. 实现并在原组件中使用

避坑指南

  1. Hook 规则适用于自定义 Hook:不能条件调用、循环调用
  2. 不要过早抽象:等逻辑真的重复了再提取
  3. 保持 Hook 职责单一:一个 Hook 做一件事
  4. 注意闭包陷阱:回调函数记得用 useCallback

验收清单

  • [ ] Hook 名称以 use 开头
  • [ ] 返回值类型定义清晰
  • [ ] 内部回调用 useCallback 包裹
  • [ ] 有明确的使用场景和文档