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 开发的核心概念:
- React Hooks 让函数组件拥有状态和生命周期
- 自定义 Hooks 让逻辑复用变得简单
- 项目中的 Hooks 展示了实际业务场景的应用
- 遵循使用规则 确保 Hooks 正常工作
通过合理使用 Hooks,可以写出更简洁、可维护的 React 代码!