React Hooks 完全指南

AI Content Team
23 分钟阅读

全面讲解 React Hooks,包括内置钩子、自定义钩子和最佳实践

React Hooks 完全指南

Hooks 改变了 React 的开发方式。本文全面讲解如何使用和创建 Hooks。

内置 Hooks

useState - 状态管理

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('John')
  const [user, setUser] = useState({
    age: 30,
    email: 'john@example.com',
  })
  
  // 使用函数初始化状态(对于复杂初始值)
  const [data, setData] = useState(() => {
    console.log('初始化数据...')
    return fetchInitialData() // 仅在首次渲染时调用
  })
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      
      {/* 函数式更新 */}
      <button onClick={() => setCount(prev => prev + 1)}>
        函数式增加
      </button>
      
      {/* 更新对象 */}
      <button onClick={() => setUser({ ...user, age: user.age + 1 })}>
        增加年龄
      </button>
    </div>
  )
}

useEffect - 副作用处理

import { useState, useEffect } from 'react'

function DataFetcher() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [userId, setUserId] = useState(1)
  
  // 副作用 - 每次渲染后执行
  useEffect(() => {
    console.log('组件已挂载或已更新')
  })
  
  // 挂载时执行一次
  useEffect(() => {
    console.log('组件已挂载')
    
    return () => {
      console.log('组件已卸载')
    }
  }, [])
  
  // 当 userId 改变时执行
  useEffect(() => {
    let isMounted = true // 防止内存泄漏
    
    const fetchData = async () => {
      setLoading(true)
      try {
        const response = await fetch(\`/api/users/\${userId}\`)
        const result = await response.json()
        
        if (isMounted) {
          setData(result)
        }
      } catch (err) {
        if (isMounted) {
          setError(err)
        }
      } finally {
        if (isMounted) {
          setLoading(false)
        }
      }
    }
    
    fetchData()
    
    // 清理函数
    return () => {
      isMounted = false
    }
  }, [userId])
  
  if (loading) return <p>加载中...</p>
  if (error) return <p>错误: {error.message}</p>
  
  return <div>{data && JSON.stringify(data)}</div>
}

useContext - 跨组件通信

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

// 创建上下文
const ThemeContext = createContext()

// 提供者组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }
  
  const value = { theme, toggleTheme }
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

// 使用 Hook
function useTheme() {
  const context = useContext(ThemeContext)
  
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用')
  }
  
  return context
}

// 组件使用
function App() {
  const { theme, toggleTheme } = useTheme()
  
  return (
    <div style={{
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#000' : '#fff',
    }}>
      <p>当前主题: {theme}</p>
      <button onClick={toggleTheme}>切换主题</button>
    </div>
  )
}

// 使用
export default function Root() {
  return (
    <ThemeProvider>
      <App />
    </ThemeProvider>
  )
}

自定义 Hooks

useLocalStorage

import { useState, useEffect } from 'react'

function useLocalStorage(key, initialValue) {
  // 从本地存储获取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  })
  
  // 当值改变时更新本地存储
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }
  
  return [storedValue, setValue]
}

// 使用
function App() {
  const [name, setName] = useLocalStorage('name', 'Guest')
  
  return (
    <div>
      <p>姓名: {name}</p>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  )
}

useAsync - 异步操作

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

function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = useState('idle')
  const [value, setValue] = useState(null)
  const [error, setError] = useState(null)
  
  // 使用 ref 来防止无限循环
  const executeRef = useRef(null)
  
  const execute = useRef(async () => {
    setStatus('pending')
    setValue(null)
    setError(null)
    
    try {
      const response = await asyncFunction()
      setValue(response)
      setStatus('success')
      return response
    } catch (error) {
      setError(error)
      setStatus('error')
    }
  })
  
  executeRef.current = execute.current
  
  useEffect(() => {
    if (!immediate) return
    
    executeRef.current()
  }, [immediate])
  
  return { execute: executeRef.current, status, value, error }
}

// 使用
function UserProfile({ userId }) {
  const { execute, status, value: user, error } = useAsync(
    () => fetch(\`/api/users/\${userId}\`).then(r => r.json()),
    true
  )
  
  if (status === 'pending') return <p>加载中...</p>
  if (status === 'error') return <p>错误: {error?.message}</p>
  if (status === 'success') return <p>用户: {user?.name}</p>
  
  return null
}

useFetch - 数据获取

import { useState, useEffect } from 'react'

function useFetch(url, options = {}) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    let isMounted = true
    
    const fetchData = async () => {
      try {
        const response = await fetch(url, {
          method: 'GET',
          ...options,
        })
        
        if (!response.ok) {
          throw new Error(\`HTTP error! status: \${response.status}\`)
        }
        
        const result = await response.json()
        
        if (isMounted) {
          setData(result)
          setError(null)
        }
      } catch (err) {
        if (isMounted) {
          setError(err)
          setData(null)
        }
      } finally {
        if (isMounted) {
          setLoading(false)
        }
      }
    }
    
    fetchData()
    
    return () => {
      isMounted = false
    }
  }, [url, options])
  
  const refetch = async () => {
    setLoading(true)
    try {
      const response = await fetch(url, options)
      const result = await response.json()
      setData(result)
    } catch (err) {
      setError(err)
    } finally {
      setLoading(false)
    }
  }
  
  return { data, loading, error, refetch }
}

// 使用
function UserList() {
  const { data: users, loading, error, refetch } = useFetch('/api/users')
  
  if (loading) return <p>加载中...</p>
  if (error) return <p>错误: {error.message}</p>
  
  return (
    <div>
      <button onClick={refetch}>刷新</button>
      <ul>
        {users?.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

usePrevious - 保存前一个值

import { useEffect, useRef } from 'react'

function usePrevious(value) {
  const ref = useRef()
  
  useEffect(() => {
    ref.current = value
  }, [value])
  
  return ref.current
}

// 使用
function Counter() {
  const [count, setCount] = React.useState(0)
  const prevCount = usePrevious(count)
  
  return (
    <div>
      <p>当前: {count}, 前一个: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  )
}

高级模式

useReducer - 复杂状态管理

import { useReducer } from 'react'

const initialState = {
  todos: [],
  filter: 'all',
  error: null,
}

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.payload }],
      }
    
    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload),
      }
    
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    
    case 'SET_ERROR':
      return { ...state, error: action.payload }
    
    default:
      return state
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState)
  
  const addTodo = (text) => {
    dispatch({ type: 'ADD_TODO', payload: text })
  }
  
  const removeTodo = (id) => {
    dispatch({ type: 'REMOVE_TODO', payload: id })
  }
  
  return (
    <div>
      {state.todos.map(todo => (
        <div key={todo.id}>
          {todo.text}
          <button onClick={() => removeTodo(todo.id)}>删除</button>
        </div>
      ))}
    </div>
  )
}

最佳实践

应该做的事:

  • 将相关逻辑提取到自定义 Hooks
  • 在 useEffect 的依赖数组中包含所有依赖
  • 使用 useCallback 和 useMemo 优化性能
  • 为自定义 Hooks 编写文档
  • 及时清理副作用

不应该做的事:

  • 在条件或循环中调用 Hooks
  • 在普通函数中调用 Hooks
  • 忘记依赖数组
  • 过度使用 useMemo/useCallback
  • 在 Hooks 中创建过多的闭包

检查清单

  • Hooks 调用顺序正确
  • 依赖数组完整
  • 副作用正确清理
  • 性能优化得当
  • 代码易于理解和测试