前端框架 精选推荐

自定义 React Hook 设计模式与最佳实践

HTMLPAGE 团队
12 分钟阅读

深度讲解如何设计高复用、高效能的自定义 Hook,包括常用模式、性能优化、状态同步、副作用管理等,适合想要提升 React 开发能力的开发者。

#React #Hooks #自定义 Hook #设计模式 #性能优化

自定义 React Hook 设计模式与最佳实践

自定义 Hook 是复用 React 逻辑的最强大工具。本文系统讲解设计和优化自定义 Hook 的最佳实践。

1. Hook 基础回顾

Hook 的规则

// ✅ 规则 1: 只在函数式组件顶层调用 Hook
function Component() {
  const [count, setCount] = useState(0)  // ✅
  return <div>{count}</div>
}

// ❌ 错误: 在条件中调用
function BadComponent() {
  if (true) {
    const [count, setCount] = useState(0)  // ❌
  }
}

// ✅ 规则 2: 只在 React 函数式组件或自定义 Hook 中调用
const useCustom = () => {
  const [count, setCount] = useState(0)  // ✅ 在自定义 Hook 中
  return count
}

// ❌ 错误: 在普通函数中调用
const normalFunction = () => {
  const [count, setCount] = useState(0)  // ❌
}

执行顺序很关键

// Hook 调用顺序必须一致
function Component({ showAge }: { showAge: boolean }) {
  const [name, setName] = useState('')  // ✅ 第 1 个
  
  // ❌ 错误: 条件调用会破坏顺序
  // if (showAge) {
  //   const [age, setAge] = useState(0)  // 第 ? 个
  // }
  
  // ✅ 正确: 总是调用,但可能不使用
  const [age, setAge] = useState(0)  // ✅ 第 2 个
  
  return <div>{name}</div>
}

2. 常用自定义 Hook 模式

模式 1: useAsync - 异步数据获取

interface AsyncState<T> {
  data: T | null
  loading: boolean
  error: Error | null
}

function useAsync<T>(
  asyncFunction: () => Promise<T>,
  immediate: boolean = true
): AsyncState<T> & { execute: () => Promise<void> } {
  const [state, setState] = useState<AsyncState<T>>({
    data: null,
    loading: immediate,
    error: null
  })

  const execute = useCallback(async () => {
    setState({ data: null, loading: true, error: null })
    try {
      const result = await asyncFunction()
      setState({ data: result, loading: false, error: null })
    } catch (error) {
      setState({ 
        data: null, 
        loading: false, 
        error: error instanceof Error ? error : new Error(String(error))
      })
    }
  }, [asyncFunction])

  useEffect(() => {
    if (immediate) {
      execute()
    }
  }, [execute, immediate])

  return { ...state, execute }
}

// 使用例子
function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error, execute } = useAsync(
    () => fetch(`/api/users/${userId}`).then(r => r.json()),
    true
  )

  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error.message}</div>
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={execute}>重新加载</button>
    </div>
  )
}

模式 2: usePrevious - 获取前一个值

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

// 使用例子
function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)

  return (
    <div>
      <p>现在: {count}</p>
      <p>之前: {prevCount}</p>
      <p>变化: {count - (prevCount ?? 0)}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  )
}

模式 3: useDebounce - 防抖值

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(handler)
  }, [value, delay])

  return debouncedValue
}

// 使用例子
function SearchUsers() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 500)
  const { data: results } = useAsync(
    () => fetch(`/api/users?q=${debouncedSearchTerm}`).then(r => r.json()),
    true
  )

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="搜索用户..."
      />
      <ul>
        {results?.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

模式 4: useThrottle - 节流值

function useThrottle<T>(value: T, delay: number): T {
  const [throttledValue, setThrottledValue] = useState(value)
  const lastUpdated = useRef<number | null>(null)

  useEffect(() => {
    const now = Date.now()
    
    if (lastUpdated.current === null || now - lastUpdated.current >= delay) {
      lastUpdated.current = now
      setThrottledValue(value)
    } else {
      const timer = setTimeout(() => {
        lastUpdated.current = Date.now()
        setThrottledValue(value)
      }, delay - (now - (lastUpdated.current ?? 0)))

      return () => clearTimeout(timer)
    }
  }, [value, delay])

  return throttledValue
}

// 使用例子: 监听窗口滚动
function ScrollListener() {
  const [scrollPos, setScrollPos] = useState(0)
  const throttledScrollPos = useThrottle(scrollPos, 200)

  useEffect(() => {
    const handleScroll = () => setScrollPos(window.scrollY)
    window.addEventListener('scroll', handleScroll)
    return () => window.removeEventListener('scroll', handleScroll)
  }, [])

  return <div>滚动位置: {throttledScrollPos}px</div>
}

模式 5: useLocalStorage - 本地存储同步

function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }, [key, storedValue])

  return [storedValue, setValue]
}

// 使用例子
function UserPreferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)

  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option>light</option>
        <option>dark</option>
      </select>
      <input 
        type="number" 
        value={fontSize} 
        onChange={(e) => setFontSize(Number(e.target.value))}
      />
    </div>
  )
}

3. 性能优化 Hook

useCallback - 缓存回调函数

// ❌ 问题: 每次渲染都创建新的回调函数
function Parent() {
  const [data, setData] = useState([])
  
  const handleAdd = () => {
    setData([...data, Math.random()])
  }
  
  // Child 会因为 handleAdd 改变而重新渲染
  return <Child onAdd={handleAdd} />
}

// ✅ 解决: 用 useCallback 缓存
function Parent2() {
  const [data, setData] = useState([])
  
  const handleAdd = useCallback(() => {
    setData(prev => [...prev, Math.random()])
  }, [])  // 依赖项为空,函数不会改变
  
  return <Child onAdd={handleAdd} />
}

// 正确的依赖项
function Parent3() {
  const [multiplier, setMultiplier] = useState(1)
  
  const handleAdd = useCallback((value: number) => {
    // 使用 multiplier,必须加到依赖项
    return value * multiplier
  }, [multiplier])  // 当 multiplier 变化时,函数会重新创建
}

useMemo - 缓存计算结果

// ❌ 问题: 复杂计算每次都执行
function ExpensiveComponent({ items }: { items: Item[] }) {
  const sorted = items
    .filter(item => item.active)
    .sort((a, b) => b.priority - a.priority)
    .map(item => ({
      ...item,
      score: calculateComplexScore(item)
    }))
  
  return <List data={sorted} />
}

// ✅ 解决: 用 useMemo 缓存结果
function ExpensiveComponent2({ items }: { items: Item[] }) {
  const sorted = useMemo(() => {
    return items
      .filter(item => item.active)
      .sort((a, b) => b.priority - a.priority)
      .map(item => ({
        ...item,
        score: calculateComplexScore(item)
      }))
  }, [items])  // items 变化时才重新计算
  
  return <List data={sorted} />
}

// 合并 useCallback 和 useMemo
function SearchResults({ query }: { query: string }) {
  const results = useMemo(() => {
    return performExpensiveSearch(query)
  }, [query])
  
  const handleSelect = useCallback((item: Item) => {
    // 处理选择
  }, [])
  
  return <ResultsList items={results} onSelect={handleSelect} />
}

React.memo + useCallback 的组合

interface ChildProps {
  items: Item[]
  onSelect: (item: Item) => void
}

// 使用 React.memo 防止不必要重渲染
const MemoChild = React.memo(function Child({ items, onSelect }: ChildProps) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
})

function Parent() {
  const [selectedId, setSelectedId] = useState<number | null>(null)
  const [items, setItems] = useState<Item[]>([])
  
  // useCallback 很重要: 如果不用,每次 Parent 渲染时创建新的 handleSelect
  // 导致 MemoChild 无法避免重渲染
  const handleSelect = useCallback((item: Item) => {
    setSelectedId(item.id)
  }, [])
  
  return <MemoChild items={items} onSelect={handleSelect} />
}

4. 高级模式

模式 1: useReducer - 复杂状态管理

interface State {
  count: number
  error: string | null
  history: number[]
}

type Action = 
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number }
  | { type: 'RESET' }
  | { type: 'ERROR'; payload: string }

function countReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + action.payload,
        history: [...state.history, state.count]
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - action.payload,
        history: [...state.history, state.count]
      }
    case 'RESET':
      return { count: 0, error: null, history: [] }
    case 'ERROR':
      return { ...state, error: action.payload }
    default:
      return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(countReducer, {
    count: 0,
    error: null,
    history: []
  })

  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>
        +1
      </button>
      <button onClick={() => dispatch({ type: 'DECREMENT', payload: 1 })}>
        -1
      </button>
      <button onClick={() => dispatch({ type: 'RESET' })}>
        重置
      </button>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      <p>历史: {state.history.join(', ')}</p>
    </div>
  )
}

模式 2: useContext + useReducer - 全局状态

interface AppState {
  user: User | null
  loading: boolean
}

type AppAction = 
  | { type: 'SET_USER'; payload: User }
  | { type: 'LOGOUT' }

const AppContext = createContext<AppState | null>(null)
const AppDispatchContext = createContext<React.Dispatch<AppAction> | null>(null)

function AppProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(appReducer, {
    user: null,
    loading: false
  })

  return (
    <AppContext.Provider value={state}>
      <AppDispatchContext.Provider value={dispatch}>
        {children}
      </AppDispatchContext.Provider>
    </AppContext.Provider>
  )
}

// 自定义 Hook: 使用全局状态
function useAppState() {
  const state = useContext(AppContext)
  if (!state) throw new Error('useAppState 必须在 AppProvider 内使用')
  return state
}

function useAppDispatch() {
  const dispatch = useContext(AppDispatchContext)
  if (!dispatch) throw new Error('useAppDispatch 必须在 AppProvider 内使用')
  return dispatch
}

// 使用
function Header() {
  const { user } = useAppState()
  const dispatch = useAppDispatch()

  return (
    <div>
      {user ? (
        <>
          <p>欢迎, {user.name}</p>
          <button onClick={() => dispatch({ type: 'LOGOUT' })}>
            退出
          </button>
        </>
      ) : (
        <p>未登录</p>
      )}
    </div>
  )
}

5. Hook 最佳实践检查清单

// ✅ Hook 设计检查清单

// 1. 命名规范: use 开头
function useCustomHook() {  // ✅
  // ...
}

// 2. 单一职责: 一个 Hook 做一件事
function useUserData() {  // ✅ 只负责获取用户数据
  // ...
}

// 3. 返回类型明确
function useCustom(): { value: string; setValue: (v: string) => void } {  // ✅
  // ...
}

// 4. 依赖项完整: 所有使用的值都在依赖项中
function useHook() {
  const [count, setCount] = useState(0)
  const external = useContext(SomeContext)
  
  useEffect(() => {
    // 使用了 count 和 external
    console.log(count, external)
  }, [count, external])  // ✅ 两者都包含
}

// 5. 清理副作用
function useHook() {
  useEffect(() => {
    const listener = () => {}
    window.addEventListener('resize', listener)
    
    return () => {  // ✅ 清理监听器
      window.removeEventListener('resize', listener)
    }
  }, [])
}

// 6. 避免在 Hook 中创建新对象作为依赖
// ❌ 错误
function useHook(config: { value: number }) {
  useEffect(() => {
    // ...
  }, [config])  // config 每次都是新对象,导致无限循环
}

// ✅ 正确
function useHook(configValue: number) {
  useEffect(() => {
    // ...
  }, [configValue])  // 使用原始值作为依赖
}

总结

自定义 Hook 的核心优势:

优势说明
逻辑复用跨组件复用有状态逻辑
代码组织按功能而非生命周期组织
易于测试Hook 是普通函数,易于单元测试
可组合Hook 可以相互调用,构建复杂功能
渐进式可以逐步迁移现有代码到 Hook

相关资源