React Hook 入门指南

0 阅读19分钟

React Hook 入门指南

适合新手学习,从零开始理解 React Hook,每个 Hook 都有详细的使用示例


一、什么是 Hook?

Hook 是 React 16.8 引入的一种机制,让你在函数组件中使用状态(state)和其他 React 特性,而无需编写类组件。

为什么需要 Hook?

在 Hook 出现之前,React 组件主要有两种写法:

// ❌ 类组件 - 代码冗长,this 指向容易出错
class Counter extends React.Component {
  state = { count: 0 }
  
  increment = () => {
    this.setState({ count: this.state.count + 1 })
  }
  
  render() {
    return (
      <button onClick={this.increment}>
        Count: {this.state.count}
      </button>
    )
  }
}
// ✅ 函数组件 + Hook - 简洁直观
function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

函数组件的优势:

  • 代码量减少 20-30%
  • 没有 this 指向问题
  • 逻辑复用更简单
  • 更容易理解和测试

二、内置 Hook 一览

React 提供了多个内置 Hook,按用途分类:

2.1 基础 Hook

Hook用途示例
useState管理组件状态const [count, setCount] = useState(0)
useEffect处理副作用数据获取、订阅、DOM 操作
useContext访问上下文跨组件共享数据

2.2 性能优化 Hook

Hook用途示例
useMemo缓存计算结果避免重复计算
useCallback缓存函数引用避免子组件不必要的渲染
useRef获取 DOM 引用或保存可变值访问 DOM 元素

2.3 其他 Hook

Hook用途
useReducer复杂状态管理(类似 Redux)
useLayoutEffect同步执行副作用(DOM 更新前)
useImperativeHandle自定义暴露给父组件的方法

三、useState 详解

useState 是最基础的 Hook,用于在函数组件中添加状态。

3.1 基础用法:简单计数器

import { useState } from 'react'

function Counter() {
  // 声明一个名为 count 的状态变量,初始值为 0
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  )
}

3.2 管理对象状态

import { useState } from 'react'

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  })
  
  // 更新单个字段
  const updateField = (field: string, value: string | number) => {
    setUser(prevUser => ({
      ...prevUser,  // 保留其他字段
      [field]: value
    }))
  }
  
  return (
    <form>
      <div>
        <label>姓名:</label>
        <input 
          value={user.name}
          onChange={e => updateField('name', e.target.value)}
        />
      </div>
      <div>
        <label>邮箱:</label>
        <input 
          value={user.email}
          onChange={e => updateField('email', e.target.value)}
        />
      </div>
      <div>
        <label>年龄:</label>
        <input 
          type="number"
          value={user.age}
          onChange={e => updateField('age', parseInt(e.target.value) || 0)}
        />
      </div>
      <p>当前用户: {JSON.stringify(user)}</p>
    </form>
  )
}

3.3 管理数组状态

import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState<string[]>([])
  const [input, setInput] = useState('')
  
  // 添加元素
  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, input.trim()])
      setInput('')
    }
  }
  
  // 删除元素
  const removeTodo = (index: number) => {
    setTodos(todos.filter((_, i) => i !== index))
  }
  
  // 清空所有
  const clearAll = () => {
    setTodos([])
  }
  
  return (
    <div>
      <div>
        <input 
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="输入待办事项"
        />
        <button onClick={addTodo}>添加</button>
        <button onClick={clearAll}>清空</button>
      </div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>删除</button>
          </li>
        ))}
      </ul>
      <p>共 {todos.length} 个待办事项</p>
    </div>
  )
}

3.4 函数式更新

当你需要基于前一个状态值来更新状态时,使用函数式更新:

import { useState } from 'react'

function AdvancedCounter() {
  const [count, setCount] = useState(0)
  
  // ✅ 推荐:函数式更新,确保拿到最新值
  const increment = () => {
    setCount(prev => prev + 1)
  }
  
  // 批量更新:连续增加 3 次
  const incrementThreeTimes = () => {
    // ❌ 错误方式:这样只会增加 1,因为 count 值在当前渲染周期不变
    // setCount(count + 1)
    // setCount(count + 1)
    // setCount(count + 1)
    
    // ✅ 正确方式:每次都基于最新的 prev 值
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={incrementThreeTimes}>+3</button>
    </div>
  )
}

3.5 惰性初始化

当初始值需要复杂计算时,可以传入一个函数,它只会在首次渲染时执行:

import { useState } from 'react'

function ExpensiveInitialValue() {
  // ❌ 每次渲染都会执行这个计算
  // const [state, setState] = useState(computeExpensiveValue())
  
  // ✅ 惰性初始化:函数只在首次渲染时执行
  const [state, setState] = useState(() => {
    console.log('只在首次渲染时计算')
    // 模拟昂贵的计算
    let result = 0
    for (let i = 0; i < 10000; i++) {
      result += i
    }
    return result
  })
  
  return (
    <div>
      <p>初始值: {state}</p>
      <button onClick={() => setState(s => s + 1)}>增加</button>
    </div>
  )
}

3.6 切换布尔值

import { useState } from 'react'

function ToggleButton() {
  const [isOn, setIsOn] = useState(false)
  
  const toggle = () => {
    setIsOn(prev => !prev)  // 切换布尔值
  }
  
  return (
    <button onClick={toggle}>
      状态: {isOn ? '开启' : '关闭'}
    </button>
  )
}

// 模态框示例
function Modal() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>打开模态框</button>
      
      {isOpen && (
        <div className="modal">
          <div className="modal-content">
            <h2>模态框标题</h2>
            <p>这是模态框内容</p>
            <button onClick={() => setIsOpen(false)}>关闭</button>
          </div>
        </div>
      )}
    </div>
  )
}

四、useEffect 详解

useEffect 用于处理"副作用",比如数据获取、订阅、DOM 操作等。

4.1 基础用法:数据获取

import { useState, useEffect } from 'react'

interface User {
  id: number
  name: string
  email: string
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  
  useEffect(() => {
    // 开始加载
    setLoading(true)
    setError(null)
    
    // 请求用户数据
    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) {
          throw new Error('请求失败')
        }
        return res.json()
      })
      .then(data => {
        setUser(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err.message)
        setLoading(false)
      })
  }, [userId])  // userId 变化时重新请求
  
  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error}</div>
  
  return (
    <div>
      <h2>{user?.name}</h2>
      <p>邮箱: {user?.email}</p>
    </div>
  )
}

4.2 订阅与清理

import { useState, useEffect } from 'react'

function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<string[]>([])
  
  useEffect(() => {
    // 模拟连接到聊天室
    console.log(`连接到聊天室: ${roomId}`)
    
    // 模拟接收消息
    const interval = setInterval(() => {
      setMessages(prev => [...prev, `新消息 @ ${new Date().toLocaleTimeString()}`])
    }, 3000)
    
    // 清理函数:组件卸载或 roomId 变化前执行
    return () => {
      console.log(`断开聊天室: ${roomId}`)
      clearInterval(interval)
    }
  }, [roomId])
  
  return (
    <div>
      <h3>聊天室: {roomId}</h3>
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>{msg}</li>
        ))}
      </ul>
    </div>
  )
}

4.3 定时器示例

import { useState, useEffect } from 'react'

function Timer() {
  const [seconds, setSeconds] = useState(0)
  const [isRunning, setIsRunning] = useState(false)
  
  useEffect(() => {
    let interval: number | null = null
    
    if (isRunning) {
      interval = setInterval(() => {
        setSeconds(prev => prev + 1)
      }, 1000)
    }
    
    // 清理定时器
    return () => {
      if (interval) {
        clearInterval(interval)
      }
    }
  }, [isRunning])
  
  const reset = () => {
    setSeconds(0)
    setIsRunning(false)
  }
  
  return (
    <div>
      <h2>计时器: {seconds}秒</h2>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? '暂停' : '开始'}
      </button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

4.4 事件监听

import { useState, useEffect } from 'react'

function WindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0
  })
  
  useEffect(() => {
    // 定义处理函数
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }
    
    // 添加事件监听
    window.addEventListener('resize', handleResize)
    
    // 清理:移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])  // 空数组:只在挂载时添加,卸载时移除
  
  return (
    <div>
      <p>窗口宽度: {windowSize.width}px</p>
      <p>窗口高度: {windowSize.height}px</p>
    </div>
  )
}

// 键盘事件示例
function KeyboardListener() {
  const [lastKey, setLastKey] = useState('')
  
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      setLastKey(e.key)
    }
    
    window.addEventListener('keydown', handleKeyDown)
    
    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [])
  
  return <p>你按下了: {lastKey || '等待输入...'}</p>
}

4.5 依赖数组的三种情况

import { useState, useEffect } from 'react'

function DependencyExamples({ userId, count }: { userId: string; count: number }) {
  const [logs, setLogs] = useState<string[]>([])
  
  // 情况1:无依赖数组 - 每次渲染后都执行
  useEffect(() => {
    console.log('每次渲染都执行')
  })
  
  // 情况2:空数组 - 只在挂载时执行一次
  useEffect(() => {
    console.log('组件挂载时执行一次')
    // 相当于类组件的 componentDidMount
    
    return () => {
      console.log('组件卸载时执行')
      // 相当于类组件的 componentWillUnmount
    }
  }, [])
  
  // 情况3:有依赖 - 依赖变化时执行
  useEffect(() => {
    console.log(`userId 变化为: ${userId}`)
    // 相当于类组件的 componentDidUpdate(针对特定 props)
  }, [userId])
  
  // 多个依赖
  useEffect(() => {
    console.log(`userId 或 count 变化: userId=${userId}, count=${count}`)
  }, [userId, count])
  
  return <div>查看控制台日志</div>
}

4.6 DOM 操作示例

import { useState, useEffect, useRef } from 'react'

function DynamicTitle() {
  const [count, setCount] = useState(0)
  
  // 更新页面标题
  useEffect(() => {
    document.title = `点击次数: ${count}`
  }, [count])
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      点击次数: {count}
    </button>
  )
}

// 自动滚动到底部
function ChatMessages({ messages }: { messages: string[] }) {
  const bottomRef = useRef<HTMLDivElement>(null)
  
  useEffect(() => {
    // 新消息时滚动到底部
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])
  
  return (
    <div style={{ height: '200px', overflow: 'auto' }}>
      {messages.map((msg, i) => (
        <div key={i}>{msg}</div>
      ))}
      <div ref={bottomRef} />
    </div>
  )
}

五、useRef 详解

useRef 有两个主要用途:

  1. 获取 DOM 元素引用
  2. 保存可变值(变化时不触发重新渲染)

5.1 基础用法:获取 DOM 引用

import { useRef, useEffect } from 'react'

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  const focusInput = () => {
    // 访问 DOM 元素
    inputRef.current?.focus()
  }
  
  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current?.focus()
  }, [])
  
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="我会自动聚焦" />
      <button onClick={focusInput}>手动聚焦</button>
    </div>
  )
}

5.2 表单提交示例

import { useRef } from 'react'

function FormWithRef() {
  const nameRef = useRef<HTMLInputElement>(null)
  const emailRef = useRef<HTMLInputElement>(null)
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    
    // 直接获取表单值,不需要 useState
    console.log({
      name: nameRef.current?.value,
      email: emailRef.current?.value
    })
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>姓名:</label>
        <input ref={nameRef} type="text" defaultValue="" />
      </div>
      <div>
        <label>邮箱:</label>
        <input ref={emailRef} type="email" defaultValue="" />
      </div>
      <button type="submit">提交</button>
    </form>
  )
}

5.3 保存定时器 ID

import { useState, useRef, useEffect } from 'react'

function Stopwatch() {
  const [time, setTime] = useState(0)
  const [isRunning, setIsRunning] = useState(false)
  const intervalRef = useRef<number | null>(null)
  
  const start = () => {
    if (!isRunning) {
      setIsRunning(true)
      intervalRef.current = setInterval(() => {
        setTime(prev => prev + 1)
      }, 1000)
    }
  }
  
  const stop = () => {
    setIsRunning(false)
    if (intervalRef.current) {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  }
  
  const reset = () => {
    stop()
    setTime(0)
  }
  
  // 组件卸载时清理
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current)
      }
    }
  }, [])
  
  return (
    <div>
      <h2>秒表: {time}秒</h2>
      <button onClick={start} disabled={isRunning}>开始</button>
      <button onClick={stop} disabled={!isRunning}>停止</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

5.4 跟踪前一次值

import { useState, useRef, useEffect } from 'react'

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()
  
  useEffect(() => {
    ref.current = value
  }, [value])
  
  return ref.current
}

// 使用示例
function CounterWithPrevious() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)
  
  return (
    <div>
      <p>当前值: {count}</p>
      <p>前一个值: {prevCount ?? '无'}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}

5.5 可变值(不触发渲染)

import { useState, useRef } from 'react'

function RenderCounter() {
  const [count, setCount] = useState(0)
  const renderCount = useRef(0)
  
  // 每次渲染时增加,但不会触发额外的渲染
  renderCount.current += 1
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>组件渲染次数: {renderCount.current}</p>
      <button onClick={() => setCount(c => c + 1)}>增加 Count</button>
    </div>
  )
}

5.6 useState vs useRef 对比

import { useState, useRef } from 'react'

function CompareStateAndRef() {
  const [stateCount, setStateCount] = useState(0)
  const refCount = useRef(0)
  
  const incrementState = () => {
    setStateCount(prev => prev + 1)
    console.log('State 更新,会触发重新渲染')
  }
  
  const incrementRef = () => {
    refCount.current += 1
    console.log('Ref 更新,不会触发重新渲染')
    console.log('当前 ref 值:', refCount.current)
  }
  
  return (
    <div>
      <div>
        <h3>useState</h3>
        <p>值: {stateCount} (UI 会更新)</p>
        <button onClick={incrementState}>增加 State</button>
      </div>
      <div>
        <h3>useRef</h3>
        <p>值: {refCount.current} (UI 不会自动更新)</p>
        <button onClick={incrementRef}>增加 Ref</button>
        <button onClick={() => setStateCount(c => c)}>强制刷新</button>
      </div>
    </div>
  )
}
特性useStateuseRef
值变化时重新渲染✅ 是❌ 否
用途需要显示的数据DOM 引用、定时器 ID、前一次值等
更新方式setter 函数直接修改 .current

六、useContext 详解

useContext 用于跨组件共享数据,避免 prop drilling(层层传递 props)。

6.1 基础用法:创建和使用 Context

import { createContext, useContext, useState } from 'react'

// 1. 创建 Context
interface ThemeContextType {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

// 2. 创建 Provider 组件
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. 创建自定义 Hook 方便使用
function useTheme() {
  const context = useContext(ThemeContext)
  if (context === undefined) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用')
  }
  return context
}

// 4. 使用 Context 的组件
function ThemedButton() {
  const { theme, toggleTheme } = useTheme()
  
  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        border: '1px solid',
        padding: '10px 20px'
      }}
    >
      当前主题: {theme}
    </button>
  )
}

function ThemedBox() {
  const { theme } = useTheme()
  
  return (
    <div style={{
      background: theme === 'light' ? '#f5f5f5' : '#222',
      color: theme === 'light' ? '#333' : '#fff',
      padding: '20px'
    }}>
      这是一个主题化的盒子
    </div>
  )
}

// 5. 应用入口
function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>主题切换示例</h1>
        <ThemedButton />
        <ThemedBox />
      </div>
    </ThemeProvider>
  )
}

export default App

6.2 用户认证 Context

import { createContext, useContext, useState, ReactNode } from 'react'

interface User {
  id: string
  name: string
  email: string
}

interface AuthContextType {
  user: User | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  isAuthenticated: boolean
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  
  const login = async (email: string, password: string) => {
    // 模拟登录请求
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    })
    const userData = await response.json()
    setUser(userData)
  }
  
  const logout = () => {
    setUser(null)
  }
  
  return (
    <AuthContext.Provider value={{
      user,
      login,
      logout,
      isAuthenticated: !!user
    }}>
      {children}
    </AuthContext.Provider>
  )
}

function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth 必须在 AuthProvider 内使用')
  }
  return context
}

// 使用示例
function LoginButton() {
  const { user, login, logout, isAuthenticated } = useAuth()
  
  if (isAuthenticated) {
    return (
      <div>
        <span>欢迎, {user?.name}</span>
        <button onClick={logout}>退出登录</button>
      </div>
    )
  }
  
  return (
    <button onClick={() => login('test@example.com', 'password')}>
      登录
    </button>
  )
}

function ProtectedContent() {
  const { isAuthenticated } = useAuth()
  
  if (!isAuthenticated) {
    return <p>请先登录查看内容</p>
  }
  
  return <p>这是受保护的内容,只有登录用户可见</p>
}

6.3 多层嵌套 Context

import { createContext, useContext, useState } from 'react'

// Context 1: 用户信息
const UserContext = createContext<{ name: string } | null>(null)

// Context 2: 主题设置
const ThemeContext = createContext<{ theme: string } | null>(null)

// Context 3: 语言设置
const LocaleContext = createContext<{ locale: string } | null>(null)

function DeepNestedComponent() {
  // 可以同时使用多个 Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)
  const locale = useContext(LocaleContext)
  
  return (
    <div>
      <p>用户: {user?.name}</p>
      <p>主题: {theme?.theme}</p>
      <p>语言: {locale?.locale}</p>
    </div>
  )
}

function App() {
  return (
    <UserContext.Provider value={{ name: '张三' }}>
      <ThemeContext.Provider value={{ theme: 'dark' }}>
        <LocaleContext.Provider value={{ locale: 'zh-CN' }}>
          <DeepNestedComponent />
        </LocaleContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  )
}

七、useMemo 详解

useMemo 用于缓存计算结果,避免在每次渲染时重复执行昂贵的计算。

7.1 基础用法:缓存计算结果

import { useState, useMemo } from 'react'

function ExpensiveCalculation({ numbers }: { numbers: number[] }) {
  // 昂贵的计算函数
  const calculateSum = (nums: number[]): number => {
    console.log('执行计算...')  // 观察是否执行
    let sum = 0
    for (let i = 0; i < 10000000; i++) {
      // 模拟耗时计算
      sum += nums.reduce((a, b) => a + b, 0)
    }
    return sum
  }
  
  // ❌ 不使用 useMemo:每次渲染都会重新计算
  // const total = calculateSum(numbers)
  
  // ✅ 使用 useMemo:只在 numbers 变化时重新计算
  const total = useMemo(() => {
    return calculateSum(numbers)
  }, [numbers])
  
  return <div>计算结果: {total}</div>
}

function App() {
  const [numbers] = useState([1, 2, 3, 4, 5])
  const [count, setCount] = useState(0)  // 无关的状态
  
  return (
    <div>
      <ExpensiveCalculation numbers={numbers} />
      <button onClick={() => setCount(c => c + 1)}>
        点击计数: {count}
      </button>
      <p>点击按钮不会触发重新计算,因为 numbers 没变</p>
    </div>
  )
}

7.2 列表过滤和排序

import { useState, useMemo } from 'react'

interface Product {
  id: number
  name: string
  price: number
  category: string
}

function ProductList({ products }: { products: Product[] }) {
  const [searchTerm, setSearchTerm] = useState('')
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
  const [category, setCategory] = useState<string>('all')
  
  // 过滤和排序:只在相关状态变化时重新计算
  const filteredProducts = useMemo(() => {
    console.log('重新计算过滤结果')
    
    let result = [...products]
    
    // 按类别过滤
    if (category !== 'all') {
      result = result.filter(p => p.category === category)
    }
    
    // 按搜索词过滤
    if (searchTerm) {
      result = result.filter(p => 
        p.name.toLowerCase().includes(searchTerm.toLowerCase())
      )
    }
    
    // 排序
    result.sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      }
      return a.price - b.price
    })
    
    return result
  }, [products, searchTerm, sortBy, category])
  
  return (
    <div>
      <div>
        <input
          placeholder="搜索..."
          value={searchTerm}
          onChange={e => setSearchTerm(e.target.value)}
        />
        <select value={category} onChange={e => setCategory(e.target.value)}>
          <option value="all">全部类别</option>
          <option value="electronics">电子产品</option>
          <option value="clothing">服装</option>
        </select>
        <select value={sortBy} onChange={e => setSortBy(e.target.value as 'name' | 'price')}>
          <option value="name">按名称</option>
          <option value="price">按价格</option>
        </select>
      </div>
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price}
          </li>
        ))}
      </ul>
    </div>
  )
}

7.3 避免子组件不必要的渲染

import { useState, useMemo, memo } from 'react'

// 子组件使用 memo 包裹
const ExpensiveChild = memo(function ExpensiveChild({ 
  data 
}: { 
  data: { items: number[] } 
}) {
  console.log('子组件渲染')
  return (
    <div>
      数据项数量: {data.items.length}
    </div>
  )
})

function ParentComponent() {
  const [count, setCount] = useState(0)
  const [items] = useState([1, 2, 3, 4, 5])
  
  // ✅ 使用 useMemo:保持对象引用稳定
  const data = useMemo(() => ({
    items
  }), [items])
  
  // ❌ 不使用 useMemo:每次渲染都创建新对象
  // const data = { items }
  // 这样会导致 ExpensiveChild 每次都重新渲染
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        计数: {count}
      </button>
      <ExpensiveChild data={data} />
      <p>点击按钮,子组件不会重新渲染</p>
    </div>
  )
}

7.4 计算派生状态

import { useState, useMemo } from 'react'

interface Todo {
  id: number
  text: string
  completed: boolean
}

function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: '学习 React', completed: true },
    { id: 2, text: '学习 TypeScript', completed: false },
    { id: 3, text: '写项目', completed: false }
  ])
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')
  
  // 派生状态:根据过滤条件计算显示的 todo 列表
  const visibleTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(t => !t.completed)
      case 'completed':
        return todos.filter(t => t.completed)
      default:
        return todos
    }
  }, [todos, filter])
  
  // 统计信息
  const stats = useMemo(() => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  }), [todos])
  
  return (
    <div>
      <div>
        <button onClick={() => setFilter('all')}>全部 ({stats.total})</button>
        <button onClick={() => setFilter('active')}>待办 ({stats.active})</button>
        <button onClick={() => setFilter('completed')}>已完成 ({stats.completed})</button>
      </div>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            <span style={{ 
              textDecoration: todo.completed ? 'line-through' : 'none' 
            }}>
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
    </div>
  )
}

八、useCallback 详解

useCallback 用于缓存函数引用,避免在每次渲染时创建新的函数实例。

8.1 基础用法:缓存回调函数

import { useState, useCallback } from 'react'

function SearchComponent() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<string[]>([])
  
  // ✅ 使用 useCallback:函数引用稳定
  const handleSearch = useCallback((searchQuery: string) => {
    console.log('搜索:', searchQuery)
    // 模拟搜索
    setResults(['结果1', '结果2', '结果3'])
  }, [])  // 无依赖,函数永远不会改变
  
  // ❌ 不使用 useCallback:每次渲染都创建新函数
  // const handleSearch = (searchQuery: string) => {
  //   console.log('搜索:', searchQuery)
  // }
  
  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <button onClick={() => handleSearch(query)}>搜索</button>
      <ul>
        {results.map((r, i) => <li key={i}>{r}</li>)}
      </ul>
    </div>
  )
}

8.2 配合子组件使用

import { useState, useCallback, memo } from 'react'

// 子组件:使用 memo 包裹,只有 props 变化才重新渲染
const Button = memo(function Button({ 
  onClick, 
  label 
}: { 
  onClick: () => void
  label: string 
}) {
  console.log(`Button "${label}" 渲染`)
  return <button onClick={onClick}>{label}</button>
})

function ParentWithCallback() {
  const [countA, setCountA] = useState(0)
  const [countB, setCountB] = useState(0)
  
  // ✅ 使用 useCallback:保持函数引用稳定
  const incrementA = useCallback(() => {
    setCountA(c => c + 1)
  }, [])
  
  const incrementB = useCallback(() => {
    setCountB(c => c + 1)
  }, [])
  
  // ❌ 不使用 useCallback:每次渲染都会创建新函数
  // 这样 Button 组件每次都会重新渲染
  // const incrementA = () => setCountA(c => c + 1)
  // const incrementB = () => setCountB(c => c + 1)
  
  return (
    <div>
      <p>Count A: {countA}</p>
      <p>Count B: {countB}</p>
      <Button onClick={incrementA} label="增加 A" />
      <Button onClick={incrementB} label="增加 B" />
    </div>
  )
}

8.3 依赖其他状态

import { useState, useCallback } from 'react'

function TodoList() {
  const [todos, setTodos] = useState<string[]>([])
  const [input, setInput] = useState('')
  
  // ✅ 使用 useCallback + 依赖
  const addTodo = useCallback(() => {
    if (input.trim()) {
      setTodos(prev => [...prev, input.trim()])
      setInput('')
    }
  }, [input])  // input 变化时,函数会更新
  
  // 删除 todo
  const removeTodo = useCallback((index: number) => {
    setTodos(prev => prev.filter((_, i) => i !== index))
  }, [])  // 不依赖外部变量,函数稳定
  
  return (
    <div>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
      />
      <button onClick={addTodo}>添加</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

8.4 传递给 useEffect 依赖

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

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<{ name: string } | null>(null)
  
  // ✅ 使用 useCallback 创建稳定的函数引用
  const fetchUser = useCallback(async () => {
    const response = await fetch(`/api/users/${userId}`)
    const data = await response.json()
    setUser(data)
  }, [userId])  // userId 变化时函数会更新
  
  // 可以安全地放入依赖数组
  useEffect(() => {
    fetchUser()
  }, [fetchUser])
  
  return <div>{user?.name ?? '加载中...'}</div>
}

8.5 useCallback vs useMemo

import { useCallback, useMemo } from 'react'

function CallbackVsMemo() {
  const [count, setCount] = useState(0)
  
  // useCallback: 缓存函数本身
  const handleClick = useCallback(() => {
    console.log('点击')
  }, [])
  
  // useMemo: 缓存函数的返回值(虽然也可以缓存函数)
  const memoizedFunction = useMemo(() => {
    return () => console.log('点击')
  }, [])
  
  // 实际上 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
  
  return <button onClick={handleClick}>Count: {count}</button>
}
特性useCallbackuseMemo
缓存内容函数引用任意计算结果
返回值函数本身计算结果
使用场景传递给子组件的回调、useEffect 依赖昂贵的计算、派生状态

九、useReducer 详解

useReducer 用于管理复杂状态逻辑,是 useState 的替代方案,适合状态更新逻辑复杂的场景。

9.1 基础用法:计数器

import { useReducer } from 'react'

// 1. 定义状态类型
interface State {
  count: number
}

// 2. 定义 action 类型
type Action = 
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'set'; payload: number }

// 3. 定义 reducer 函数
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return { count: 0 }
    case 'set':
      return { count: action.payload }
    default:
      throw new Error('Unknown action')
  }
}

function Counter() {
  // useReducer(reducer, initial state)
  const [state, dispatch] = useReducer(reucer, { count: 0 })
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>
        设为 10
      </button>
    </div>
  )
}

9.2 复杂表单状态

import { useReducer } from 'react'

interface FormState {
  username: string
  email: string
  password: string
  errors: {
    username?: string
    email?: string
    password?: string
  }
  isSubmitting: boolean
}

type FormAction = 
  | { type: 'SET_FIELD'; field: keyof Omit<FormState, 'errors' | 'isSubmitting'>; value: string }
  | { type: 'SET_ERROR'; field: keyof FormState['errors']; error: string | undefined }
  | { type: 'CLEAR_ERRORS' }
  | { type: 'START_SUBMIT' }
  | { type: 'END_SUBMIT' }
  | { type: 'RESET' }

const initialState: FormState = {
  username: '',
  email: '',
  password: '',
  errors: {},
  isSubmitting: false
}

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value }
    case 'SET_ERROR':
      return { 
        ...state, 
        errors: { ...state.errors, [action.field]: action.error } 
      }
    case 'CLEAR_ERRORS':
      return { ...state, errors: {} }
    case 'START_SUBMIT':
      return { ...state, isSubmitting: true }
    case 'END_SUBMIT':
      return { ...state, isSubmitting: false }
    case 'RESET':
      return initialState
    default:
      return state
  }
}

function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, initialState)
  
  const handleChange = (field: keyof Omit<FormState, 'errors' | 'isSubmitting'>) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    dispatch({ type: 'SET_FIELD', field, value: e.target.value })
    // 清除该字段的错误
    dispatch({ type: 'SET_ERROR', field: field as keyof FormState['errors'], error: undefined })
  }
  
  const validate = () => {
    let hasError = false
    
    if (!state.username) {
      dispatch({ type: 'SET_ERROR', field: 'username', error: '用户名必填' })
      hasError = true
    }
    
    if (!state.email.includes('@')) {
      dispatch({ type: 'SET_ERROR', field: 'email', error: '邮箱格式不正确' })
      hasError = true
    }
    
    if (state.password.length < 6) {
      dispatch({ type: 'SET_ERROR', field: 'password', error: '密码至少6位' })
      hasError = true
    }
    
    return !hasError
  }
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    
    if (!validate()) return
    
    dispatch({ type: 'START_SUBMIT' })
    
    // 模拟提交
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    dispatch({ type: 'END_SUBMIT' })
    dispatch({ type: 'RESET' })
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          value={state.username}
          onChange={handleChange('username')}
          placeholder="用户名"
        />
        {state.errors.username && <span>{state.errors.username}</span>}
      </div>
      <div>
        <input
          value={state.email}
          onChange={handleChange('email')}
          placeholder="邮箱"
        />
        {state.errors.email && <span>{state.errors.email}</span>}
      </div>
      <div>
        <input
          type="password"
          value={state.password}
          onChange={handleChange('password')}
          placeholder="密码"
        />
        {state.errors.password && <span>{state.errors.password}</span>}
      </div>
      <button type="submit" disabled={state.isSubmitting}>
        {state.isSubmitting ? '提交中...' : '提交'}
      </button>
      <button type="button" onClick={() => dispatch({ type: 'RESET' })}>
        重置
      </button>
    </form>
  )
}

9.3 Todo 列表(经典示例)

import { useReducer } from 'react'

interface Todo {
  id: number
  text: string
  completed: boolean
}

type TodoAction = 
  | { type: 'add'; text: string }
  | { type: 'toggle'; id: number }
  | { type: 'delete'; id: number }
  | { type: 'clear_completed' }

function todosReducer(todos: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case 'add':
      return [...todos, {
        id: Date.now(),
        text: action.text,
        completed: false
      }]
    case 'toggle':
      return todos.map(todo =>
        todo.id === action.id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    case 'delete':
      return todos.filter(todo => todo.id !== action.id)
    case 'clear_completed':
      return todos.filter(todo => !todo.completed)
    default:
      return todos
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todosReducer, [])
  const [input, setInput] = useState('')
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (input.trim()) {
      dispatch({ type: 'add', text: input.trim() })
      setInput('')
    }
  }
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="添加待办事项"
        />
        <button type="submit">添加</button>
      </form>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'toggle', id: todo.id })}
            />
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'delete', id: todo.id })}>
              删除
            </button>
          </li>
        ))}
      </ul>
      
      <button onClick={() => dispatch({ type: 'clear_completed' })}>
        清除已完成
      </button>
    </div>
  )
}

9.4 useReducer vs useState

特性useStateuseReducer
适用场景简单状态(单个值)复杂状态(多字段、多逻辑)
更新方式直接设置值通过 action 描述更新
可测试性较难单独测试reducer 是纯函数,易测试
状态逻辑分散在组件中集中在 reducer 中
// useState:适合简单状态
const [count, setCount] = useState(0)

// useReducer:适合复杂状态
const [state, dispatch] = useReducer(reducer, initialState)

十、useLayoutEffect 详解

useLayoutEffectuseEffect 类似,但它在所有 DOM 变更后同步触发,在浏览器绘制之前执行。

10.1 基础用法:防止闪烁

import { useState, useLayoutEffect, useRef } from 'react'

function Tooltip({ content }: { content: string }) {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  const [visible, setVisible] = useState(false)
  const tooltipRef = useRef<HTMLDivElement>(null)
  
  useLayoutEffect(() => {
    if (visible && tooltipRef.current) {
      // 在绘制前计算位置,避免闪烁
      const rect = tooltipRef.current.getBoundingClientRect()
      
      // 如果超出屏幕,调整位置
      if (rect.right > window.innerWidth) {
        setPosition(prev => ({
          ...prev,
          x: prev.x - rect.width
        }))
      }
    }
  }, [visible])
  
  return (
    <div>
      <button 
        onMouseEnter={() => setVisible(true)}
        onMouseLeave={() => setVisible(false)}
      >
        悬停显示提示
      </button>
      {visible && (
        <div 
          ref={tooltipRef}
          style={{
            position: 'absolute',
            left: position.x,
            top: position.y,
            background: '#333',
            color: '#fff',
            padding: '5px 10px'
          }}
        >
          {content}
        </div>
      )}
    </div>
  )
}

10.2 动画初始状态

import { useState, useLayoutEffect, useRef } from 'react'

function AnimatedModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
  const modalRef = useRef<HTMLDivElement>(null)
  
  useLayoutEffect(() => {
    if (isOpen && modalRef.current) {
      // 在绘制前设置初始状态
      modalRef.current.style.opacity = '0'
      modalRef.current.style.transform = 'scale(0.9)'
      
      // 强制浏览器应用初始状态后开始动画
      requestAnimationFrame(() => {
        if (modalRef.current) {
          modalRef.current.style.opacity = '1'
          modalRef.current.style.transform = 'scale(1)'
        }
      })
    }
  }, [isOpen])
  
  if (!isOpen) return null
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div 
        ref={modalRef}
        className="modal-content"
        style={{
          transition: 'opacity 0.3s, transform 0.3s'
        }}
        onClick={e => e.stopPropagation()}
      >
        <h2>模态框标题</h2>
        <p>模态框内容</p>
        <button onClick={onClose}>关闭</button>
      </div>
    </div>
  )
}

10.3 useEffect vs useLayoutEffect

import { useState, useEffect, useLayoutEffect, useRef } from 'react'

function EffectComparison() {
  const [count, setCount] = useState(0)
  const boxRef = useRef<HTMLDivElement>(null)
  
  // useEffect:浏览器绘制后执行
  useEffect(() => {
    console.log('useEffect: DOM 已绘制')
  })
  
  // useLayoutEffect:浏览器绘制前执行
  useLayoutEffect(() => {
    console.log('useLayoutEffect: DOM 更新后,绘制前')
    if (boxRef.current) {
      // 同步修改 DOM,不会看到闪烁
      boxRef.current.style.background = count % 2 === 0 ? 'lightblue' : 'lightgreen'
    }
  })
  
  return (
    <div>
      <div 
        ref={boxRef}
        style={{ 
          width: '100px', 
          height: '100px',
          transition: 'background 0.3s'
        }}
      >
        Count: {count}
      </div>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
    </div>
  )
}
特性useEffectuseLayoutEffect
执行时机DOM 更新后,浏览器绘制后DOM 更新后,浏览器绘制前
阻塞绘制否(异步)是(同步)
使用场景大多数副作用DOM 测量、防止闪烁
服务端渲染支持会警告,需特殊处理

十一、useImperativeHandle 详解

useImperativeHandle 用于自定义暴露给父组件的实例方法,需要配合 forwardRef 使用。

11.1 基础用法

import { useRef, useImperativeHandle, forwardRef } from 'react'

// 子组件
interface InputHandle {
  focus: () => void
  clear: () => void
  getValue: () => string
}

const CustomInput = forwardRef<InputHandle, { defaultValue?: string }>(
  ({ defaultValue = '' }, ref) => {
    const inputRef = useRef<HTMLInputElement>(null)
    
    // 暴露给父组件的方法
    useImperativeHandle(ref, () => ({
      focus: () => {
        inputRef.current?.focus()
      },
      clear: () => {
        if (inputRef.current) {
          inputRef.current.value = ''
        }
      },
      getValue: () => {
        return inputRef.current?.value ?? ''
      }
    }))
    
    return (
      <input 
        ref={inputRef} 
        type="text" 
        defaultValue={defaultValue}
        style={{ padding: '8px', fontSize: '16px' }}
      />
    )
  }
)

// 父组件
function ParentComponent() {
  const inputRef = useRef<InputHandle>(null)
  
  const handleFocus = () => {
    inputRef.current?.focus()
  }
  
  const handleClear = () => {
    inputRef.current?.clear()
  }
  
  const handleGetValue = () => {
    alert(`当前值: ${inputRef.current?.getValue()}`)
  }
  
  return (
    <div>
      <CustomInput ref={inputRef} defaultValue="初始值" />
      <div style={{ marginTop: '10px' }}>
        <button onClick={handleFocus}>聚焦</button>
        <button onClick={handleClear}>清空</button>
        <button onClick={handleGetValue}>获取值</button>
      </div>
    </div>
  )
}

11.2 模态框组件示例

import { useState, useRef, useImperativeHandle, forwardRef } from 'react'

interface ModalHandle {
  open: () => void
  close: () => void
}

interface ModalProps {
  title: string
  children: React.ReactNode
  onConfirm?: () => void
}

const Modal = forwardRef<ModalHandle, ModalProps>(
  ({ title, children, onConfirm }, ref) => {
    const [isOpen, setIsOpen] = useState(false)
    
    useImperativeHandle(ref, () => ({
      open: () => setIsOpen(true),
      close: () => setIsOpen(false)
    }))
    
    if (!isOpen) return null
    
    return (
      <div className="modal-overlay" onClick={() => setIsOpen(false)}>
        <div 
          className="modal-content"
          onClick={e => e.stopPropagation()}
          style={{
            background: 'white',
            padding: '20px',
            borderRadius: '8px',
            maxWidth: '400px'
          }}
        >
          <h2>{title}</h2>
          <div>{children}</div>
          <div style={{ marginTop: '20px' }}>
            <button onClick={() => setIsOpen(false)}>取消</button>
            <button 
              onClick={() => {
                onConfirm?.()
                setIsOpen(false)
              }}
            >
              确认
            </button>
          </div>
        </div>
      </div>
    )
  }
)

// 使用示例
function App() {
  const modalRef = useRef<ModalHandle>(null)
  
  return (
    <div>
      <button onClick={() => modalRef.current?.open()}>
        打开模态框
      </button>
      
      <Modal
        ref={modalRef}
        title="确认操作"
        onConfirm={() => console.log('已确认')}
      >
        <p>你确定要执行此操作吗?</p>
      </Modal>
    </div>
  )
}

11.3 暴露部分方法

import { useRef, useImperativeHandle, forwardRef, useState } from 'react'

interface VideoPlayerHandle {
  play: () => void
  pause: () => void
  // 注意:不暴露其他内部方法
}

const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(
  ({ src }, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null)
    const [isPlaying, setIsPlaying] = useState(false)
    
    // 只暴露 play 和 pause 方法
    useImperativeHandle(ref, () => ({
      play: () => {
        videoRef.current?.play()
        setIsPlaying(true)
      },
      pause: () => {
        videoRef.current?.pause()
        setIsPlaying(false)
      }
    }))
    
    // 内部方法,不暴露给父组件
    const handleTimeUpdate = () => {
      // 处理时间更新
    }
    
    const handleEnded = () => {
      setIsPlaying(false)
    }
    
    return (
      <div>
        <video
          ref={videoRef}
          src={src}
          onTimeUpdate={handleTimeUpdate}
          onEnded={handleEnded}
        />
        <p>状态: {isPlaying ? '播放中' : '已暂停'}</p>
      </div>
    )
  }
)

十二、自定义 Hook

12.1 什么是自定义 Hook?

当多个组件需要共享相同逻辑时,可以抽取为自定义 Hook

规则:

  • 函数名必须以 use 开头(如 useFetchUser
  • 内部可以调用其他 Hook

12.2 示例:封装数据获取逻辑

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

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 = async () => {
    setLoading(true)
    setError(null)
    
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const json = await response.json()
      setData(json)
    } catch (e) {
      setError(e instanceof Error ? e : new Error('Unknown error'))
    } finally {
      setLoading(false)
    }
  }
  
  useEffect(() => {
    fetchData()
  }, [url])
  
  return { data, loading, error, refetch: fetchData }
}

export default useFetch
// 使用自定义 Hook
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error, refetch } = useFetch<User>(
    `/api/users/${userId}`
  )
  
  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error.message}</div>
  
  return (
    <div>
      <h2>{user?.name}</h2>
      <p>邮箱: {user?.email}</p>
      <button onClick={refetch}>刷新</button>
    </div>
  )
}

12.3 自定义 Hook:useLocalStorage

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

function useLocalStorage<T>(
  key: string, 
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  // 初始化时从 localStorage 读取
  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 (error) {
      console.error(error)
      return initialValue
    }
  })
  
  // 更新时同步到 localStorage
  useEffect(() => {
    if (typeof window !== 'undefined') {
      try {
        window.localStorage.setItem(key, JSON.stringify(storedValue))
      } catch (error) {
        console.error(error)
      }
    }
  }, [key, storedValue])
  
  const setValue = (value: T | ((prev: T) => T)) => {
    setStoredValue(prev => {
      const newValue = value instanceof Function ? value(prev) : value
      return newValue
    })
  }
  
  return [storedValue, setValue]
}

export default useLocalStorage
// 使用示例
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
  
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      当前主题: {theme}
    </button>
  )
}

function PersistentForm() {
  const [name, setName] = useLocalStorage('form-name', '')
  const [email, setEmail] = useLocalStorage('form-email', '')
  
  return (
    <form>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="姓名"
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="邮箱"
      />
      <p>刷新页面,数据不会丢失</p>
    </form>
  )
}

12.4 自定义 Hook:useDebounce

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

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

export default useDebounce
// 使用示例:搜索输入
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearch = useDebounce(searchTerm, 500)
  
  // 只在 debouncedSearch 变化时发起请求
  useEffect(() => {
    if (debouncedSearch) {
      console.log('搜索:', debouncedSearch)
      // fetchSearchResults(debouncedSearch)
    }
  }, [debouncedSearch])
  
  return (
    <input
      value={searchTerm}
      onChange={e => setSearchTerm(e.target.value)}
      placeholder="输入搜索关键词..."
    />
  )
}

12.5 自定义 Hook:useWindowSize

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

interface WindowSize {
  width: number
  height: number
}

function useWindowSize(): WindowSize {
  const [windowSize, setWindowSize] = useState<WindowSize>({
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0
  })
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }
    
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  
  return windowSize
}

export default useWindowSize
// 使用示例
function ResponsiveComponent() {
  const { width, height } = useWindowSize()
  
  return (
    <div>
      <p>窗口大小: {width} x {height}</p>
      {width < 768 && <p>移动端布局</p>}
      {width >= 768 && width < 1024 && <p>平板布局</p>}
      {width >= 1024 && <p>桌面端布局</p>}
    </div>
  )
}

12.6 自定义 Hook 的独立性

每次调用都是独立的状态实例:

function Component() {
  // 两个独立的请求,互不影响
  const user1 = useFetch('/api/users/1')
  const user2 = useFetch('/api/users/2')
  
  return (
    <div>
      {user1.loading ? '加载中...' : user1.data?.name} - 
      {user2.loading ? '加载中...' : user2.data?.name}
    </div>
  )
}

十三、TanStack Query(推荐)

对于数据获取场景,推荐使用 TanStack Query(原 React Query),它提供了更强大的功能。

13.1 基本使用

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

// 查询数据
function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('请求失败')
      return response.json()
    }
  })
  
  if (isLoading) return <Spinner />
  if (error) return <ErrorMessage />
  
  return (
    <div>
      <h2>{data.name}</h2>
      <button onClick={() => refetch()}>刷新</button>
    </div>
  )
}

// 修改数据
function UpdateUser() {
  const queryClient = useQueryClient()
  
  const mutation = useMutation({
    mutationFn: async (newName: string) => {
      const response = await fetch('/api/users/1', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: newName })
      })
      return response.json()
    },
    onSuccess: () => {
      // 更新成功后,使缓存失效,重新获取数据
      queryClient.invalidateQueries({ queryKey: ['user'] })
    }
  })
  
  return (
    <button onClick={() => mutation.mutate('新名字')}>
      {mutation.isPending ? '更新中...' : '更新用户名'}
    </button>
  )
}

13.2 核心优势

特性说明
自动缓存相同 queryKey 的请求自动缓存
后台刷新窗口聚焦时自动更新数据
重复请求合并多个组件请求同一数据,只发一次请求
状态管理内置 loading、error、refetch 等
失败重试自动重试失败的请求

13.3 缓存机制

// 组件 A
const { data } = useQuery({ 
  queryKey: ['user', '123'], 
  queryFn: fetchUser 
})

// 组件 B(其他地方)
const { data } = useQuery({ 
  queryKey: ['user', '123'], 
  queryFn: fetchUser 
})

// ✅ 只发起一次网络请求,两个组件共享数据

十四、Hook 使用规则

React 对 Hook 有两条强制规则:

规则 1:只在顶层调用

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

// ❌ 错误:在循环中调用
for (let i = 0; i < 3; i++) {
  const [value, setValue] = useState(i)
}

// ✅ 正确:始终在顶层调用
const [count, setCount] = useState(0)
if (condition) {
  setCount(count + 1)
}

原因: React 依靠 Hook 的调用顺序来管理状态。

规则 2:只在 React 函数中调用

// ❌ 错误:在普通函数中调用
function handleClick() {
  const [count, setCount] = useState(0)  // 会报错!
}

// ✅ 正确:在组件或自定义 Hook 中调用
function MyComponent() {
  const [count, setCount] = useState(0)  // OK
}

// ✅ 正确:在自定义 Hook 中调用
function useMyHook() {
  const [count, setCount] = useState(0)  // OK
  return count
}

ESLint 检查

推荐使用 eslint-plugin-react-hooks 插件,自动检查这些规则:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

十五、常见问题

Q1: 为什么 useEffect 中的数据不是最新的?

function Counter() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count)  // 始终打印 0!
    }, 1000)
    return () => clearInterval(id)
  }, [])  // 空依赖数组
  
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

原因: 空依赖数组导致 effect 只执行一次,闭包捕获的是初始值。

解决方案:

// 方案1:添加依赖
useEffect(() => {
  const id = setInterval(() => {
    console.log(count)
  }, 1000)
  return () => clearInterval(id)
}, [count])  // 添加 count 到依赖

// 方案2:使用函数式更新
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1)  // 使用函数式更新
  }, 1000)
  return () => clearInterval(id)
}, [])

// 方案3:使用 useRef
const countRef = useRef(count)
countRef.current = count

useEffect(() => {
  const id = setInterval(() => {
    console.log(countRef.current)
  }, 1000)
  return () => clearInterval(id)
}, [])

Q2: 什么时候用 useMemouseCallback

function Parent({ items }: { items: Item[] }) {
  // useMemo: 缓存计算结果
  const sortedItems = useMemo(() => {
    console.log('重新排序')
    return [...items].sort((a, b) => a.name.localeCompare(b.name))
  }, [items])
  
  // useCallback: 缓存函数引用
  const handleClick = useCallback((id: string) => {
    console.log('Clicked:', id)
  }, [])
  
  return <Child items={sortedItems} onClick={handleClick} />
}

原则: 先写代码,遇到性能问题再优化。大多数情况下不需要过早优化。

Q3: 函数组件 vs 类组件,应该用哪个?

新项目:全部使用函数组件 + Hook

场景推荐
新项目函数组件
旧项目维护可以混用,新代码用函数组件
错误边界类组件(目前唯一例外)

React 官方明确表示:Hook 是 React 的未来,新特性优先支持函数组件。


十六、命名约定

React 生态有统一的命名约定:

前缀用途示例
useHookuseState, useFetch, useGetUser
handle事件处理函数handleClick, handleSubmit
on事件回调 proponClick, onChange, onSubmit
is/has布尔值isLoading, hasError, isVisible
get获取数据的函数getUser, getParams

Hook 命名示例:

// 内置 Hook
useState, useEffect, useContext, useRef

// 自定义 Hook
useFetch, useLocalStorage, useDebounce

// TanStack Query 封装
useGetWebAppParams, useGetUserCanAccessApp

十七、学习路径建议

  1. 基础阶段

    • 理解 useStateuseEffect
    • 完成官方教程的小项目
  2. 进阶阶段

    • 学习 useContextuseReducer
    • 学习自定义 Hook 的封装
    • 了解性能优化(useMemouseCallback
  3. 实战阶段

    • 学习 TanStack Query 处理数据请求
    • 学习状态管理(Zustand、Jotai 等)
    • 阅读优秀开源项目的代码
  4. 推荐资源


十八、总结

Hook用途核心要点
useState状态管理返回 [值, 更新函数],不可变更新
useEffect副作用处理注意依赖数组,记得清理
useRefDOM引用/持久值修改 .current 不触发渲染
useContext跨组件共享避免 prop drilling
useMemo缓存计算结果优化昂贵计算
useCallback缓存函数引用配合 memo 子组件使用
useReducer复杂状态类似 Redux 的模式
useLayoutEffect同步副作用DOM 测量、防闪烁
useImperativeHandle暴露方法配合 forwardRef

记住:Hook 让 React 更简单、更优雅。掌握 Hook,就是掌握了现代 React 开发的核心。