React Hook 介绍

半兽人 发表于: 2025-11-13   最后更新时间: 2025-11-13 14:20:12  
{{totalSubscript}} 订阅, 16 游览

Hook 是什么

React 内置的 Hooks

import { useState, useEffect, useCallback, useMemo } from 'react'

function Component() {
  const [count, setCount] = useState(0)         // 这是 Hook

  useEffect(() => {                             // 这也是 Hook
    console.log('mounted')
  }, [])

  const handleClick = useCallback(() => {       // 这也是 Hook
    setCount(count + 1)
  }, [count])

  return <button onClick={handleClick}>{count}</button>
}

React 常用的内置 Hooks

  • useState - 状态管理
  • useEffect - 副作用
  • useCallback - 函数缓存
  • useMemo - 值缓存
  • useRef - 引用
  • useContext - Context
  • useReducer - 复杂状态
  • 等等...

什么是自定义 Hooks?

就是你自己写的、可以复用的逻辑!

举个例子

不用自定义 Hook(代码重复)

// 组件 A - 需要 localStorage
function ComponentA() {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem('key')
    return stored ? JSON.parse(stored) : 'default'
  })

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

  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// 组件 B - 也需要 localStorage(重复代码!)
function ComponentB() {
  const [theme, setTheme] = useState(() => {
    const stored = localStorage.getItem('theme')
    return stored ? JSON.parse(stored) : 'light'
  })

  useEffect(() => {
    localStorage.setItem('theme', JSON.stringify(theme))
  }, [theme])

  return <button onClick={() => setTheme('dark')}>{theme}</button>
}

用自定义 Hook(复用逻辑)

// hooks/useLocalStorage.ts - 自定义 Hook
import { useState, useEffect } from 'react'

export function useLocalStorage<T>(key: string, initialValue: T) {
  // 从 localStorage 读取初始值
  const [value, setValue] = useState<T>(() => {
    try {
      const stored = localStorage.getItem(key)
      return stored ? JSON.parse(stored) : initialValue
    } catch {
      return initialValue
    }
  })

  // 值变化时保存到 localStorage
  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.error('Error saving to localStorage:', error)
    }
  }, [key, value])

  return [value, setValue] as const
}

// 组件 A - 简洁!
function ComponentA() {
  const [value, setValue] = useLocalStorage('key', 'default')

  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// 组件 B - 也很简洁!
function ComponentB() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')

  return <button onClick={() => setTheme('dark')}>{theme}</button>
}

看到区别了吗?

  • √ 逻辑复用,不用重复写
  • √ 代码更清晰
  • √ 易于测试
  • √ 易于维护

更多自定义 Hooks 例子

1. useDebounce - 防抖(搜索框常用)

// hooks/useDebounce.ts
import { useState, useEffect } from 'react'

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

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

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

  return debouncedValue
}

// 使用 - 搜索框
function SearchBox() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 500)

  useEffect(() => {
    // 只在用户停止输入 500ms 后才调用 API
    if (debouncedSearchTerm) {
      fetch(`/api/search?q=${debouncedSearchTerm}`)
        .then(res => res.json())
        .then(data => console.log(data))
    }
  }, [debouncedSearchTerm])

  return (
    <input 
      value={searchTerm} 
      onChange={e => setSearchTerm(e.target.value)}
      placeholder="搜索..."
    />
  )
}

2. useMediaQuery - 响应式判断

// hooks/useMediaQuery.ts
import { useState, useEffect } from 'react'

export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false)

  useEffect(() => {
    const media = window.matchMedia(query)

    if (media.matches !== matches) {
      setMatches(media.matches)
    }

    const listener = () => setMatches(media.matches)
    media.addEventListener('change', listener)

    return () => media.removeEventListener('change', listener)
  }, [query, matches])

  return matches
}

// 使用 - 响应式布局
function ResponsiveComponent() {
  const isMobile = useMediaQuery('(max-width: 768px)')
  const isDesktop = useMediaQuery('(min-width: 1024px)')

  return (
    <div>
      {isMobile && <MobileMenu />}
      {isDesktop && <DesktopMenu />}
    </div>
  )
}

3. useOnClickOutside - 点击外部关闭

// hooks/useOnClickOutside.ts
import { useEffect, RefObject } from 'react'

export function useOnClickOutside<T extends HTMLElement>(
  ref: RefObject<T>,
  handler: (event: MouseEvent | TouchEvent) => void
) {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      const el = ref?.current
      if (!el || el.contains(event.target as Node)) {
        return
      }
      handler(event)
    }

    document.addEventListener('mousedown', listener)
    document.addEventListener('touchstart', listener)

    return () => {
      document.removeEventListener('mousedown', listener)
      document.removeEventListener('touchstart', listener)
    }
  }, [ref, handler])
}

// 使用 - 下拉菜单
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false)
  const dropdownRef = useRef<HTMLDivElement>(null)

  useOnClickOutside(dropdownRef, () => {
    setIsOpen(false) // 点击外部关闭
  })

  return (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(!isOpen)}>打开菜单</button>
      {isOpen && (
        <ul>
          <li>选项 1</li>
          <li>选项 2</li>
        </ul>
      )}
    </div>
  )
}

4. useCopyToClipboard - 复制到剪贴板

// hooks/useCopyToClipboard.ts
import { useState } from 'react'

export function useCopyToClipboard() {
  const [copiedText, setCopiedText] = useState<string | null>(null)

  const copy = async (text: string) => {
    if (!navigator?.clipboard) {
      console.warn('Clipboard not supported')
      return false
    }

    try {
      await navigator.clipboard.writeText(text)
      setCopiedText(text)
      return true
    } catch (error) {
      console.error('Copy failed', error)
      setCopiedText(null)
      return false
    }
  }

  return { copiedText, copy }
}

// 使用
function CopyButton({ text }: { text: string }) {
  const { copiedText, copy } = useCopyToClipboard()

  return (
    <button onClick={() => copy(text)}>
      {copiedText === text ? '已复制!' : '复制'}
    </button>
  )
}

所以,/hooks/ 目录是干什么的?

存放这些自定义 Hooks!

hooks/
├── index.ts                  // 统一导出
├── useLocalStorage.ts       // localStorage Hook
├── useDebounce.ts           // 防抖 Hook
├── useThrottle.ts           // 节流 Hook
├── useMediaQuery.ts         // 媒体查询 Hook
├── useOnClickOutside.ts     // 点击外部 Hook
├── useCopyToClipboard.ts    // 复制 Hook
├── useToggle.ts             // 开关 Hook
└── useAsync.ts              // 异步 Hook

使用方式

// 在任何组件中使用
import { useLocalStorage, useDebounce, useMediaQuery } from '@/hooks'

function MyComponent() {
  const [user, setUser] = useLocalStorage('user', null)
  const [search, setSearch] = useState('')
  const debouncedSearch = useDebounce(search, 300)
  const isMobile = useMediaQuery('(max-width: 768px)')

  // ... 你的逻辑
}

总结

React Hook 包括:

  1. React 内置 Hooks(你不用创建目录)

    • useState, useEffect, useCallback, useMemo
    • 直接从 react 导入使用
  2. 自定义 Hooks(需要创建 /hooks/ 目录)

    • 你自己写的,可复用的逻辑
    • 通常会用到 React 内置 Hooks
    • 命名必须以 use 开头(React 规则)

为什么需要 /hooks/ 目录?

  • 复用逻辑 - 不用重复写代码
  • 代码整洁 - 组件更简洁
  • 易于测试 - 可以单独测试 Hook
  • 易于维护 - 逻辑集中管理
更新于 2025-11-13
在线,9小时前登录

查看React更多相关的文章或提一个关于React的问题,也可以与我们一起分享文章