在 React Hooks 出现之前,函数组件总给人一种 "能力不足" 的感觉 —— 不能拥有自己的状态,也无法处理副作用。Hooks 的出现彻底改变了这一点,让函数组件能够轻松实现状态管理、生命周期控制等复杂功能。本文从实际开发场景出发,带你掌握从基础 Hook 到自定义 Hook 的完整用法。
一、useState:让函数组件拥有状态
useState是最基础也最常用的 Hook,它让函数组件能够管理自己的状态。
比如实现一个计数器:
import { useState } from 'react';function Counter() { // 声明一个count状态,初始值为0 // setCount是更新状态的函数 const [count, setCount] = useState(0); return ( <div> <p>你点击了{count}次</p> <button onClick={() => setCount(count + 1)}> 点击我 </button> </div> );}
useState的参数是初始状态,返回值是一个数组,第一个元素是当前状态,第二个是更新状态的函数。
更新状态时可以使用函数形式,确保依赖最新状态:
// 处理异步更新或依赖前一次状态的情况<button onClick={() => setCount(prevCount => prevCount + 1)}> 点击我</button>
这种函数形式在处理倒计时、累加器等场景特别有用,避免因闭包导致的状态更新问题。
二、useEffect:处理副作用
组件中除了渲染 UI 之外的操作(如请求数据、订阅事件、操作 DOM)都叫副作用,这些工作都应该放在useEffect中处理。
基础用法:组件挂载和更新时执行
import { useState, useEffect } from 'react';function UserProfile({ userId }) { const [user, setUser] = useState(null); // 当userId变化时,重新获取用户数据 useEffect(() => { // 定义获取数据的函数 const fetchUser = async () => { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); setUser(data); }; fetchUser(); }, [userId]); // 依赖数组:只有userId变化时才重新执行 if (!user) return <div>加载中...</div>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );}
useEffect的第一个参数是副作用函数,第二个参数是依赖数组:
- 依赖数组为空[]:只在组件挂载时执行一次
- 依赖数组有值[a, b]:组件挂载时执行,且 a 或 b 变化时重新执行
- 没有依赖数组:每次组件渲染都会执行
清理副作用
有些副作用需要清理(如事件监听、定时器),可以在副作用函数中返回一个清理函数:
function WindowSize() { const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { // 定义处理函数 const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight }); }; // 监听窗口大小变化 window.addEventListener('resize', handleResize); // 初始化执行一次 handleResize(); // 返回清理函数 return () => { window.removeEventListener('resize', handleResize); }; }, []); // 空依赖数组:只执行一次 return ( <div> 窗口宽度:{size.width},高度:{size.height} </div> );}
清理函数会在组件卸载时执行,避免内存泄漏。
三、useContext:跨组件传值
当组件层级较深时,用props传值很繁琐,useContext可以轻松实现跨组件数据共享。
首先创建上下文:
// 创建上下文import { createContext, useContext, useState } from 'react';const ThemeContext = createContext();// 提供上下文的组件export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> );}// 自定义Hook简化使用export function useTheme() { return useContext(ThemeContext);}
然后在任意子组件中使用:
import { useTheme } from './ThemeContext';function ThemedButton() { const { theme, setTheme } = useTheme(); return ( <button style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#333' : '#fff' }} onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} > 当前主题:{theme},点击切换 </button> );}
这种方式避免了 "props drilling"( props 层层传递)的问题,特别适合主题切换、用户登录状态等全局数据的管理。
四、自定义 Hook:封装复用逻辑
当多个组件需要用到相同的逻辑时,可以把这些逻辑封装成自定义 Hook,这是 Hooks 最强大的特性之一。
示例:封装表单处理逻辑
import { useState } from 'react';// 自定义Hook:处理表单输入function useForm(initialValues) { const [values, setValues] = useState(initialValues); const handleChange = (e) => { const { name, value } = e.target; setValues(prev => ({ ...prev, [name]: value })); }; const resetForm = () => setValues(initialValues); return { values, handleChange, resetForm };}// 使用自定义Hookfunction LoginForm() { const { values, handleChange, resetForm } = useForm({ username: '', password: '' }); const handleSubmit = (e) => { e.preventDefault(); console.log('提交表单:', values); // 调用API提交数据... }; return ( <form onSubmit={handleSubmit}> <div> <label>用户名:</label> <input name="username" value={values.username} onChange={handleChange} /> </div> <div> <label>密码:</label> <input name="password" type="password" value={values.password} onChange={handleChange} /> </div> <button type="submit">登录</button> <button type="button" onClick={resetForm}>重置</button> </form> );}
自定义 Hook 必须以use开头,这样 React 才能识别并应用 Hooks 的规则。
另一个示例:处理本地存储
// 自定义Hook:同步localStorage的状态function useLocalStorage(key, initialValue) { // 从localStorage读取初始值 const [value, setValue] = useState(() => { const storedValue = localStorage.getItem(key); return storedValue ? JSON.parse(storedValue) : initialValue; }); // 当值变化时同步到localStorage useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue];}// 使用function Settings() { const [darkMode, setDarkMode] = useLocalStorage('darkMode', false); return ( <div> <label> <input type="checkbox" checked={darkMode} onChange={(e) => setDarkMode(e.target.checked)} /> 开启深色模式 </label> </div> );}
这个 Hook 让状态自动同步到本地存储,刷新页面后状态不会丢失,非常实用。
五、使用 Hooks 的注意事项
- 只能在函数组件或自定义 Hook 中使用:不能在普通函数、循环、条件语句中调用 Hooks。
- 遵循调用顺序:Hooks 的调用顺序必须稳定,不要在if、for中调用:
// 错误做法if (count > 0) { useEffect(() => { // ... });}// 正确做法useEffect(() => { if (count > 0) { // ... }});
- 依赖数组要完整:useEffect的依赖数组要包含所有用到的外部变量,否则可能导致闭包问题。
可以安装eslint-plugin-react-hooks插件,自动检测这些问题:
// .eslintrc.jsmodule.exports = { plugins: ['react-hooks'], rules: { 'react-hooks/rules-of-hooks': 'error', // 检查Hook调用规则 'react-hooks/exhaustive-deps': 'warn' // 检查依赖数组 }};
总结
React Hooks 彻底改变了 React 组件的编写方式:
- useState让函数组件拥有状态
- useEffect处理各种副作用,替代类组件的生命周期
- 自定义 Hook 实现逻辑复用,让代码更简洁
学习 Hooks 的关键是理解 "副作用隔离" 和 "逻辑复用" 这两个核心思想。刚开始使用时可能会遇到闭包陷阱、依赖数组设置错误等问题,多写多练就能慢慢掌握。
记住,Hooks 不是要完全替代类组件,而是提供了一种更简洁、更灵活的方式来编写 React 组件。在实际项目中,根据场景选择合适的写法,才能发挥 React 的最大威力。