React Hooks 和自定义 Hooks 的概念和使用方法

13 阅读6分钟

React Hooks 是什么?

React Hooks 是 React 16.8 引入的新特性,让函数组件也能使用状态和其他 React 特性。

传统类组件 vs 函数组件 + Hooks

传统类组件:

class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }

  componentDidMount() {
    document.title = `点击了 ${this.state.count} 次`
  }

  componentDidUpdate() {
    document.title = `点击了 ${this.state.count} 次`
  }

  render() {
    return (
      <div>
        <p>你点击了 {this.state.count} 次</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          点击我
        </button>
      </div>
    )
  }
}

函数组件 + Hooks:

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `点击了 ${count} 次`
  }, [count])

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  )
}

常用 React Hooks

1. useState - 状态管理

import React, { useState } from 'react'
import { View, Text } from '@tarojs/components'
import { Button } from '@nutui/nutui-react-taro'

const UseStateExample = () => {
  // 基本用法
  const [count, setCount] = useState(0)
  
  // 对象状态
  const [user, setUser] = useState({
    name: '张三',
    age: 25
  })
  
  // 数组状态
  const [items, setItems] = useState(['苹果', '香蕉'])

  // 更新基本状态
  const handleIncrement = () => {
    setCount(count + 1)
  }

  // 更新对象状态
  const handleUpdateUser = () => {
    setUser({
      ...user,  // 展开运算符,保留原有属性
      age: user.age + 1
    })
  }

  // 更新数组状态
  const handleAddItem = () => {
    setItems([...items, '橙子'])
  }

  return (
    <View className='p-4'>
      <Text className='text-lg font-bold mb-4'>useState 示例</Text>
      
      {/* 基本状态 */}
      <View className='mb-4'>
        <Text>计数: {count}</Text>
        <Button onClick={handleIncrement}>增加</Button>
      </View>

      {/* 对象状态 */}
      <View className='mb-4'>
        <Text>用户: {user.name}, 年龄: {user.age}</Text>
        <Button onClick={handleUpdateUser}>增加年龄</Button>
      </View>

      {/* 数组状态 */}
      <View className='mb-4'>
        <Text>物品: {items.join(', ')}</Text>
        <Button onClick={handleAddItem}>添加物品</Button>
      </View>
    </View>
  )
}

export default UseStateExample

2. useEffect - 副作用处理

import React, { useState, useEffect } from 'react'
import { View, Text } from '@tarojs/components'
import { Button } from '@nutui/nutui-react-taro'

const UseEffectExample = () => {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')
  const [windowWidth, setWindowWidth] = useState(0)

  // 1. 每次渲染都执行
  useEffect(() => {
    console.log('组件渲染了')
  })

  // 2. 只在组件挂载时执行一次
  useEffect(() => {
    console.log('组件挂载了')
    document.title = 'useEffect 示例'
    
    // 清理函数
    return () => {
      console.log('组件卸载了')
      document.title = 'React App'
    }
  }, [])

  // 3. 当 count 变化时执行
  useEffect(() => {
    console.log(`count 变化了: ${count}`)
  }, [count])

  // 4. 当 name 变化时执行
  useEffect(() => {
    if (name) {
      console.log(`名字变化了: ${name}`)
    }
  }, [name])

  // 5. 监听窗口大小变化
  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth)
    }

    // 设置初始值
    setWindowWidth(window.innerWidth)
    
    // 添加事件监听
    window.addEventListener('resize', handleResize)
    
    // 清理函数:移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return (
    <View className='p-4'>
      <Text className='text-lg font-bold mb-4'>useEffect 示例</Text>
      
      <View className='mb-4'>
        <Text>计数: {count}</Text>
        <Button onClick={() => setCount(count + 1)}>增加计数</Button>
      </View>

      <View className='mb-4'>
        <Text>名字: {name}</Text>
        <Button onClick={() => setName('李四')}>设置名字</Button>
      </View>

      <View className='mb-4'>
        <Text>窗口宽度: {windowWidth}px</Text>
      </View>

      <Text className='text-sm text-gray-500'>
        打开控制台查看 useEffect 的执行情况
      </Text>
    </View>
  )
}

export default UseEffectExample

3. useContext - 上下文使用

import React, { createContext, useContext, useState } from 'react'
import { View, Text } from '@tarojs/components'
import { Button } from '@nutui/nutui-react-taro'

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

// 提供者组件
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light')

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

const UserProvider = ({ children }) => {
  const [user, setUser] = useState({ name: '张三', age: 25 })

  const updateUser = (newUser) => {
    setUser(newUser)
  }

  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {children}
    </UserContext.Provider>
  )
}

// 使用上下文的组件
const ThemedButton = () => {
  const { theme, toggleTheme } = useContext(ThemeContext)
  
  return (
    <Button 
      onClick={toggleTheme}
      className={theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-black'}
    >
      当前主题: {theme}
    </Button>
  )
}

const UserInfo = () => {
  const { user, updateUser } = useContext(UserContext)
  
  const handleUpdateAge = () => {
    updateUser({ ...user, age: user.age + 1 })
  }
  
  return (
    <View>
      <Text>用户: {user.name}, 年龄: {user.age}</Text>
      <Button onClick={handleUpdateAge}>增加年龄</Button>
    </View>
  )
}

// 主组件
const UseContextExample = () => {
  return (
    <ThemeProvider>
      <UserProvider>
        <View className='p-4'>
          <Text className='text-lg font-bold mb-4'>useContext 示例</Text>
          
          <View className='mb-4'>
            <ThemedButton />
          </View>
          
          <View className='mb-4'>
            <UserInfo />
          </View>
        </View>
      </UserProvider>
    </ThemeProvider>
  )
}

export default UseContextExample

🎯 自定义 Hooks

自定义 Hooks 是封装可复用逻辑的函数,必须以 use 开头。

1. 基础自定义 Hook

import React, { useState, useEffect } from 'react'
import { View, Text } from '@tarojs/components'
import { Button } from '@nutui/nutui-react-taro'

// 自定义 Hook:计数器
const useCounter = (initialValue = 0, step = 1) => {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount(count + step)
  const decrement = () => setCount(count - step)
  const reset = () => setCount(initialValue)

  return {
    count,
    increment,
    decrement,
    reset
  }
}

// 自定义 Hook:窗口大小
const useWindowSize = () => {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  })

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return size
}

// 自定义 Hook:本地存储
const 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]
}

// 使用自定义 Hooks 的组件
const CustomHooksBasicExample = () => {
  // 使用计数器 Hook
  const { count, increment, decrement, reset } = useCounter(0, 2)
  
  // 使用窗口大小 Hook
  const { width, height } = useWindowSize()
  
  // 使用本地存储 Hook
  const [name, setName] = useLocalStorage('userName', '张三')

  return (
    <View className='p-4'>
      <Text className='text-lg font-bold mb-4'>自定义 Hooks 基础示例</Text>
      
      {/* 计数器 */}
      <View className='mb-4'>
        <Text className='font-bold'>计数器 (步长: 2)</Text>
        <Text>当前值: {count}</Text>
        <View className='flex gap-2'>
          <Button onClick={decrement}>-2</Button>
          <Button onClick={increment}>+2</Button>
          <Button onClick={reset}>重置</Button>
        </View>
      </View>

      {/* 窗口大小 */}
      <View className='mb-4'>
        <Text className='font-bold'>窗口大小</Text>
        <Text>宽度: {width}px, 高度: {height}px</Text>
      </View>

      {/* 本地存储 */}
      <View className='mb-4'>
        <Text className='font-bold'>本地存储</Text>
        <Text>用户名: {name}</Text>
        <Button onClick={() => setName('李四')}>更改用户名</Button>
      </View>
    </View>
  )
}

export default CustomHooksBasicExample

2. 复杂自定义 Hook

import React, { useState, useEffect, useCallback, useRef } from 'react'
import { View, Text } from '@tarojs/components'
import { Button, Toast } from '@nutui/nutui-react-taro'

// 自定义 Hook:异步数据获取
const useAsync = (asyncFunction, immediate = true) => {
  const [status, setStatus] = useState('idle') // idle, pending, success, error
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)

  const execute = useCallback(async (...params) => {
    setStatus('pending')
    setData(null)
    setError(null)

    try {
      const response = await asyncFunction(...params)
      setData(response)
      setStatus('success')
      return response
    } catch (err) {
      setError(err)
      setStatus('error')
      throw err
    }
  }, [asyncFunction])

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

  return { execute, status, data, error }
}

// 自定义 Hook:防抖
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value)

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

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

  return debouncedValue
}

// 自定义 Hook:点击外部关闭
const useClickOutside = (ref, handler) => {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return
      }
      handler(event)
    }

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

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

// 自定义 Hook:键盘事件
const useKeyPress = (targetKey) => {
  const [keyPressed, setKeyPressed] = useState(false)

  useEffect(() => {
    const downHandler = ({ key }) => {
      if (key === targetKey) {
        setKeyPressed(true)
      }
    }

    const upHandler = ({ key }) => {
      if (key === targetKey) {
        setKeyPressed(false)
      }
    }

    window.addEventListener('keydown', downHandler)
    window.addEventListener('keyup', upHandler)

    return () => {
      window.removeEventListener('keydown', downHandler)
      window.removeEventListener('keyup', upHandler)
    }
  }, [targetKey])

  return keyPressed
}

// 模拟 API 函数
const fetchUserData = async (userId) => {
  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 1000))
  
  if (userId === 'error') {
    throw new Error('用户不存在')
  }
  
  return {
    id: userId,
    name: '张三',
    email: 'zhangsan@example.com',
    avatar: 'https://via.placeholder.com/100'
  }
}

// 使用复杂自定义 Hooks 的组件
const CustomHooksAdvancedExample = () => {
  const [userId, setUserId] = useState('1')
  const [searchTerm, setSearchTerm] = useState('')
  const [isDropdownOpen, setIsDropdownOpen] = useState(false)
  
  // 使用异步数据获取 Hook
  const { execute: fetchUser, status, data: user, error } = useAsync(
    () => fetchUserData(userId),
    false // 不立即执行
  )

  // 使用防抖 Hook
  const debouncedSearchTerm = useDebounce(searchTerm, 500)

  // 使用点击外部关闭 Hook
  const dropdownRef = useRef()
  useClickOutside(dropdownRef, () => setIsDropdownOpen(false))

  // 使用键盘事件 Hook
  const isEnterPressed = useKeyPress('Enter')

  // 处理搜索
  useEffect(() => {
    if (debouncedSearchTerm) {
      console.log('搜索:', debouncedSearchTerm)
    }
  }, [debouncedSearchTerm])

  // 处理回车键
  useEffect(() => {
    if (isEnterPressed) {
      Toast.show('按下了回车键!')
    }
  }, [isEnterPressed])

  return (
    <View className='p-4'>
      <Text className='text-lg font-bold mb-4'>复杂自定义 Hooks 示例</Text>
      
      {/* 异步数据获取 */}
      <View className='mb-4'>
        <Text className='font-bold'>异步数据获取</Text>
        <View className='flex gap-2 mb-2'>
          <Button onClick={() => fetchUser('1')}>获取用户1</Button>
          <Button onClick={() => fetchUser('2')}>获取用户2</Button>
          <Button onClick={() => fetchUser('error')}>测试错误</Button>
        </View>
        
        {status === 'pending' && <Text>加载中...</Text>}
        {status === 'success' && user && (
          <View>
            <Text>用户: {user.name}</Text>
            <Text>邮箱: {user.email}</Text>
          </View>
        )}
        {status === 'error' && <Text className='text-red-500'>错误: {error?.message}</Text>}
      </View>

      {/* 防抖搜索 */}
      <View className='mb-4'>
        <Text className='font-bold'>防抖搜索</Text>
        <input
          type='text'
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder='输入搜索内容...'
          className='border p-2 rounded'
        />
        <Text>防抖后的搜索词: {debouncedSearchTerm}</Text>
      </View>

      {/* 点击外部关闭 */}
      <View className='mb-4'>
        <Text className='font-bold'>点击外部关闭</Text>
        <View ref={dropdownRef} className='relative'>
          <Button onClick={() => setIsDropdownOpen(!isDropdownOpen)}>
            {isDropdownOpen ? '关闭下拉菜单' : '打开下拉菜单'}
          </Button>
          {isDropdownOpen && (
            <View className='absolute top-full left-0 bg-white border p-2 rounded shadow'>
              <Text>下拉菜单内容</Text>
              <Text>点击外部可以关闭</Text>
            </View>
          )}
        </View>
      </View>

      {/* 键盘事件 */}
      <View className='mb-4'>
        <Text className='font-bold'>键盘事件</Text>
        <Text>按下回车键试试: {isEnterPressed ? '已按下' : '未按下'}</Text>
      </View>
    </View>
  )
}

export default CustomHooksAdvancedExample

📚 自定义 Hooks 的优势

1. 代码复用

  • 将通用逻辑提取到自定义 Hook 中
  • 多个组件可以共享相同的逻辑
  • 减少重复代码

2. 关注点分离

  • 将业务逻辑从 UI 组件中分离
  • 组件专注于渲染,Hook 专注于逻辑
  • 提高代码可维护性

3. 状态管理简化

  • 封装复杂的状态管理逻辑
  • 提供简洁的 API 接口
  • 隐藏实现细节

Hooks 使用规则

1. 只在函数组件中使用

// ✅ 正确
function MyComponent() {
  const [count, setCount] = useState(0)
  return <div>{count}</div>
}

// ❌ 错误
class MyComponent extends React.Component {
  render() {
    const [count, setCount] = useState(0) // 不能在类组件中使用
    return <div>{count}</div>
  }
}

2. 只在顶层调用

// ✅ 正确
function MyComponent() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')
  
  if (count > 0) {
    // 逻辑处理
  }
  
  return <div>{count}</div>
}

// ❌ 错误
function MyComponent() {
  const [count, setCount] = useState(0)
  
  if (count > 0) {
    const [name, setName] = useState('') // 不能在条件语句中调用
  }
  
  return <div>{count}</div>
}

3. 自定义 Hook 必须以 use 开头

// ✅ 正确
const useCounter = () => {
  const [count, setCount] = useState(0)
  return { count, setCount }
}

// ❌ 错误
const counter = () => { // 不以 use 开头
  const [count, setCount] = useState(0)
  return { count, setCount }
}

🔧 实际应用场景

1. 表单处理

const useForm = (initialValues) => {
  const [values, setValues] = useState(initialValues)
  const [errors, setErrors] = useState({})

  const handleChange = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }))
  }

  const handleSubmit = (onSubmit) => {
    onSubmit(values)
  }

  return { values, errors, handleChange, handleSubmit }
}

2. API 调用

const useAPI = (apiFunction) => {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  const execute = async (...params) => {
    setLoading(true)
    setError(null)
    try {
      const result = await apiFunction(...params)
      setData(result)
      return result
    } catch (err) {
      setError(err)
      throw err
    } finally {
      setLoading(false)
    }
  }

  return { data, loading, error, execute }
}

3. 本地存储

const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (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]
}

🎯 总结

React Hooks 和自定义 Hooks 是现代 React 开发的核心概念:

  1. React Hooks 让函数组件拥有状态和生命周期
  2. 自定义 Hooks 让逻辑复用变得简单
  3. 项目中的 Hooks 展示了实际业务场景的应用
  4. 遵循使用规则 确保 Hooks 正常工作

通过合理使用 Hooks,可以写出更简洁、可维护的 React 代码!