React学习教程,从入门到精通,React Hook 详解 —— 语法知识点、使用方法与案例代码(26)
React Hook 详解 —— 语法知识点、使用方法与案例代码
🎯 本章目标
全面掌握 React Hook 的核心概念与实战用法,包括:
- ✅ Hook 技术介绍
- ✅ State Hook(useState)
- ✅ Effect Hook(useEffect)
- ✅ React 内置 Hook(useContext、useReducer、useRef、useMemo、useCallback 等)
- ✅ 自定义 Hook
- ✅ 注意事项与最佳实践
- ✅ 综合性实战案例
一、Hook 技术介绍
Hook 是 React 16.8 新增特性,让你在不编写 class 的情况下使用 state 以及其他 React 特性。
✅ 为什么使用 Hook?
- 函数组件也能拥有状态和生命周期
- 逻辑复用更简单(自定义 Hook)
- 代码更简洁、易测试、易理解
- 避免“Wrapper Hell”(组件嵌套地狱)
⚠️ Hook 使用规则
- 只能在函数组件或自定义 Hook 中调用
- 必须在顶层调用(不能在循环、条件、嵌套函数中)
- 命名以
use
开头
二、State Hook —— useState
用于在函数组件中声明状态变量。
2.1 基础语法
const [state, setState] = useState(initialValue);
state
:当前状态值setState
:更新状态的函数initialValue
:初始值,可以是任意类型(对象、数组、函数等)
2.2 案例1:计数器
import React, { useState } from 'react';function Counter() {// 声明一个叫 count 的状态变量,初始值为 0const [count, setCount] = useState(0);return (<div><h2>当前计数: {count}</h2><button onClick={() => setCount(count + 1)}>➕ 增加</button><button onClick={() => setCount(count - 1)}>➖ 减少</button><button onClick={() => setCount(0)}>🔄 重置</button></div>);
}export default Counter;
✅ 注:
setCount
是异步的,多次调用可能合并。如需基于前一状态更新,应使用函数形式:
setCount(prevCount => prevCount + 1);
2.3 案例2:对象状态更新(表单)
import React, { useState } from 'react';function UserProfile() {// 初始化对象状态const [user, setUser] = useState({name: '',email: '',age: ''});// 处理输入变化const handleChange = (e) => {const { name, value } = e.target;// 使用函数式更新,避免状态覆盖setUser(prevUser => ({...prevUser, // 展开旧状态[name]: value // 动态键名更新}));};const handleSubmit = (e) => {e.preventDefault();alert(`提交数据: ${JSON.stringify(user, null, 2)}`);};return (<form onSubmit={handleSubmit}><inputname="name"value={user.name}onChange={handleChange}placeholder="姓名"required/><inputname="email"type="email"value={user.email}onChange={handleChange}placeholder="邮箱"required/><inputname="age"type="number"value={user.age}onChange={handleChange}placeholder="年龄"/><button type="submit">提交</button></form>);
}export default UserProfile;
三、Effect Hook —— useEffect
用于处理副作用(如数据获取、订阅、手动 DOM 操作等),替代 class 组件中的
componentDidMount
、componentDidUpdate
、componentWillUnmount
。
3.1 基础语法
useEffect(() => {// 副作用逻辑return () => {// 清理函数(可选)};
}, [dependencies]); // 依赖数组(可选)
3.2 案例1:组件挂载时执行(模拟 componentDidMount)
import React, { useEffect } from 'react';function Welcome() {useEffect(() => {console.log('✅ 组件已挂载');// 可选:返回清理函数return () => {console.log('🧹 组件将卸载');};}, []); // 空数组 = 仅在挂载/卸载时执行return <h1>欢迎使用 React Hook!</h1>;
}export default Welcome;
3.3 案例2:依赖更新时执行(模拟 componentDidUpdate)
import React, { useState, useEffect } from 'react';function Clock() {const [time, setTime] = useState(new Date());useEffect(() => {console.log('⏰ 时间更新:', time.toLocaleTimeString());// 设置定时器const timer = setInterval(() => {setTime(new Date());}, 1000);// 清理定时器(避免内存泄漏)return () => {clearInterval(timer);console.log('🧹 定时器已清理');};}, []); // 仅在组件挂载时设置一次return <h2>当前时间: {time.toLocaleTimeString()}</h2>;
}export default Clock;
3.4 案例3:依赖特定状态(监听 count 变化)
import React, { useState, useEffect } from 'react';function EffectDemo() {const [count, setCount] = useState(0);const [name, setName] = useState('');// 仅当 count 变化时执行useEffect(() => {console.log(`🔢 count 变为: ${count}`);}, [count]); // 依赖数组包含 count// 仅当 name 变化时执行useEffect(() => {console.log(`📛 name 变为: ${name}`);}, [name]);return (<div><p>Count: {count}</p><button onClick={() => setCount(c => c + 1)}>增加 Count</button><p>Name: {name}</p><inputvalue={name}onChange={(e) => setName(e.target.value)}placeholder="输入名字"/></div>);
}export default EffectDemo;
四、React 内置 Hook
4.1 useContext
—— 访问上下文
用于在组件树中共享数据(如主题、用户信息),避免逐层传递 props。
// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';// 创建 Context
const ThemeContext = createContext();// 提供者组件
export function ThemeProvider({ children }) {const [theme, setTheme] = useState('light');const toggleTheme = () => {setTheme(prev => prev === 'light' ? 'dark' : 'light');};return (<ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>);
}// 自定义 Hook 方便使用
export function useTheme() {const context = useContext(ThemeContext);if (!context) {throw new Error('useTheme 必须在 ThemeProvider 内使用');}return context;
}
// App.js
import React from 'react';
import { ThemeProvider, useTheme } from './ThemeContext';function ThemedButton() {const { theme, toggleTheme } = useTheme();return (<buttononClick={toggleTheme}style={{background: theme === 'dark' ? '#333' : '#eee',color: theme === 'dark' ? '#fff' : '#000',padding: '10px 20px',border: 'none',borderRadius: '4px',cursor: 'pointer'}}>当前主题: {theme} 🌓</button>);
}function App() {return (<ThemeProvider><div style={{ padding: '20px' }}><h1> useContext 示例</h1><ThemedButton /></div></ThemeProvider>);
}export default App;
4.2 useReducer
—— 复杂状态逻辑
适用于状态逻辑较复杂、包含多个子值、或下一个 state 依赖于前一个 state 的情况。
import React, { useReducer } from 'react';// 定义 reducer 函数
function todoReducer(state, action) {switch (action.type) {case 'ADD_TODO':return [...state,{id: Date.now(),text: action.payload,completed: false}];case 'TOGGLE_TODO':return state.map(todo =>todo.id === action.payload? { ...todo, completed: !todo.completed }: todo);case 'DELETE_TODO':return state.filter(todo => todo.id !== action.payload);default:return state;}
}function TodoApp() {// 初始化状态和 dispatchconst [todos, dispatch] = useReducer(todoReducer, []);const [inputValue, setInputValue] = useState('');const handleSubmit = (e) => {e.preventDefault();if (inputValue.trim()) {dispatch({ type: 'ADD_TODO', payload: inputValue });setInputValue('');}};return (<div><form onSubmit={handleSubmit}><inputvalue={inputValue}onChange={(e) => setInputValue(e.target.value)}placeholder="添加待办事项"/><button type="submit">添加</button></form><ul>{todos.map(todo => (<li key={todo.id} style={{textDecoration: todo.completed ? 'line-through' : 'none',color: todo.completed ? '#888' : '#000'}}><inputtype="checkbox"checked={todo.completed}onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}/>{todo.text}<button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>❌</button></li>))}</ul></div>);
}export default TodoApp;
4.3 useRef
—— 访问 DOM 或保存可变值
用于获取 DOM 元素引用,或保存在渲染之间不变的可变值(不会触发重渲染)。
import React, { useRef, useEffect } from 'react';function TextInputWithFocusButton() {// 创建 refconst inputRef = useRef(null);const focusInput = () => {// 聚焦输入框inputRef.current.focus();};// 组件挂载后自动聚焦useEffect(() => {inputRef.current.focus();}, []);return (<div><inputref={inputRef}type="text"placeholder="点击按钮或自动聚焦"/><button onClick={focusInput}>聚焦输入框</button></div>);
}export default TextInputWithFocusButton;
// 保存上一次值(不触发重渲染)
function CounterWithPrevious() {const [count, setCount] = useState(0);const prevCountRef = useRef();useEffect(() => {// 更新 ref(不会触发重渲染)prevCountRef.current = count;}, [count]); // 仅当 count 变化时更新const prevCount = prevCountRef.current;return (<div><h3>当前: {count}, 上次: {prevCount !== undefined ? prevCount : '无'}</h3><button onClick={() => setCount(c => c + 1)}>+1</button></div>);
}
4.4 useMemo
—— 缓存计算结果
用于缓存昂贵的计算结果,避免不必要的重复计算。
import React, { useState, useMemo } from 'react';function ExpensiveComponent({ list, filterText }) {// 模拟昂贵计算:过滤 + 映射const filteredList = useMemo(() => {console.log('🔍 执行过滤计算');return list.filter(item => item.name.toLowerCase().includes(filterText.toLowerCase())).map(item => ({...item,displayName: `${item.name} (${item.age})`}));}, [list, filterText]); // 仅当 list 或 filterText 变化时重新计算return (<ul>{filteredList.map(item => (<li key={item.id}>{item.displayName}</li>))}</ul>);
}function App() {const [filter, setFilter] = useState('');const [count, setCount] = useState(0); // 无关状态// 模拟大型列表const users = [{ id: 1, name: '张三', age: 25 },{ id: 2, name: '李四', age: 30 },{ id: 3, name: '王五', age: 35 },{ id: 4, name: '赵六', age: 28 },];return (<div><inputvalue={filter}onChange={(e) => setFilter(e.target.value)}placeholder="过滤用户..."/><button onClick={() => setCount(c => c + 1)}>改变无关状态: {count}</button><ExpensiveComponent list={users} filterText={filter} /></div>);
}export default App;
✅ 控制台只会打印一次 “执行过滤计算”,除非
filter
或users
变化。
4.5 useCallback
—— 缓存函数
用于缓存函数引用,避免子组件因函数引用变化而重新渲染。
import React, { useState, useCallback } from 'react';// 模拟“昂贵”的子组件(带 memo)
const Child = React.memo(({ onClick, name }) => {console.log(`👶 ${name} 重新渲染`);return <button onClick={onClick}>点击 {name}</button>;
});function Parent() {const [count, setCount] = useState(0);// ❌ 每次渲染都创建新函数(导致 Child 重新渲染)// const handleClick = () => {// console.log('点击了');// };// ✅ 使用 useCallback 缓存函数const handleClick = useCallback(() => {console.log('点击了');}, []); // 无依赖,函数引用不变return (<div><h3>父组件状态: {count}</h3><button onClick={() => setCount(c => c + 1)}>增加父组件状态</button>{/* 传递缓存函数,避免 Child 不必要重渲染 */}<Child onClick={handleClick} name="子组件A" /><Child onClick={handleClick} name="子组件B" /></div>);
}export default Parent;
✅ 控制台只会在首次渲染时打印 “👶 子组件A 重新渲染” 和 “👶 子组件B 重新渲染”,之后点击“增加父组件状态”不会导致子组件重新渲染。
五、自定义 Hook
将组件逻辑提取到可重用的函数中。
5.1 案例1:自定义计数器 Hook
// hooks/useCounter.js
import { useState, useCallback } from 'react';export function useCounter(initialValue = 0) {const [count, setCount] = useState(initialValue);const increment = useCallback(() => setCount(c => c + 1), []);const decrement = useCallback(() => setCount(c => c - 1), []);const reset = useCallback(() => setCount(initialValue), [initialValue]);return {count,increment,decrement,reset};
}
// 组件中使用
import React from 'react';
import { useCounter } from './hooks/useCounter';function CounterA() {const { count, increment, decrement, reset } = useCounter(10);return (<div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}><h4>计数器A (初始值10)</h4><p>值: {count}</p><button onClick={increment}>+</button><button onClick={decrement}>-</button><button onClick={reset}>重置</button></div>);
}function CounterB() {const { count, increment } = useCounter(0);return (<div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}><h4>计数器B (初始值0)</h4><p>值: {count}</p><button onClick={increment}>+</button></div>);
}function App() {return (<div><CounterA /><CounterB /></div>);
}export default App;
5.2 案例2:自定义 localStorage Hook
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';export 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('localStorage 读取失败:', error);return initialValue;}});// 当 storedValue 变化时,更新 localStorageuseEffect(() => {try {window.localStorage.setItem(key, JSON.stringify(storedValue));} catch (error) {console.error('localStorage 写入失败:', error);}}, [key, storedValue]);return [storedValue, setStoredValue];
}
// 使用
import React from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';function ThemeSwitcher() {const [theme, setTheme] = useLocalStorage('app-theme', 'light');const toggleTheme = () => {setTheme(prev => prev === 'light' ? 'dark' : 'light');};return (<divstyle={{background: theme === 'dark' ? '#333' : '#fff',color: theme === 'dark' ? '#fff' : '#000',minHeight: '100vh',padding: '20px'}}><h1>主题: {theme}</h1><button onClick={toggleTheme}>切换主题</button><p>刷新页面,主题状态将被保留!</p></div>);
}export default ThemeSwitcher;
六、注意事项
6.1 Hook 调用顺序必须一致
❌ 错误示例:
function BadComponent({ condition }) {const [count, setCount] = useState(0);if (condition) {// 条件调用 Hook —— 违反规则!const [name, setName] = useState('张三');}useEffect(() => {// ...}, []);return <div>...</div>;
}
✅ 正确做法:
function GoodComponent({ condition }) {const [count, setCount] = useState(0);const [name, setName] = useState('张三'); // 始终调用useEffect(() => {// ...}, []);return <div>...</div>;
}
6.2 不要在循环、条件或嵌套函数中调用 Hook
❌ 错误:
function BadList({ items }) {items.forEach(item => {const [state, setState] = useState(null); // 在循环中调用 —— 错误!});
}
6.3 使用 ESLint 插件避免错误
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc
{"plugins": ["react-hooks"],"rules": {"react-hooks/rules-of-hooks": "error","react-hooks/exhaustive-deps": "warn"}
}
6.4 useEffect 依赖数组注意事项
❌ 错误:忘记添加依赖
function BadEffect({ userId }) {const [data, setData] = useState(null);useEffect(() => {// userId 变化时不会重新执行!fetch(`/api/user/${userId}`).then(res => res.json()).then(setData);}, []); // ❌ 缺少 userId 依赖return <div>{data?.name}</div>;
}
✅ 正确:
useEffect(() => {fetch(`/api/user/${userId}`).then(res => res.json()).then(setData);
}, [userId]); // ✅ 添加依赖
✅ 更安全:使用 eslint-plugin-react-hooks 自动检测依赖
七、综合性案例:Todo 应用(包含多种 Hook)
// hooks/useTodos.js
import { useReducer, useEffect } from 'react';const TODO_STORAGE_KEY = 'react-todos';function todoReducer(state, action) {switch (action.type) {case 'ADD':return [...state, { id: Date.now(), text: action.text, completed: false }];case 'TOGGLE':return state.map(todo =>todo.id === action.id ? { ...todo, completed: !todo.completed } : todo);case 'DELETE':return state.filter(todo => todo.id !== action.id);case 'LOAD':return action.todos;default:return state;}
}export function useTodos() {const [todos, dispatch] = useReducer(todoReducer, []);// 从 localStorage 加载useEffect(() => {const saved = localStorage.getItem(TODO_STORAGE_KEY);if (saved) {dispatch({ type: 'LOAD', todos: JSON.parse(saved) });}}, []);// 保存到 localStorageuseEffect(() => {localStorage.setItem(TODO_STORAGE_KEY, JSON.stringify(todos));}, [todos]);const addTodo = (text) => dispatch({ type: 'ADD', text });const toggleTodo = (id) => dispatch({ type: 'TOGGLE', id });const deleteTodo = (id) => dispatch({ type: 'DELETE', id });return {todos,addTodo,toggleTodo,deleteTodo};
}
// components/TodoForm.js
import React, { useState } from 'react';export default function TodoForm({ onAdd }) {const [text, setText] = useState('');const handleSubmit = (e) => {e.preventDefault();if (text.trim()) {onAdd(text);setText('');}};return (<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}><inputtype="text"value={text}onChange={(e) => setText(e.target.value)}placeholder="添加新任务..."style={{ padding: '8px', marginRight: '8px', width: '200px' }}/><button type="submit" style={{ padding: '8px 16px' }}>添加</button></form>);
}
// components/TodoItem.js
import React from 'react';export default function TodoItem({ todo, onToggle, onDelete }) {return (<li style={{display: 'flex',alignItems: 'center',padding: '8px',borderBottom: '1px solid #eee',textDecoration: todo.completed ? 'line-through' : 'none',color: todo.completed ? '#888' : '#000'}}><inputtype="checkbox"checked={todo.completed}onChange={() => onToggle(todo.id)}style={{ marginRight: '10px' }}/><span style={{ flex: 1 }}>{todo.text}</span><buttononClick={() => onDelete(todo.id)}style={{background: 'none',border: 'none',color: 'red',cursor: 'pointer',fontSize: '18px'}}>❌</button></li>);
}
// App.js
import React from 'react';
import TodoForm from './components/TodoForm';
import TodoItem from './components/TodoItem';
import { useTodos } from './hooks/useTodos';function App() {const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();const activeCount = todos.filter(t => !t.completed).length;const completedCount = todos.filter(t => t.completed).length;return (<div style={{ maxWidth: '500px', margin: '50px auto', padding: '20px' }}><h1>📝 Todo 应用</h1><TodoForm onAdd={addTodo} /><div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}><span>活动: {activeCount}</span><span>已完成: {completedCount}</span></div><ul style={{ listStyle: 'none', padding: 0 }}>{todos.map(todo => (<TodoItemkey={todo.id}todo={todo}onToggle={toggleTodo}onDelete={deleteTodo}/>))}</ul>{todos.length === 0 && (<p style={{ textAlign: 'center', color: '#888' }}>暂无任务,添加一个吧!</p>)}</div>);
}export default App;
✅ 功能亮点:
- 使用
useReducer
管理复杂状态 - 使用
useEffect
同步 localStorage - 自定义 Hook
useTodos
封装逻辑 - 组件拆分清晰
- 状态持久化
📌 本章小结
Hook 名称 | 用途 | 关键点 |
---|---|---|
useState | 声明状态 | 支持函数式更新,避免状态覆盖 |
useEffect | 处理副作用 | 依赖数组控制执行时机,注意清理函数 |
useContext | 访问上下文 | 避免 prop drilling,全局状态共享 |
useReducer | 复杂状态管理 | 适合多个子状态、状态转换逻辑复杂场景 |
useRef | 访问 DOM / 保存可变值 | 不触发重渲染,用于聚焦、计时器、前值保存等 |
useMemo | 缓存计算结果 | 优化性能,避免重复昂贵计算 |
useCallback | 缓存函数引用 | 避免子组件不必要重渲染 |
自定义 Hook | 逻辑复用 | 命名以 use 开头,可组合多个 Hook |
🚀 最佳实践:
- 优先使用
useState
+useEffect
- 复杂状态用
useReducer
- 性能优化用
useMemo
/useCallback
- 逻辑复用写自定义 Hook
- 始终遵守 Hook 规则
- 使用 ESLint 插件辅助
掌握这些 Hook,你已能应对绝大多数 React 函数组件开发场景!