React Hook 入门指南

React Hook 入门指南

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


一、什么是 Hook?

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

为什么需要 Hook?

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

tsx 复制代码
// ❌ 类组件 - 代码冗长,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>
    )
  }
}
tsx 复制代码
// ✅ 函数组件 + 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 基础用法:简单计数器

tsx 复制代码
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 管理对象状态

tsx 复制代码
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 管理数组状态

tsx 复制代码
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 函数式更新

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

tsx 复制代码
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 惰性初始化

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

tsx 复制代码
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 切换布尔值

tsx 复制代码
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 基础用法:数据获取

tsx 复制代码
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 订阅与清理

tsx 复制代码
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 定时器示例

tsx 复制代码
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 事件监听

tsx 复制代码
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 依赖数组的三种情况

tsx 复制代码
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 操作示例

tsx 复制代码
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 引用

tsx 复制代码
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 表单提交示例

tsx 复制代码
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

tsx 复制代码
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 跟踪前一次值

tsx 复制代码
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 可变值(不触发渲染)

tsx 复制代码
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 对比

tsx 复制代码
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>
  )
}
特性 useState useRef
值变化时重新渲染 ✅ 是 ❌ 否
用途 需要显示的数据 DOM 引用、定时器 ID、前一次值等
更新方式 setter 函数 直接修改 .current

六、useContext 详解

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

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

tsx 复制代码
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

tsx 复制代码
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

tsx 复制代码
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 基础用法:缓存计算结果

tsx 复制代码
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 列表过滤和排序

tsx 复制代码
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 避免子组件不必要的渲染

tsx 复制代码
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 计算派生状态

tsx 复制代码
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 基础用法:缓存回调函数

tsx 复制代码
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 配合子组件使用

tsx 复制代码
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 依赖其他状态

tsx 复制代码
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 依赖

tsx 复制代码
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

tsx 复制代码
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>
}
特性 useCallback useMemo
缓存内容 函数引用 任意计算结果
返回值 函数本身 计算结果
使用场景 传递给子组件的回调、useEffect 依赖 昂贵的计算、派生状态

九、useReducer 详解

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

9.1 基础用法:计数器

tsx 复制代码
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 复杂表单状态

tsx 复制代码
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 列表(经典示例)

tsx 复制代码
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

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

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

十、useLayoutEffect 详解

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

10.1 基础用法:防止闪烁

tsx 复制代码
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 动画初始状态

tsx 复制代码
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

tsx 复制代码
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>
  )
}
特性 useEffect useLayoutEffect
执行时机 DOM 更新后,浏览器绘制后 DOM 更新后,浏览器绘制前
阻塞绘制 否(异步) 是(同步)
使用场景 大多数副作用 DOM 测量、防止闪烁
服务端渲染 支持 会警告,需特殊处理

十一、useImperativeHandle 详解

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

11.1 基础用法

tsx 复制代码
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 模态框组件示例

tsx 复制代码
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 暴露部分方法

tsx 复制代码
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 示例:封装数据获取逻辑

tsx 复制代码
// 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
tsx 复制代码
// 使用自定义 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

tsx 复制代码
// 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
tsx 复制代码
// 使用示例
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

tsx 复制代码
// 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
tsx 复制代码
// 使用示例:搜索输入
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

tsx 复制代码
// 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
tsx 复制代码
// 使用示例
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 的独立性

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

tsx 复制代码
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 基本使用

tsx 复制代码
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 缓存机制

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

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

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

十四、Hook 使用规则

React 对 Hook 有两条强制规则:

规则 1:只在顶层调用

tsx 复制代码
// ❌ 错误:在条件语句中调用
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 函数中调用

tsx 复制代码
// ❌ 错误:在普通函数中调用
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 插件,自动检查这些规则:

json 复制代码
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

十五、常见问题

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

tsx 复制代码
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 只执行一次,闭包捕获的是初始值。

解决方案:

tsx 复制代码
// 方案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

tsx 复制代码
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 生态有统一的命名约定:

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

Hook 命名示例:

tsx 复制代码
// 内置 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 副作用处理 注意依赖数组,记得清理
useRef DOM引用/持久值 修改 .current 不触发渲染
useContext 跨组件共享 避免 prop drilling
useMemo 缓存计算结果 优化昂贵计算
useCallback 缓存函数引用 配合 memo 子组件使用
useReducer 复杂状态 类似 Redux 的模式
useLayoutEffect 同步副作用 DOM 测量、防闪烁
useImperativeHandle 暴露方法 配合 forwardRef

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

相关推荐
核以解忧2 小时前
借助VTable Skill实现10W+数据渲染
前端
WangHappy2 小时前
不写 Canvas 也能搞定!小程序图片导出的 WebView 通信方案
前端·微信小程序
李剑一2 小时前
要闹哪样?又出现了一款新的格式化插件,尤雨溪力荐,速度提升了惊人的45倍!
前端·vue.js
闲云一鹤2 小时前
Git LFS 扫盲教程 - 你不会还在用 Git 管理大文件吧?
前端·git·前端工程化
阿虎儿3 小时前
React Context 详解:从入门到性能优化
前端·vue.js·react.js
Sailing3 小时前
🚀 别再乱写 16px 了!CSS 单位体系已经进入“计算时代”,真正的响应式布局
前端·css·面试
喝水的长颈鹿3 小时前
【大白话前端 03】Web 标准与最佳实践
前端
爱泡脚的鸡腿3 小时前
Node.js 拓展
前端·后端
左夕5 小时前
分不清apply,bind,call?看这篇文章就够了
前端·javascript