React的hooks
React Hooks 是 React 16.8 引入的革命性特性,它让你在函数组件中使用状态(state)和其他 React 特性,而无需编写 class。
简单来说:Hooks 就是让函数组件拥有类组件的能力,但代码更简洁。
一、为什么需要 Hooks?
1.1 类组件的痛点
// 类组件示例 - 复杂、冗余
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this); // 烦人的绑定
}
componentDidMount() {
// 初始化逻辑
console.log('组件挂载');
}
componentDidUpdate() {
// 更新逻辑
console.log('组件更新');
}
componentWillUnmount() {
// 清理逻辑
console.log('组件卸载');
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>你点击了 {this.state.count} 次</p>
<button onClick={this.handleClick}>
点击我
</button>
</div>
);
}
}
类组件的问题:
- this 绑定问题:需要手动绑定或使用箭头函数
- 逻辑分散:相关代码分散在多个生命周期方法中
- 状态逻辑复用难:需要使用高阶组件或 Render Props
- 组件嵌套地狱:多个高阶组件嵌套导致代码难以理解
1.2 Hooks 的优势
// 使用 Hooks 的函数组件 - 简洁、直观
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('组件挂载或更新');
return () => {
console.log('清理副作用');
};
}, [count]);
return (
<div>
<p>你点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>
点击我
</button>
</div>
);
}
二、核心 Hooks 详解
2.1 useState - 状态管理
作用:为函数组件添加状态
import { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量,初始值为 0
const [count, setCount] = useState(0);
// 可以声明多个 state 变量
const [name, setName] = useState('张三');
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React' },
{ id: 2, text: '学习 Hooks' }
]);
return (
<div>
<p>姓名: {name}</p>
<p>点击次数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
<button onClick={() => setName('李四')}>
改名
</button>
{/* 更新数组 */}
<button onClick={() => {
setTodos([...todos, { id: 3, text: '学习 TypeScript' }]);
}}>
添加待办
</button>
</div>
);
}
2.2 useEffect - 副作用处理
- 作用:处理副作用(数据获取、订阅、手动修改 DOM 等).
- 什么是“副作用”: 在 React 组件中,主要职责是计算和渲染 UI。但除了渲染 UI,我们常常还需要做一些其他事情,比如:
- 数据获取:从 API 获取数据.
- 订阅:设置一个 WebSocket 连接或 addEventListener.
- 手动修改 DOM:直接操作页面标题、焦点等.
- 设置定时器:setTimeout 或 setInterval.
这些在组件渲染之外,与外部系统进行交互的操作,就叫做副作用.
2.2.1 useEffect 示例
package.json
index.html
index.js
App.jsx
App.css
{
"name": "react",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --base ./"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8"
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Hello World</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.jsx"></script>
</body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
import React, { useState, useEffect } from 'react';
import './App.css';
function TodoApp() {
// 状态
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
const [countdown, setCountdown] = useState(5); // 倒计时(秒)
// 🔹 useEffect 1:挂载时从 localStorage 加载数据
useEffect(() => {
console.log('✅ 挂载:尝试从 localStorage 加载待办事项...');
const saved = localStorage.getItem('react-todos');
if (saved) {
try {
setTodos(JSON.parse(saved));
} catch (e) {
console.error('❌ 解析 localStorage 数据失败', e);
}
}
}, []); // 仅挂载时执行
// 🔹 useEffect 2:当 todos 变化时,自动保存到 localStorage(持久化)
useEffect(() => {
console.log('💾 自动保存 todos 到 localStorage');
localStorage.setItem('react-todos', JSON.stringify(todos));
}, [todos]); // 依赖 todos
// 🔹 useEffect 3:根据 filter 和 todos 更新页面标题
useEffect(() => {
const activeCount = todos.filter(t => !t.completed).length;
const title = filter === 'completed'
? `✅ ${todos.filter(t => t.completed).length} 已完成`
: filter === 'active'
? `⏳ ${activeCount} 进行中`
: `📝 全部 (${todos.length})`;
document.title = `Todo App - ${title}`;
}, [todos, filter]); // 依赖 todos 和 filter
// 🔹 useEffect 4:设置倒计时提醒(每秒减1),卸载时清除
useEffect(() => {
console.log('⏰ 启动倒计时提醒...');
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
// 倒计时结束,重置为5
return 5;
}
return prev - 1;
});
}, 1000);
// 🔁 清理函数:组件卸载或重新执行前清除定时器
return () => {
console.log('🧹 清理倒计时定时器');
clearInterval(timer);
};
}, []); // 仅挂载时设置
// 事件处理
const addTodo = (e) => {
e.preventDefault();
if (inputValue.trim() === '') return;
const newTodo = {
id: Date.now(),
text: inputValue.trim(),
completed: false,
createdAt: new Date().toISOString()
};
setTodos([...todos, newTodo]);
setInputValue('');
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
// 过滤后的待办事项
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<div className="app">
<header className="header">
<h1>📝 React Todo List</h1>
<div className="countdown-badge">
提醒倒计时: <strong>{countdown}s</strong>
</div>
</header>
{/* 添加表单 */}
<form onSubmit={addTodo} className="add-form">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入新任务,回车添加..."
className="input"
autoFocus
/>
<button type="submit" className="add-btn">+</button>
</form>
{/* 过滤器 */}
<div className="filters">
<button
onClick={() => setFilter('all')}
className={filter === 'all' ? 'active' : ''}
>
全部 ({todos.length})
</button>
<button
onClick={() => setFilter('active')}
className={filter === 'active' ? 'active' : ''}
>
进行中 ({todos.filter(t => !t.completed).length})
</button>
<button
onClick={() => setFilter('completed')}
className={filter === 'completed' ? 'active' : ''}
>
已完成 ({todos.filter(t => t.completed).length})
</button>
</div>
{/* 待办列表 */}
<ul className="todo-list">
{filteredTodos.length === 0 ? (
<li className="empty">暂无任务 {filter === 'completed' ? '✅' : '⏳'}</li>
) : (
filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className="todo-text">{todo.text}</span>
</label>
<button
onClick={() => deleteTodo(todo.id)}
className="delete-btn"
aria-label="删除"
>
×
</button>
</li>
))
)}
</ul>
{/* 底部操作 */}
{todos.length > 0 && (
<div className="footer">
<span>
{todos.filter(t => !t.completed).length} 项未完成
</span>
<button onClick={clearCompleted} className="clear-btn">
清除已完成
</button>
</div>
)}
<footer className="credits">
💡 数据自动保存至浏览器 localStorage | 刷新页面不丢失
</footer>
</div>
);
}
export default TodoApp;
.app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.header {
text-align: center;
margin-bottom: 20px;
position: relative;
}
.countdown-badge {
position: absolute;
top: 0;
right: 0;
background: #ff6b6b;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.85rem;
}
.add-form {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 1rem;
}
.input:focus {
outline: none;
border-color: #4caf50;
}
.add-btn {
width: 40px;
background: #4caf50;
color: white;
border: none;
border-radius: 6px;
font-size: 1.5rem;
cursor: pointer;
}
.filters {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.filters button {
flex: 1;
min-width: 80px;
padding: 8px;
border: 1px solid #ccc;
background: #f8f9fa;
border-radius: 4px;
cursor: pointer;
}
.filters button.active {
background: #4caf50;
color: white;
border-color: #4caf50;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed .todo-text {
text-decoration: line-through;
color: #888;
}
.todo-list li:hover {
background-color: #f9f9f9;
}
.todo-text {
margin-left: 8px;
}
.delete-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 1.2rem;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.empty {
text-align: center;
color: #888;
padding: 20px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #eee;
}
.clear-btn {
background: #ff6b6b;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.credits {
text-align: center;
margin-top: 30px;
color: #888;
font-size: 0.85rem;
}
在线测试
2.3 useContext - 跨组件传值
作用:useContext 提供了一种在组件树中跨层级传递数据的方法,而无需通过每一层组件手动地传递 props。简单来说,它解决了 React 中一个常见的痛点:Props 穿透。
2.3.1 不使用 useContext 的痛点
<App>
<Page>
<Layout>
<Header> {/* Header 需要主题信息 */}
<ThemeButton> {/* ThemeButton 也需要主题信息来改变样式 */}
点击我
</ThemeButton>
</Header>
</Layout>
</Page>
</App>
如果 App 组件有一个 theme 状态,ThemeButton 需要用它来决定自己的样式。在 useContext 出现之前,你必须这样传递数据:
- App 把 theme 传给 Page。
- Page 对 theme 没兴趣,但只能把它再传给 Layout.
- Layout 也对 theme 没兴趣,也只能把它传给 Header.
- Header 可能还是用不上,最后才传给 ThemeButton.
这个过程就像一个“击鼓传花”的游戏,中间的组件被迫接收它们自己并不需要的 props,这使得代码变得冗余、难以维护.
2.3.2 使用 usecontext 示例
package.json
index.html
index.js
App.jsx
App.css
{
"name": "react",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --base ./"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8"
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Hello World</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.jsx"></script>
</body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
// App.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import './App.css';
// 🔹 1. 创建 Context(主题 + 语言)
const AppContext = createContext();
// 🔹 2. 自定义 Hook:useAppContext(推荐写法)
const useAppContext = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within AppProvider');
}
return context;
};
// 🔹 3. Provider 组件:包裹整个应用,提供状态和方法
const AppProvider = ({ children }) => {
// 从 localStorage 读取初始值(持久化)
const getInitialTheme = () => {
const saved = localStorage.getItem('app-theme');
return saved === 'dark' ? 'dark' : 'light';
};
const getInitialLang = () => {
const saved = localStorage.getItem('app-lang');
return saved === 'en' ? 'en' : 'zh';
};
const [theme, setTheme] = useState(getInitialTheme());
const [lang, setLang] = useState(getInitialLang());
// 🔸 useEffect:当 theme/lang 变化时,保存到 localStorage
useEffect(() => {
localStorage.setItem('app-theme', theme);
// 更新 <html> class,方便全局样式控制
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
useEffect(() => {
localStorage.setItem('app-lang', lang);
}, [lang]);
// 切换主题
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// 切换语言
const toggleLanguage = () => {
setLang(prev => prev === 'zh' ? 'en' : 'zh');
};
// 翻译函数(简单实现,实际可用 i18n 库)
const t = (key) => {
const translations = {
zh: {
dashboard: '仪表盘',
welcome: name => `欢迎回来,${name}!`,
profile: '个人资料',
settings: '设置',
logout: '退出登录',
theme: '主题',
language: '语言',
light: '亮色',
dark: '暗色',
chinese: '中文',
english: 'English',
tasks: '待办事项',
completed: '已完成',
pending: '进行中',
notifications: '通知',
newMessage: '你有 3 条新消息',
systemUpdate: '系统将在 5 分钟后更新',
viewAll: '查看全部',
noTasks: '暂无待办事项'
},
en: {
dashboard: 'Dashboard',
welcome: name => `Welcome back, ${name}!`,
profile: 'Profile',
settings: 'Settings',
logout: 'Logout',
theme: 'Theme',
language: 'Language',
light: 'Light',
dark: 'Dark',
chinese: '中文',
english: 'English',
tasks: 'Tasks',
completed: 'Completed',
pending: 'Pending',
notifications: 'Notifications',
newMessage: 'You have 3 new messages',
systemUpdate: 'System will update in 5 minutes',
viewAll: 'View All',
noTasks: 'No tasks yet'
}
};
return translations[lang][key] || key;
};
return (
<AppContext.Provider value={{
theme,
lang,
toggleTheme,
toggleLanguage,
t
}}>
{children}
</AppContext.Provider>
);
};
// 🔹 4. 子组件:Header(使用 Context)
const Header = () => {
const { theme, lang, toggleTheme, toggleLanguage, t } = useAppContext();
return (
<header className="header">
<h1>{t('dashboard')}</h1>
<div className="controls">
<button
onClick={toggleLanguage}
className="control-btn"
title={t('language')}
>
{lang === 'zh' ? '🇨🇳' : '🇺🇸'}
</button>
<button
onClick={toggleTheme}
className="control-btn"
title={t('theme')}
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
<button className="control-btn logout-btn" title={t('logout')}>
👤
</button>
</div>
</header>
);
};
// 🔹 5. 子组件:Sidebar(使用 Context)
const Sidebar = () => {
const { t } = useAppContext();
const menuItems = [
{ icon: '📊', label: t('dashboard'), active: true },
{ icon: '👤', label: t('profile') },
{ icon: '⚙️', label: t('settings') }
];
return (
<aside className="sidebar">
<nav>
<ul>
{menuItems.map((item, index) => (
<li key={index} className={item.active ? 'active' : ''}>
<span>{item.icon}</span>
<span>{item.label}</span>
</li>
))}
</ul>
</nav>
</aside>
);
};
// 🔹 6. 子组件:TaskCard(使用 Context)
const TaskCard = () => {
const { t } = useAppContext();
const tasks = [
{ id: 1, title: '完成项目提案', status: 'pending' },
{ id: 2, title: '审核设计稿', status: 'completed' },
{ id: 3, title: '团队周会', status: 'pending' }
];
return (
<div className="card">
<div className="card-header">
<h2>{t('tasks')}</h2>
<span>{tasks.filter(t => t.status === 'pending').length} {t('pending')}</span>
</div>
<ul className="task-list">
{tasks.length > 0 ? (
tasks.map(task => (
<li key={task.id} className={task.status}>
<label>
<input
type="checkbox"
defaultChecked={task.status === 'completed'}
/>
<span>{task.title}</span>
</label>
</li>
))
) : (
<li className="empty">{t('noTasks')}</li>
)}
</ul>
</div>
);
};
// 🔹 7. 子组件:NotificationCard(使用 Context)
const NotificationCard = () => {
const { t } = useAppContext();
return (
<div className="card">
<div className="card-header">
<h2>{t('notifications')}</h2>
<button className="view-all">{t('viewAll')}</button>
</div>
<div className="notification-item">
<span className="dot"></span>
<p>{t('newMessage')}</p>
</div>
<div className="notification-item">
<span className="dot warning"></span>
<p>{t('systemUpdate')}</p>
</div>
</div>
);
};
// 🔹 8. 主应用组件
const Dashboard = () => {
const { t } = useAppContext();
return (
<div className="dashboard">
<Header />
<div className="main-content">
<Sidebar />
<main className="content">
<div className="welcome">
<h2>{t('welcome')('张三')}</h2>
<p>2024年6月15日 • 周六</p>
</div>
<div className="cards-grid">
<TaskCard />
<NotificationCard />
</div>
</main>
</div>
</div>
);
};
// 🔹 9. 根组件:包裹 Provider
function App() {
return (
<AppProvider>
<Dashboard />
</AppProvider>
);
}
export default App;
/* App.css */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #e9ecef;
--card-bg: #ffffff;
--sidebar-bg: #ffffff;
--accent: #4361ee;
--success: #4cc9f0;
--warning: #f72585;
}
[data-theme="dark"] {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--border-color: #333333;
--card-bg: #1e1e1e;
--sidebar-bg: #1a1a1a;
--accent: #4cc9f0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: 'Segoe UI', system-ui, sans-serif;
transition: background-color 0.3s, color 0.3s;
}
.dashboard {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.header h1 {
font-size: 1.8rem;
font-weight: 700;
}
.controls {
display: flex;
gap: 0.5rem;
}
.control-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: var(--card-bg);
color: var(--text-primary);
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.control-btn:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.logout-btn {
background: #ff6b6b !important;
color: white !important;
}
.main-content {
display: flex;
flex: 1;
}
.sidebar {
width: 220px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
padding: 1.5rem 0;
}
.sidebar ul {
list-style: none;
}
.sidebar li {
padding: 0.8rem 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.8rem;
transition: background 0.2s;
}
.sidebar li:hover,
.sidebar li.active {
background: var(--accent);
color: white;
}
.sidebar li.active {
border-left: 4px solid #fff;
}
.content {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
.welcome h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.welcome p {
color: var(--text-secondary);
font-size: 1.1rem;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.card {
background: var(--card-bg);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
overflow: hidden;
transition: box-shadow 0.3s;
}
[data-theme="dark"] .card {
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.card:hover {
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.2rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.card-header h2 {
font-size: 1.3rem;
font-weight: 600;
}
.view-all {
background: none;
border: none;
color: var(--accent);
cursor: pointer;
font-weight: 500;
}
.task-list {
list-style: none;
padding: 0.5rem 0;
}
.task-list li {
padding: 0.8rem 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
}
.task-list li:last-child {
border-bottom: none;
}
.task-list li.pending input {
accent-color: var(--accent);
}
.task-list li.completed input {
accent-color: #4ade80;
}
.task-list li.completed span {
text-decoration: line-through;
color: var(--text-secondary);
}
.empty {
text-align: center;
padding: 1.5rem;
color: var(--text-secondary);
}
.notification-item {
display: flex;
align-items: flex-start;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.notification-item:last-child {
border-bottom: none;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent);
margin-right: 12px;
margin-top: 6px;
}
.dot.warning {
background: var(--warning);
}
/* 响应式 */
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.cards-grid {
grid-template-columns: 1fr;
}
}
在线测试
2.4 useReducer - 复杂状态管理
作用:复杂状态逻辑的替代方案(类似 Redux)
2.4.1 使用 useReducer 示例
- useReducer 用于管理复杂的状态逻辑.它是 useState 的替代方案,特别适合处理包含多个子值的状态对象,或者下一个状态依赖于前一个状态的情况.
package.json
index.html
index.js
App.jsx
App.css
{
"name": "react",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --base ./"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8"
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Hello World</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.jsx"></script>
</body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
import React, { useReducer, useEffect, createContext, useContext } from 'react';
import './App.css';
// 🔹 1. 定义 Action 类型(推荐用常量)
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const UPDATE_QUANTITY = 'UPDATE_QUANTITY';
const CLEAR_CART = 'CLEAR_CART';
const APPLY_COUPON = 'APPLY_COUPON';
const REMOVE_COUPON = 'REMOVE_COUPON';
// 🔹 2. 初始状态
const initialState = {
items: [],
coupon: null, // { code: 'SAVE10', discount: 10 }
total: 0,
subtotal: 0,
discount: 0
};
// 🔹 3. Reducer 函数(核心逻辑)
const cartReducer = (state, action) => {
switch (action.type) {
case ADD_ITEM: {
const { item } = action;
const existingItem = state.items.find(i => i.id === item.id);
let newItems;
if (existingItem) {
// 商品已存在:数量 +1
newItems = state.items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
);
} else {
// 新商品:添加到列表
newItems = [...state.items, { ...item, quantity: 1 }];
}
return calculateTotals({ ...state, items: newItems });
}
case REMOVE_ITEM: {
const newItems = state.items.filter(i => i.id !== action.id);
return calculateTotals({ ...state, items: newItems });
}
case UPDATE_QUANTITY: {
const { id, quantity } = action;
if (quantity < 1) return state; // 防止数量 ≤0
const newItems = state.items.map(item =>
item.id === id ? { ...item, quantity } : item
);
return calculateTotals({ ...state, items: newItems });
}
case CLEAR_CART:
return { ...initialState };
case APPLY_COUPON: {
const { code, discount } = action;
return {
...state,
coupon: { code, discount },
discount: discount
};
}
case REMOVE_COUPON:
return {
...state,
coupon: null,
discount: 0
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
// 🔹 4. 辅助函数:计算总价
const calculateTotals = (state) => {
const subtotal = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const discount = state.coupon ? state.coupon.discount : 0;
const total = Math.max(0, subtotal - discount); // 防止负数
return {
...state,
subtotal: parseFloat(subtotal.toFixed(2)),
discount: parseFloat(discount.toFixed(2)),
total: parseFloat(total.toFixed(2))
};
};
// 🔹 5. 创建 Context(可选:用于跨组件共享)
const CartContext = createContext();
// 🔹 6. 自定义 Hook(推荐用法)
const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
};
// 🔹 7. Provider 组件
const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialState, () => {
// 初始化:从 localStorage 恢复(带验证)
const saved = localStorage.getItem('shopping-cart');
if (saved) {
try {
const parsed = JSON.parse(saved);
// 验证结构
if (Array.isArray(parsed.items)) {
return calculateTotals(parsed);
}
} catch (e) {
console.warn('Failed to parse cart from localStorage');
}
}
return initialState;
});
// 🔸 useEffect:持久化状态到 localStorage
useEffect(() => {
localStorage.setItem('shopping-cart', JSON.stringify(state));
}, [state]);
// 操作方法(封装 dispatch)
const addItem = (item) => dispatch({ type: ADD_ITEM, item });
const removeItem = (id) => dispatch({ type: REMOVE_ITEM, id });
const updateQuantity = (id, quantity) =>
dispatch({ type: UPDATE_QUANTITY, id, quantity });
const clearCart = () => dispatch({ type: CLEAR_CART });
const applyCoupon = (code, discount) =>
dispatch({ type: APPLY_COUPON, code, discount });
const removeCoupon = () => dispatch({ type: REMOVE_COUPON });
return (
<CartContext.Provider value={{
...state,
addItem,
removeItem,
updateQuantity,
clearCart,
applyCoupon,
removeCoupon
}}>
{children}
</CartContext.Provider>
);
};
// 🔹 8. 商品列表组件
const ProductList = () => {
const { addItem } = useCart();
const products = [
{ id: 1, name: '无线蓝牙耳机', price: 299, image: '🎧' },
{ id: 2, name: '机械键盘', price: 599, image: '⌨️' },
{ id: 3, name: '27寸显示器', price: 1299, image: '🖥️' },
{ id: 4, name: 'Type-C 数据线', price: 39, image: '🔌' }
];
return (
<div className="products">
<h2>热门商品</h2>
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<div className="product-icon">{product.image}</div>
<h3>{product.name}</h3>
<p className="price">¥{product.price}</p>
<button
onClick={() => addItem(product)}
className="add-btn"
>
加入购物车
</button>
</div>
))}
</div>
</div>
);
};
// 🔹 9. 购物车组件
const ShoppingCart = () => {
const {
items,
subtotal,
discount,
total,
coupon,
updateQuantity,
removeItem,
applyCoupon,
removeCoupon
} = useCart();
const handleApplyCoupon = () => {
const code = prompt('输入优惠码(示例:SAVE10 → 减10元):');
if (!code) return;
// 简单验证:SAVE10=10元, SAVE20=20元
const discountMap = { SAVE10: 10, SAVE20: 20 };
const discount = discountMap[code.toUpperCase()];
if (discount) {
applyCoupon(code, discount);
alert(`✅ 优惠码 ${code} 已生效!立减 ¥${discount}`);
} else {
alert('❌ 无效优惠码');
}
};
return (
<div className="cart">
<h2>🛒 购物车 ({items.length} 件)</h2>
{items.length === 0 ? (
<div className="empty-cart">
<p>购物车空空如也~</p>
<button onClick={() => document.getElementById('products').scrollIntoView()}>
去逛逛
</button>
</div>
) : (
<>
<ul className="cart-items">
{items.map(item => (
<li key={item.id} className="cart-item">
<span className="item-name">{item.name}</span>
<div className="item-controls">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
>
-
</button>
<span className="quantity">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
+
</button>
<span className="item-price">
¥{(item.price * item.quantity).toFixed(2)}
</span>
<button
className="remove-btn"
onClick={() => removeItem(item.id)}
title="删除"
>
×
</button>
</div>
</li>
))}
</ul>
<div className="coupon-section">
<input
type="text"
placeholder="输入优惠码"
value={coupon?.code || ''}
readOnly
className="coupon-input"
/>
{coupon ? (
<button onClick={removeCoupon} className="coupon-btn remove">
移除
</button>
) : (
<button onClick={handleApplyCoupon} className="coupon-btn apply">
使用
</button>
)}
</div>
<div className="summary">
<div className="summary-row">
<span>商品总额</span>
<span>¥{subtotal.toFixed(2)}</span>
</div>
{discount > 0 && (
<div className="summary-row discount">
<span>优惠 ({coupon?.code})</span>
<span>-¥{discount.toFixed(2)}</span>
</div>
)}
<div className="summary-row total">
<strong>实付总额</strong>
<strong>¥{total.toFixed(2)}</strong>
</div>
</div>
<button className="checkout-btn" disabled={total === 0}>
立即结算 (¥{total.toFixed(2)})
</button>
</>
)}
</div>
);
};
// 🔹 10. 主应用
function App() {
return (
<CartProvider>
<div className="app">
<header className="header">
<h1>🛒 React 购物车系统</h1>
<p>useReducer 实战示例 | 数据自动保存至 localStorage</p>
</header>
<main className="main">
<section id="products">
<ProductList />
</section>
<aside>
<ShoppingCart />
</aside>
</main>
<footer className="footer">
💡 尝试:添加商品 → 修改数量 → 使用优惠码 SAVE10 或 SAVE20 → 刷新页面
</footer>
</div>
</CartProvider>
);
}
export default App;
/* App.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.header h1 {
font-size: 2.2rem;
color: #2c3e50;
margin-bottom: 10px;
}
.header p {
color: #7f8c8d;
font-size: 1.1rem;
}
.main {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
/* 商品列表 */
.products h2 {
font-size: 1.8rem;
margin-bottom: 20px;
color: #2c3e50;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.product-card {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
}
.product-icon {
font-size: 3rem;
margin-bottom: 12px;
}
.product-card h3 {
font-size: 1.3rem;
margin: 10px 0;
color: #2c3e50;
}
.price {
font-size: 1.4rem;
color: #e74c3c;
font-weight: bold;
margin: 10px 0;
}
.add-btn {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.add-btn:hover {
background: #2980b9;
}
/* 购物车 */
.cart {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
position: sticky;
top: 20px;
height: fit-content;
}
.cart h2 {
font-size: 1.6rem;
margin-bottom: 20px;
color: #2c3e50;
display: flex;
align-items: center;
gap: 8px;
}
.empty-cart {
text-align: center;
padding: 40px 20px;
color: #7f8c8d;
}
.empty-cart button {
margin-top: 16px;
background: #3498db;
color: white;
border: none;
padding: 10px 24px;
border-radius: 6px;
cursor: pointer;
}
/* 购物车项 */
.cart-items {
list-style: none;
margin-bottom: 20px;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.item-name {
flex: 1;
font-weight: 500;
}
.item-controls {
display: flex;
align-items: center;
gap: 8px;
}
.item-controls button {
width: 32px;
height: 32px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.item-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.quantity {
width: 36px;
text-align: center;
font-weight: 500;
}
.item-price {
min-width: 70px;
text-align: right;
font-weight: 500;
}
.remove-btn {
width: 28px;
height: 28px;
border-radius: 50%;
background: #ff6b6b;
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* 优惠券 */
.coupon-section {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.coupon-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
}
.coupon-btn {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
.coupon-btn.apply {
background: #2ecc71;
color: white;
}
.coupon-btn.remove {
background: #e74c3c;
color: white;
}
/* 总计 */
.summary {
margin-bottom: 20px;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 1.1rem;
}
.summary-row.discount {
color: #27ae60;
}
.summary-row.total {
font-size: 1.3rem;
padding-top: 12px;
border-top: 1px solid #eee;
}
.checkout-btn {
width: 100%;
padding: 14px;
background: #e74c3c;
color: white;
border: none;
border-radius: 8px;
font-size: 1.2rem;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.checkout-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.checkout-btn:hover:not(:disabled) {
background: #c0392b;
}
/* 页脚 */
.footer {
text-align: center;
margin-top: 30px;
padding: 20px;
color: #7f8c8d;
font-size: 0.95rem;
}
/* 响应式 */
@media (max-width: 768px) {
.main {
grid-template-columns: 1fr;
}
.cart {
position: static;
}
}
在线测试
2.5 useRef - 引用 DOM 或存储可变值
- useRef 用于创建可变的引用对象,这些引用对象在组件的整个生命周期中保持不变。它的主要特点是:
- 返回一个可变的对象:{ current: initialValue }
- 不会触发组件重新渲染:修改 .current 不会导致组件重新渲染
- 跨渲染周期保持稳定:每次渲染返回的是同一个对象
- 与实例变量类似:类似于类组件中的实例属性
import { useRef, useEffect } from 'react';
function TextInputWithFocusButton() {
// 1. 创建 ref
const inputRef = useRef(null);
const countRef = useRef(0); // 存储可变值,不会触发重新渲染
const prevValueRef = useRef('');
// 2. 访问 DOM
const focusInput = () => {
inputRef.current.focus();
inputRef.current.select();
};
useEffect(() => {
// 记录渲染次数
countRef.current += 1;
console.log(`组件渲染了 ${countRef.current} 次`);
});
// 3. 存储上一次的值
useEffect(() => {
prevValueRef.current = inputRef.current?.value || '';
});
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="输入一些文字"
/>
<button onClick={focusInput}>
聚焦输入框
</button>
<p>渲染次数: {countRef.current}</p>
<p>上一次的值: {prevValueRef.current}</p>
</div>
);
}
在线测试
2.6 useMemo 和 useCallback - 性能优化
作用:避免不必要的计算和重新渲染
import { useState, useMemo, useCallback } from 'react';
function ExpensiveCalculation({ num }) {
// useMemo: 缓存计算结果
const expensiveResult = useMemo(() => {
console.log('正在执行昂贵的计算...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i * num;
}
return result;
}, [num]); // 只有当 num 变化时才重新计算
return <div>计算结果: {expensiveResult}</div>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(1);
// useCallback: 缓存函数,避免子组件不必要的重新渲染
const handleClick = useCallback(() => {
console.log('点击处理');
setCount(c => c + 1);
}, []); // 依赖数组为空,函数不会重新创建
// 不好的写法:每次渲染都会创建新函数
// const badHandleClick = () => setCount(count + 1);
return (
<div>
<ExpensiveCalculation num={num} />
<p>计数: {count}</p>
<button onClick={handleClick}>增加计数</button>
<button onClick={() => setNum(num + 1)}>改变计算参数</button>
<ChildComponent onClick={handleClick} />
</div>
);
}
// React.memo 配合 useCallback 使用
const ChildComponent = React.memo(({ onClick }) => {
console.log('子组件渲染了');
return <button onClick={onClick}>子组件按钮</button>;
});
在线测试
三、自定义 Hooks - 逻辑复用
3.1 创建自定义 Hook
// useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 从 localStorage 读取初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 当值变化时保存到 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// 使用自定义 Hook
function App() {
const [name, setName] = useLocalStorage('username', '张三');
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="输入用户名"
/>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
<p>保存的用户名: {name}</p>
<p>当前主题: {theme}</p>
</div>
);
}
3.2 更多自定义 Hook 示例
// useFetch.js - 数据获取 Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
async function fetchData() {
try {
const response = await fetch(url, {
signal: abortController.signal
});
if (!response.ok) throw new Error('请求失败');
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
// useWindowSize.js - 窗口尺寸 Hook
function 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;
}
四、Hooks 使用规则
4.1 两个黄金规则
- 只在最顶层使用 Hooks
- 不要在循环、条件或嵌套函数中调用 Hooks
- 确保每次组件渲染时 Hooks 的调用顺序相同
// ✅ 正确:在顶层调用
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => { /* ... */ });
return <div>{count}</div>;
}
// ❌ 错误:在条件中调用
function BadComponent({ shouldUse }) {
if (shouldUse) {
const [count, setCount] = useState(0); // 错误!
}
// ...
}
// ❌ 错误:在循环中调用
function AnotherBadComponent() {
const items = [1, 2, 3];
items.forEach(item => {
const [value, setValue] = useState(item); // 错误!
});
// ...
}
- 只在 React 函数中调用 Hooks
- 在 React 函数组件中调用
- 在自定义 Hook 中调用
4.2 ESLint 插件
安装 ESLint 插件自动检查规则:
npm install eslint-plugin-react-hooks --save-dev
配置:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
五、Hooks 与 Class 组件对比
| 特性 | Class 组件 | 函数组件 + Hooks |
|---|---|---|
| 状态管理 | this.state, this.setState() |
useState, useReducer |
| 生命周期 | componentDidMount, componentDidUpdate 等 |
useEffect |
| Context | static contextType 或 Context.Consumer |
useContext |
| 性能优化 | shouldComponentUpdate, PureComponent |
useMemo, useCallback, React.memo |
| 逻辑复用 | 高阶组件、Render Props | 自定义 Hooks |
| 代码量 | 较多 | 较少 |
| this 绑定 | 需要处理 | 不需要 |
| 学习曲线 | 较陡峭 | 较平缓 |
六、实用技巧和最佳实践
6.1 处理依赖数组
function ProductList({ category, userId }) {
const [products, setProducts] = useState([]);
useEffect(() => {
fetchProducts(category, userId);
}, [category, userId]); // ✅ 正确:包含所有依赖
// ❌ 错误:缺少依赖
// useEffect(() => {
// fetchProducts(category, userId);
// }, [category]); // 缺少 userId
// ✅ 如果函数定义在 useEffect 内部,不需要添加依赖
useEffect(() => {
async function fetchProducts() {
const result = await fetch(`/api/products?category=${category}`);
setProducts(await result.json());
}
fetchProducts();
}, [category]);
return <div>{/* ... */}</div>;
}
6.2 使用 useReducer 管理复杂状态
const initialState = {
loading: false,
data: null,
error: null
};
function fetchReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { loading: false, data: action.payload, error: null };
case 'FETCH_ERROR':
return { loading: false, data: null, error: action.payload };
default:
return state;
}
}
function useAsync(fetchFn) {
const [state, dispatch] = useReducer(fetchReducer, initialState);
const execute = useCallback(async (...args) => {
dispatch({ type: 'FETCH_START' });
try {
const data = await fetchFn(...args);
dispatch({ type: 'FETCH_SUCCESS', payload: data });
return data;
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
throw error;
}
}, [fetchFn]);
return { ...state, execute };
}
6.3 避免无限循环
// ❌ 错误:导致无限循环
function BadExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 每次渲染都更新 state,导致无限循环
}); // 缺少依赖数组
return <div>{count}</div>;
}
// ✅ 正确:使用函数式更新
function GoodExample() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // 使用函数式更新
}, 1000);
return () => clearInterval(interval);
}, []); // 空依赖数组,只运行一次
return <div>{count}</div>;
}