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 的状态变量,初始值为 0
const [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}>
<input
name="name"
value={user.name}
onChange={handleChange}
placeholder="姓名"
required
/>
<input
name="email"
type="email"
value={user.email}
onChange={handleChange}
placeholder="邮箱"
required
/>
<input
name="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>
<input
value={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 (
<button
onClick={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() {
// 初始化状态和 dispatch
const [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}>
<input
value={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'
}}>
<input
type="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() {
// 创建 ref
const inputRef = useRef(null);
const focusInput = () => {
// 聚焦输入框
inputRef.current.focus();
};
// 组件挂载后自动聚焦
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<input
ref={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>
<input
value={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 变化时,更新 localStorage
useEffect(() => {
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 (
<div
style={{
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) });
}
}, []);
// 保存到 localStorage
useEffect(() => {
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' }}>
<input
type="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'
}}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
style={{ marginRight: '10px' }}
/>
<span style={{ flex: 1 }}>{todo.text}</span>
<button
onClick={() => 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 => (
<TodoItem
key={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 函数组件开发场景!

985

被折叠的 条评论
为什么被折叠?



