React极简入门
本文面向具有 JavaScript 基础的开发者,快速介绍 React 的核心概念和开发方式。如果你已经熟悉 JavaScript 和基本的前端开发,这篇文章将帮助你快速上手 React。
# 一、React 简介
# 什么是 React
React 是由 Facebook(现 Meta)开发的用于构建用户界面的 JavaScript 库。它于 2013 年开源,现已成为最流行的前端框架之一。
React 与 JavaScript 的关系:
- JavaScript 是一门编程语言,提供基础语法和 API
- React 是基于 JavaScript 构建的库,专注于视图层的构建
- 如果你还不熟悉 JavaScript,建议先阅读 JavaScript极简入门
# 核心特性
- 组件化:UI 被拆分为独立、可复用的组件
- 声明式:描述 UI 应该是什么样子,React 负责更新
- 虚拟 DOM:高效的 DOM 更新机制
- 单向数据流:数据从父组件流向子组件
- JSX 语法:在 JavaScript 中编写类似 HTML 的标记
- 生态丰富:拥有完善的工具链和第三方库
# React vs Vue.js
两者都是优秀的前端框架,主要区别:
| 特性 | React | Vue.js |
|---|---|---|
| 学习曲线 | 需要学习 JSX、Hooks 等概念 | 模板语法更接近传统 HTML |
| 灵活性 | 更自由,选择更多 | 提供更多官方方案 |
| 数据绑定 | 单向数据流 | 双向数据绑定(v-model) |
| 生态 | 社区方案为主 | 官方方案为主 |
| 类型支持 | TypeScript 支持良好 | TypeScript 支持良好 |
如需了解 Vue.js,请参考 Vue.js快速入门。
# 二、环境准备
# 开发环境要求
- Node.js 16.0 或更高版本
- 现代浏览器(Chrome、Firefox、Edge 等)
- 代码编辑器(推荐 VS Code)
# 创建第一个 React 应用
使用 Vite 创建 React 项目(推荐方式):
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
或使用 Create React App:
npx create-react-app my-react-app
cd my-react-app
npm start
# 项目结构
my-react-app/
├── node_modules/
├── public/
│ └── index.html
├── src/
│ ├── App.jsx # 根组件
│ ├── App.css # 样式文件
│ ├── main.jsx # 入口文件
│ └── index.css
├── package.json
└── vite.config.js # Vite 配置
# 三、核心概念
# 1. 第一个组件:Hello World
在 React 中,一切都是组件。让我们从最简单的组件开始:
// 在 src/App.jsx 中
function App() {
return <h1>Hello World</h1>;
}
export default App;
这个组件做了什么?
function App()- 定义了一个名为 App 的函数组件return <h1>Hello World</h1>- 返回一个看起来像 HTML 的标记(这就是 JSX)export default App- 导出组件,让其他文件可以使用它
运行项目后,你会在浏览器中看到 "Hello World"。
# 2. JSX:在 JavaScript 中写 HTML
JSX 让你可以在 JavaScript 中编写类似 HTML 的代码。
# 基础示例
function Welcome() {
return (
<div>
<h1>欢迎来到 React</h1>
<p>这是一个简单的示例</p>
</div>
);
}
export default Welcome;
# 在 JSX 中使用 JavaScript
使用 {} 可以在 JSX 中嵌入 JavaScript 表达式:
function Greeting() {
const name = '张三';
const age = 25;
const hobbies = ['阅读', '编程', '运动'];
return (
<div>
{/* 使用变量 */}
<h1>你好,{name}!</h1>
{/* 使用表达式 */}
<p>明年你将 {age + 1} 岁</p>
{/* 使用方法 */}
<p>你的名字有 {name.length} 个字</p>
{/* 使用数组 */}
<p>爱好:{hobbies.join('、')}</p>
</div>
);
}
export default Greeting;
保存文件后,浏览器会显示:
你好,张三!
明年你将 26 岁
你的名字有 2 个字
爱好:阅读、编程、运动
# JSX 的重要规则
// ❌ 错误:必须有一个根元素
function Wrong() {
return (
<h1>标题</h1>
<p>段落</p> // 错误!不能并列两个元素
);
}
// ✅ 正确:用一个 div 包裹
function Correct1() {
return (
<div>
<h1>标题</h1>
<p>段落</p>
</div>
);
}
// ✅ 正确:使用 Fragment(不会产生额外的 DOM 节点)
function Correct2() {
return (
<>
<h1>标题</h1>
<p>段落</p>
</>
);
}
// ✅ 正确:标签必须闭合
function Tags() {
return (
<div>
<img src="avatar.jpg" /> {/* 自闭合 */}
<input type="text" /> {/* 自闭合 */}
<br /> {/* 自闭合 */}
</div>
);
}
// ⚠️ 注意:使用 className 而不是 class
function Styling() {
return (
<div className="container"> {/* ✅ className */}
<h1 className="title">标题</h1>
</div>
);
}
# 3. Props:组件间传递数据
Props(properties 的缩写)是父组件传递给子组件的数据,就像函数的参数。
# 完整示例:用户卡片
// UserCard.jsx - 子组件
function UserCard(props) {
return (
<div className="card">
<h2>{props.name}</h2>
<p>年龄:{props.age}</p>
<p>职业:{props.job}</p>
</div>
);
}
// App.jsx - 父组件
function App() {
return (
<div>
<h1>员工列表</h1>
<UserCard name="张三" age={28} job="前端工程师" />
<UserCard name="李四" age={32} job="后端工程师" />
<UserCard name="王五" age={25} job="设计师" />
</div>
);
}
export default App;
这个例子中:
UserCard是一个可复用的组件App组件使用了 3 次UserCard,每次传入不同的数据- Props 就像函数参数,让组件可以根据不同的数据显示不同的内容
# Props 解构(更简洁的写法)
// 不使用解构
function UserCard(props) {
return <h2>{props.name}</h2>;
}
// ✅ 使用解构(推荐)
function UserCard({ name, age, job }) {
return (
<div className="card">
<h2>{name}</h2>
<p>年龄:{age}</p>
<p>职业:{job}</p>
</div>
);
}
# Props 默认值
function Button({ text = '点击', color = 'blue' }) {
return (
<button style={{ backgroundColor: color }}>
{text}
</button>
);
}
// 使用
function App() {
return (
<div>
<Button /> {/* 显示:点击(蓝色) */}
<Button text="提交" /> {/* 显示:提交(蓝色) */}
<Button text="删除" color="red" /> {/* 显示:删除(红色) */}
</div>
);
}
# 特殊的 Props:children
children 是一个特殊的 prop,表示组件标签之间的内容:
// Card.jsx
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">
{children}
</div>
</div>
);
}
// App.jsx
function App() {
return (
<Card title="个人信息">
<p>姓名:张三</p>
<p>年龄:28</p>
<button>编辑</button>
</Card>
);
}
export default App;
浏览器显示:
个人信息
姓名:张三
年龄:28
[编辑按钮]
重点理解:
- Props 是只读的,子组件不能修改 props
- Props 可以传递任何类型的数据:字符串、数字、对象、数组、函数等
- Props 让组件变得可复用
# 4. State:组件的记忆
如果说 Props 是从外部传入的数据,那么 State 就是组件内部的数据,而且可以改变。
# 第一个有状态的组件:计数器
import { useState } from 'react';
function Counter() {
// useState 返回两个值:
// 1. count - 当前的状态值
// 2. setCount - 更新状态的函数
const [count, setCount] = useState(0); // 0 是初始值
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
);
}
export default Counter;
运行流程:
- 组件首次渲染,
count的值是 0 - 点击按钮,执行
setCount(count + 1),即setCount(1) - React 重新渲染组件,这次
count的值是 1 - 页面更新,显示"当前计数:1"
# 完整的计数器示例
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>计数器</h2>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
<button onClick={() => setCount(count - 1)}>
减少
</button>
<button onClick={() => setCount(0)}>
重置
</button>
<button onClick={() => setCount(count * 2)}>
翻倍
</button>
</div>
);
}
export default Counter;
# 不同类型的 State
import { useState } from 'react';
function StateExamples() {
// 字符串状态
const [name, setName] = useState('张三');
// 布尔状态
const [isVisible, setIsVisible] = useState(true);
// 对象状态
const [user, setUser] = useState({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
// 数组状态
const [items, setItems] = useState(['苹果', '香蕉', '橙子']);
return (
<div>
{/* 字符串状态 */}
<div>
<p>姓名:{name}</p>
<button onClick={() => setName('李四')}>
改名为李四
</button>
</div>
{/* 布尔状态 */}
<div>
<button onClick={() => setIsVisible(!isVisible)}>
{isVisible ? '隐藏' : '显示'}
</button>
{isVisible && <p>我是可见的内容</p>}
</div>
{/* 对象状态 */}
<div>
<p>{user.name} - {user.age}岁</p>
<button onClick={() => setUser({ ...user, age: user.age + 1 })}>
年龄+1
</button>
</div>
{/* 数组状态 */}
<div>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<button onClick={() => setItems([...items, '西瓜'])}>
添加西瓜
</button>
</div>
</div>
);
}
export default StateExamples;
# State 更新的重要规则
import { useState } from 'react';
function StateRules() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: '张三', age: 25 });
// ❌ 错误:直接修改 state
const wrongWay = () => {
count = count + 1; // 不会触发重新渲染!
};
// ✅ 正确:使用 setter 函数
const rightWay = () => {
setCount(count + 1);
};
// ❌ 错误:直接修改对象
const wrongObjectUpdate = () => {
user.age = 26; // 不会触发重新渲染!
setUser(user);
};
// ✅ 正确:创建新对象
const rightObjectUpdate = () => {
setUser({ ...user, age: 26 });
};
// ✅ 函数式更新(推荐)
const functionalUpdate = () => {
setCount(prevCount => prevCount + 1);
};
return <div>{/* ... */}</div>;
}
# 5. 事件处理:响应用户操作
React 中的事件处理与 HTML 类似,但有一些差异。
# 基础点击事件
import { useState } from 'react';
function ClickExample() {
const [message, setMessage] = useState('等待点击...');
const handleClick = () => {
setMessage('按钮被点击了!');
};
return (
<div>
<p>{message}</p>
<button onClick={handleClick}>点击我</button>
</div>
);
}
export default ClickExample;
# 表单输入处理
import { useState } from 'react';
function FormExample() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交行为
alert(`用户名:${username}\n密码:${password}`);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用户名:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
/>
</div>
<div>
<label>密码:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
/>
</div>
<button type="submit">登录</button>
<div>
<p>当前输入:{username}</p>
<p>密码长度:{password.length}</p>
</div>
</form>
);
}
export default FormExample;
运行流程:
- 用户在输入框输入 "张"
- 触发
onChange事件 - 执行
setUsername(e.target.value),即setUsername('张') - 组件重新渲染,输入框显示 "张"
- 下方实时显示"当前输入:张"
# 常见事件示例
import { useState } from 'react';
function EventExamples() {
const [logs, setLogs] = useState([]);
const addLog = (message) => {
setLogs([...logs, `${new Date().toLocaleTimeString()} - ${message}`]);
};
return (
<div>
<h2>事件演示</h2>
{/* 点击事件 */}
<button onClick={() => addLog('按钮被点击')}>
点击测试
</button>
{/* 双击事件 */}
<button onDoubleClick={() => addLog('双击了按钮')}>
双击测试
</button>
{/* 鼠标进入/离开 */}
<div
onMouseEnter={() => addLog('鼠标进入')}
onMouseLeave={() => addLog('鼠标离开')}
style={{ padding: '20px', background: '#f0f0f0', margin: '10px 0' }}
>
移动鼠标到这里
</div>
{/* 键盘事件 */}
<input
type="text"
onKeyDown={(e) => addLog(`按下了 ${e.key} 键`)}
placeholder="输入任意内容"
/>
{/* 焦点事件 */}
<input
type="text"
onFocus={() => addLog('输入框获得焦点')}
onBlur={() => addLog('输入框失去焦点')}
placeholder="点击获得焦点"
/>
{/* 事件日志 */}
<div style={{ marginTop: '20px', maxHeight: '200px', overflow: 'auto' }}>
<h3>事件日志:</h3>
<ul>
{logs.map((log, index) => (
<li key={index}>{log}</li>
))}
</ul>
</div>
</div>
);
}
export default EventExamples;
# 6. 条件渲染:根据条件显示不同内容
# 使用 if/else
import { useState } from 'react';
function LoginStatus() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
// 方式1:提前 return
if (isLoggedIn) {
return (
<div>
<h2>欢迎回来!</h2>
<button onClick={() => setIsLoggedIn(false)}>退出登录</button>
</div>
);
}
return (
<div>
<h2>请先登录</h2>
<button onClick={() => setIsLoggedIn(true)}>登录</button>
</div>
);
}
export default LoginStatus;
# 使用三元运算符
import { useState } from 'react';
function LoginButton() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
<h2>
{isLoggedIn ? '欢迎回来!' : '请先登录'}
</h2>
<button onClick={() => setIsLoggedIn(!isLoggedIn)}>
{isLoggedIn ? '退出登录' : '登录'}
</button>
</div>
);
}
export default LoginButton;
# 使用 && 运算符
import { useState } from 'react';
function Notifications() {
const [unreadCount, setUnreadCount] = useState(5);
return (
<div>
<h2>通知中心</h2>
{/* 只有当 unreadCount > 0 时才显示 */}
{unreadCount > 0 && (
<div style={{ color: 'red' }}>
您有 {unreadCount} 条未读消息
</div>
)}
{unreadCount === 0 && (
<div style={{ color: 'green' }}>
没有新消息
</div>
)}
<button onClick={() => setUnreadCount(unreadCount + 1)}>
新消息 +1
</button>
<button onClick={() => setUnreadCount(0)}>
全部已读
</button>
</div>
);
}
export default Notifications;
# 完整示例:用户权限
import { useState } from 'react';
function UserDashboard() {
const [user, setUser] = useState(null);
const loginAsUser = () => {
setUser({ name: '张三', role: 'user' });
};
const loginAsAdmin = () => {
setUser({ name: '管理员', role: 'admin' });
};
const logout = () => {
setUser(null);
};
// 未登录
if (!user) {
return (
<div>
<h2>请选择登录方式</h2>
<button onClick={loginAsUser}>普通用户登录</button>
<button onClick={loginAsAdmin}>管理员登录</button>
</div>
);
}
// 已登录
return (
<div>
<h2>欢迎,{user.name}</h2>
{/* 所有用户都能看到 */}
<div>
<h3>个人中心</h3>
<p>查看个人信息</p>
</div>
{/* 只有管理员能看到 */}
{user.role === 'admin' && (
<div style={{ background: '#ffe', padding: '10px' }}>
<h3>管理员功能</h3>
<p>用户管理</p>
<p>系统设置</p>
</div>
)}
{/* 只有普通用户能看到 */}
{user.role === 'user' && (
<div>
<p>普通用户权限有限</p>
</div>
)}
<button onClick={logout}>退出登录</button>
</div>
);
}
export default UserDashboard;
# 7. 列表渲染:显示多个相似的元素
# 基础列表渲染
function FruitList() {
const fruits = ['苹果', '香蕉', '橙子', '西瓜'];
return (
<div>
<h2>水果列表</h2>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
</div>
);
}
export default FruitList;
浏览器显示:
水果列表
• 苹果
• 香蕉
• 橙子
• 西瓜
# 对象数组渲染
function UserList() {
const users = [
{ id: 1, name: '张三', age: 28, job: '工程师' },
{ id: 2, name: '李四', age: 32, job: '设计师' },
{ id: 3, name: '王五', age: 25, job: '产品经理' }
];
return (
<div>
<h2>员工列表</h2>
<table border="1">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>职位</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.job}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default UserList;
# 可交互的列表
import { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React', completed: false },
{ id: 2, text: '写代码', completed: true },
{ id: 3, text: '看文档', completed: false }
]);
// 切换完成状态
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));
};
return (
<div>
<h2>待办事项</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map((todo) => (
<li
key={todo.id}
style={{
padding: '10px',
marginBottom: '5px',
background: '#f5f5f5',
display: 'flex',
alignItems: 'center',
gap: '10px'
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : '#000'
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
删除
</button>
</li>
))}
</ul>
</div>
);
}
export default TodoList;
运行流程:
- 初始显示 3 个任务,其中"写代码"已完成(有删除线)
- 点击"学习 React"的复选框:
- 触发
toggleTodo(1) - 更新 todos 数组,将 id 为 1 的项的 completed 改为 true
- 组件重新渲染,"学习 React"出现删除线
- 触发
- 点击"删除"按钮:
- 触发
deleteTodo(id) - 从 todos 数组中过滤掉该项
- 组件重新渲染,该任务从列表中消失
- 触发
# 为什么需要 key?
import { useState } from 'react';
function KeyImportance() {
const [items, setItems] = useState(['A', 'B', 'C']);
const shuffle = () => {
const shuffled = [...items].sort(() => Math.random() - 0.5);
setItems(shuffled);
};
return (
<div>
<button onClick={shuffle}>随机排序</button>
{/* ❌ 不好:使用索引作为 key */}
<div>
<h3>使用索引作为 key(不推荐)</h3>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<input type="text" placeholder={`${item} 的输入框`} />
</li>
))}
</ul>
</div>
{/* ✅ 好:使用唯一标识作为 key */}
<div>
<h3>使用内容作为 key(推荐)</h3>
<ul>
{items.map((item) => (
<li key={item}>
{item}
<input type="text" placeholder={`${item} 的输入框`} />
</li>
))}
</ul>
</div>
</div>
);
}
export default KeyImportance;
实验:
- 在两个列表的输入框中输入内容
- 点击"随机排序"按钮
- 观察:使用索引作为 key 的列表,输入框的内容会错位;使用内容作为 key 的列表,输入框会跟着正确的项移动
key 的规则:
- key 必须在兄弟元素中唯一
- key 应该稳定、可预测,不能随机生成
- 通常使用数据的 id 作为 key
- 只有在列表顺序永远不变时,才可以用索引作为 key
# 四、Hooks(钩子)
Hooks 是 React 16.8 引入的特性,让函数组件拥有类组件的能力。你可以把 Hooks 理解为给函数组件"增加超能力"的工具。
# 什么是 Hooks?
在没有 Hooks 之前,如果想在组件中使用状态或生命周期,必须使用 class 组件。Hooks 让函数组件也能做到这些事。
Hooks 的命名规则:
- 所有 Hooks 都以
use开头 - 必须在组件顶层调用,不能在循环、条件或嵌套函数中调用
# useState - 状态管理
我们在前面已经见过 useState,现在深入了解它。
# 基础用法回顾
import { useState } from 'react';
function Counter() {
// useState 返回一个数组:[状态值, 更新函数]
const [count, setCount] = useState(0); // 0 是初始值
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
export default Counter;
# 多个状态
一个组件可以使用多个 useState:
import { useState } from 'react';
function UserProfile() {
const [name, setName] = useState('张三');
const [age, setAge] = useState(25);
const [email, setEmail] = useState('zhangsan@example.com');
const [isEditing, setIsEditing] = useState(false);
const handleSave = () => {
setIsEditing(false);
alert('保存成功!');
};
if (isEditing) {
return (
<div>
<h2>编辑个人信息</h2>
<div>
<label>姓名:</label>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div>
<label>年龄:</label>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
/>
</div>
<div>
<label>邮箱:</label>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<button onClick={handleSave}>保存</button>
<button onClick={() => setIsEditing(false)}>取消</button>
</div>
);
}
return (
<div>
<h2>个人信息</h2>
<p>姓名:{name}</p>
<p>年龄:{age}</p>
<p>邮箱:{email}</p>
<button onClick={() => setIsEditing(true)}>编辑</button>
</div>
);
}
export default UserProfile;
运行流程:
- 初始显示个人信息和"编辑"按钮
- 点击"编辑",
setIsEditing(true),切换到编辑模式 - 修改输入框内容,实时更新对应的状态
- 点击"保存",显示提示并切换回查看模式
# 对象状态的正确更新
import { useState } from 'react';
function UserForm() {
const [user, setUser] = useState({
name: '张三',
age: 25,
email: 'zhangsan@example.com',
address: {
city: '北京',
district: '朝阳区'
}
});
// ❌ 错误:直接修改对象
const wrongUpdate = () => {
user.age = 26; // 不会触发重新渲染!
setUser(user);
};
// ✅ 正确:创建新对象(浅拷贝)
const updateAge = () => {
setUser({ ...user, age: user.age + 1 });
};
// ✅ 更新嵌套对象
const updateCity = (newCity) => {
setUser({
...user,
address: {
...user.address,
city: newCity
}
});
};
// ✅ 更新单个字段(通用方法)
const handleChange = (field, value) => {
setUser({ ...user, [field]: value });
};
return (
<div>
<h2>用户信息</h2>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<p>邮箱:{user.email}</p>
<p>地址:{user.address.city} {user.address.district}</p>
<div style={{ marginTop: '20px' }}>
<button onClick={updateAge}>年龄 +1</button>
<button onClick={() => updateCity('上海')}>迁移到上海</button>
<button onClick={() => handleChange('name', '李四')}>
改名为李四
</button>
</div>
</div>
);
}
export default UserForm;
# 数组状态的更新
import { useState } from 'react';
function ShoppingList() {
const [items, setItems] = useState(['苹果', '香蕉']);
const [inputValue, setInputValue] = useState('');
// 添加项
const addItem = () => {
if (inputValue.trim()) {
setItems([...items, inputValue]); // 创建新数组
setInputValue('');
}
};
// 删除项
const removeItem = (index) => {
setItems(items.filter((_, i) => i !== index));
};
// 更新项
const updateItem = (index, newValue) => {
const newItems = [...items];
newItems[index] = newValue;
setItems(newItems);
};
// 清空列表
const clearAll = () => {
setItems([]);
};
return (
<div>
<h2>购物清单</h2>
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addItem()}
placeholder="添加商品"
/>
<button onClick={addItem}>添加</button>
</div>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<button onClick={() => removeItem(index)}>删除</button>
</li>
))}
</ul>
<p>共 {items.length} 件商品</p>
<button onClick={clearAll}>清空</button>
</div>
);
}
export default ShoppingList;
# 函数式更新
当新状态依赖于旧状态时,使用函数式更新更安全:
import { useState } from 'react';
function UpdateComparison() {
const [count, setCount] = useState(0);
// 场景:连续多次更新
const handleMultipleUpdates = () => {
// ❌ 问题:可能不会按预期工作
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 结果:count 只会 +1(不是 +3)
};
// ✅ 正确:使用函数式更新
const handleMultipleUpdatesCorrect = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 结果:count 会 +3
};
return (
<div>
<p>计数:{count}</p>
<button onClick={handleMultipleUpdates}>错误的多次更新</button>
<button onClick={handleMultipleUpdatesCorrect}>正确的多次更新</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}
export default UpdateComparison;
# useEffect - 副作用处理
useEffect 用于处理副作用,比如数据获取、订阅、手动修改 DOM 等。
# 什么是副作用?
在 React 中,副作用是指那些影响组件外部的操作:
- 发送网络请求
- 修改 DOM
- 设置定时器
- 订阅事件
- 读写 localStorage
# 基础用法
import { useState, useEffect } from 'react';
function DocumentTitle() {
const [count, setCount] = useState(0);
// 每次组件渲染后执行
useEffect(() => {
// 副作用:修改页面标题
document.title = `你点击了 ${count} 次`;
});
return (
<div>
<p>点击次数:{count}</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
export default DocumentTitle;
运行流程:
- 组件首次渲染,count = 0
- 渲染完成后,执行 useEffect,页面标题变为"你点击了 0 次"
- 点击按钮,count = 1
- 组件重新渲染
- 渲染完成后,再次执行 useEffect,标题变为"你点击了 1 次"
# 依赖数组
import { useState, useEffect } from 'react';
function EffectDependencies() {
const [count, setCount] = useState(0);
const [name, setName] = useState('张三');
// 情况1:没有依赖数组 - 每次渲染都执行
useEffect(() => {
console.log('每次渲染都执行');
});
// 情况2:空依赖数组 - 只在挂载时执行一次
useEffect(() => {
console.log('组件挂载了');
}, []);
// 情况3:有依赖项 - 依赖项变化时执行
useEffect(() => {
console.log('count 变化了,新值:', count);
}, [count]); // 只有 count 变化时才执行
useEffect(() => {
console.log('name 变化了,新值:', name);
}, [name]); // 只有 name 变化时才执行
return (
<div>
<p>计数:{count}</p>
<p>姓名:{name}</p>
<button onClick={() => setCount(count + 1)}>增加计数</button>
<button onClick={() => setName('李四')}>改名</button>
</div>
);
}
export default EffectDependencies;
实验:
- 组件加载时,控制台输出:
- "每次渲染都执行"
- "组件挂载了"
- "count 变化了,新值:0"
- "name 变化了,新值:张三"
- 点击"增加计数":
- "每次渲染都执行"
- "count 变化了,新值:1"
- 点击"改名":
- "每次渲染都执行"
- "name 变化了,新值:李四"
# 清理函数
有些副作用需要清理,比如定时器、事件监听等。
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return;
console.log('启动定时器');
// 设置定时器
const timer = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 清理函数:组件卸载或依赖项变化时执行
return () => {
console.log('清理定时器');
clearInterval(timer);
};
}, [isRunning]); // isRunning 变化时,会先清理旧定时器,再创建新的
return (
<div>
<h2>计时器</h2>
<p>已运行:{seconds} 秒</p>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? '暂停' : '开始'}
</button>
<button onClick={() => setSeconds(0)}>重置</button>
</div>
);
}
export default Timer;
运行流程:
- 点击"开始",
isRunning变为 true - useEffect 执行,创建定时器
- 每秒更新 seconds
- 点击"暂停",
isRunning变为 false - useEffect 的清理函数执行,清除定时器
# 数据获取示例
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 标记是否已取消请求
let cancelled = false;
// 获取数据
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
// 如果组件已卸载,不更新状态
if (!cancelled) {
setUsers(data);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
}
};
fetchUsers();
// 清理函数:组件卸载时标记为已取消
return () => {
cancelled = true;
};
}, []); // 空数组表示只在挂载时获取一次
if (loading) {
return <div>加载中...</div>;
}
if (error) {
return <div>错误:{error}</div>;
}
return (
<div>
<h2>用户列表</h2>
<ul>
{users.slice(0, 5).map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default UserList;
# 监听窗口大小
import { useState, useEffect } from 'react';
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
// 事件处理函数
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 添加事件监听
window.addEventListener('resize', handleResize);
console.log('添加了 resize 监听');
// 清理函数:移除事件监听
return () => {
window.removeEventListener('resize', handleResize);
console.log('移除了 resize 监听');
};
}, []); // 空数组:只在挂载时添加,卸载时移除
return (
<div>
<h2>窗口尺寸</h2>
<p>宽度:{windowSize.width}px</p>
<p>高度:{windowSize.height}px</p>
<p style={{ color: '#666', fontSize: '14px' }}>
调整浏览器窗口大小试试
</p>
</div>
);
}
export default WindowSize;
# useRef - 引用 DOM 或保持值
useRef 有两个主要用途:
- 访问 DOM 元素
- 保存一个在组件生命周期中不变的值(修改它不会触发重新渲染)
# 访问 DOM 元素
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后自动聚焦到输入框
inputRef.current.focus();
}, []);
const handleFocus = () => {
inputRef.current.focus();
};
const handleClear = () => {
inputRef.current.value = '';
inputRef.current.focus();
};
return (
<div>
<h2>输入框操作</h2>
<input ref={inputRef} type="text" placeholder="自动聚焦" />
<div>
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空并聚焦</button>
</div>
</div>
);
}
export default AutoFocusInput;
工作原理:
const inputRef = useRef(null)创建一个 ref 对象<input ref={inputRef} />将 DOM 元素赋值给inputRef.currentinputRef.current.focus()直接操作 DOM 元素
# 视频播放器示例
import { useRef, useState } from 'react';
function VideoPlayer() {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = () => {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
};
const handleRestart = () => {
videoRef.current.currentTime = 0;
videoRef.current.play();
setIsPlaying(true);
};
return (
<div>
<h2>视频播放器</h2>
<video
ref={videoRef}
width="400"
src="https://www.w3schools.com/html/mov_bbb.mp4"
/>
<div>
<button onClick={togglePlay}>
{isPlaying ? '暂停' : '播放'}
</button>
<button onClick={handleRestart}>重新播放</button>
</div>
</div>
);
}
export default VideoPlayer;
# 保持值(不触发重新渲染)
import { useState, useRef } from 'react';
function ClickCounter() {
const [renderCount, setRenderCount] = useState(0);
const clickCountRef = useRef(0);
const handleClick = () => {
clickCountRef.current += 1;
console.log('总点击次数:', clickCountRef.current);
// 注意:修改 ref 不会触发重新渲染
};
const handleRender = () => {
setRenderCount(renderCount + 1);
// 这会触发重新渲染
};
return (
<div>
<h2>点击计数器</h2>
<p>组件渲染次数:{renderCount}</p>
<p>总点击次数:{clickCountRef.current}</p>
<button onClick={handleClick}>
点击(不重新渲染,查看控制台)
</button>
<button onClick={handleRender}>触发渲染</button>
</div>
);
}
export default ClickCounter;
# 对比 useState 和 useRef
import { useState, useRef } from 'react';
function StateVsRef() {
const [stateValue, setStateValue] = useState(0);
const refValue = useRef(0);
console.log('组件渲染了');
return (
<div>
<h2>State vs Ref</h2>
<div>
<h3>useState</h3>
<p>值:{stateValue}</p>
<button onClick={() => setStateValue(stateValue + 1)}>
增加(会重新渲染)
</button>
</div>
<div>
<h3>useRef</h3>
<p>值:{refValue.current}</p>
<button onClick={() => {
refValue.current += 1;
console.log('Ref 值:', refValue.current);
alert(`Ref 值已更新为 ${refValue.current},但界面不会自动更新`);
}}>
增加(不会重新渲染)
</button>
</div>
<button onClick={() => setStateValue(s => s + 0)}>
强制重新渲染
</button>
</div>
);
}
export default StateVsRef;
# useContext - 跨组件传递数据
useContext 用于在组件树中传递数据,避免层层传递 props(称为"props drilling")。
# 问题:层层传递 Props
// ❌ 繁琐的方式:层层传递
function App() {
const [theme, setTheme] = useState('light');
return <Parent theme={theme} />;
}
function Parent({ theme }) {
return <Child theme={theme} />;
}
function Child({ theme }) {
return <GrandChild theme={theme} />;
}
function GrandChild({ theme }) {
return <div>当前主题:{theme}</div>;
}
# 解决方案:使用 Context
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context
const ThemeContext = createContext('light');
// 2. Provider 组件
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<div>
<h1>主题切换示例</h1>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
<Parent />
</div>
</ThemeContext.Provider>
);
}
// 3. 中间组件不需要知道 theme
function Parent() {
return (
<div>
<h2>Parent 组件</h2>
<Child />
</div>
);
}
function Child() {
return (
<div>
<h3>Child 组件</h3>
<GrandChild />
</div>
);
}
// 4. 最深层组件直接使用 Context
function GrandChild() {
const theme = useContext(ThemeContext); // 获取主题
return (
<div
style={{
padding: '20px',
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}
>
当前主题:{theme}
</div>
);
}
export default App;
# 完整示例:用户信息共享
import { createContext, useContext, useState } from 'react';
// 创建 Context
const UserContext = createContext(null);
// 根组件
function App() {
const [user, setUser] = useState(null);
const login = (username) => {
setUser({ name: username, role: 'user' });
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
<div>
<Header />
<Main />
<Footer />
</div>
</UserContext.Provider>
);
}
// 头部组件
function Header() {
const { user, logout } = useContext(UserContext);
return (
<header style={{ background: '#f0f0f0', padding: '10px' }}>
<h1>我的网站</h1>
{user ? (
<div>
<span>欢迎,{user.name}</span>
<button onClick={logout}>退出</button>
</div>
) : (
<span>未登录</span>
)}
</header>
);
}
// 主要内容
function Main() {
const { user, login } = useContext(UserContext);
const [inputValue, setInputValue] = useState('');
if (!user) {
return (
<main style={{ padding: '20px' }}>
<h2>请先登录</h2>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入用户名"
/>
<button onClick={() => login(inputValue)}>登录</button>
</main>
);
}
return (
<main style={{ padding: '20px' }}>
<h2>主要内容</h2>
<p>你好,{user.name}!</p>
<UserProfile />
</main>
);
}
// 用户资料组件
function UserProfile() {
const { user } = useContext(UserContext);
return (
<div style={{ border: '1px solid #ddd', padding: '10px' }}>
<h3>个人资料</h3>
<p>用户名:{user.name}</p>
<p>角色:{user.role}</p>
</div>
);
}
// 底部组件
function Footer() {
return (
<footer style={{ background: '#f0f0f0', padding: '10px', marginTop: '20px' }}>
<p>© 2024 我的网站</p>
</footer>
);
}
export default App;
运行流程:
- 初始状态:Header 显示"未登录",Main 显示登录表单
- 输入用户名"张三",点击"登录"
- 调用
login('张三'),更新 user 状态 - Header 显示"欢迎,张三"和"退出"按钮
- Main 显示主要内容和用户资料
- Footer 始终显示,但它不需要知道用户信息
# useMemo - 缓存计算结果
useMemo 用于缓存计算结果,避免每次渲染都重新计算。
# 基础示例
import { useState, useMemo } from 'react';
function ExpensiveCalculation() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 模拟耗时计算
const expensiveValue = useMemo(() => {
console.log('执行耗时计算...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result + count;
}, [count]); // 只有 count 变化时才重新计算
return (
<div>
<h2>useMemo 示例</h2>
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
<div>
<p>计算结果:{expensiveValue}</p>
</div>
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入文本(不会触发重新计算)"
/>
</div>
</div>
);
}
export default ExpensiveCalculation;
观察:
- 点击"增加"按钮:控制台输出"执行耗时计算..."(需要重新计算)
- 在输入框输入文本:没有控制台输出(使用缓存的结果)
# 实用示例:过滤列表
import { useState, useMemo } from 'react';
function FilteredList() {
const [searchTerm, setSearchTerm] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
const allUsers = [
{ id: 1, name: '张三', age: 28 },
{ id: 2, name: '李四', age: 32 },
{ id: 3, name: '王五', age: 25 },
{ id: 4, name: '赵六', age: 30 },
{ id: 5, name: '张小明', age: 22 }
];
// 缓存过滤和排序结果
const filteredAndSortedUsers = useMemo(() => {
console.log('重新过滤和排序...');
// 过滤
const filtered = allUsers.filter(user =>
user.name.includes(searchTerm)
);
// 排序
const sorted = [...filtered].sort((a, b) =>
sortOrder === 'asc' ? a.age - b.age : b.age - a.age
);
return sorted;
}, [searchTerm, sortOrder]); // 只有搜索词或排序方式变化时才重新计算
return (
<div>
<h2>用户列表</h2>
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索姓名"
/>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
按年龄排序({sortOrder === 'asc' ? '升序' : '降序'})
</button>
</div>
<ul>
{filteredAndSortedUsers.map(user => (
<li key={user.id}>
{user.name} - {user.age}岁
</li>
))}
</ul>
<p>找到 {filteredAndSortedUsers.length} 个结果</p>
</div>
);
}
export default FilteredList;
# useCallback - 缓存函数
useCallback 用于缓存函数,避免每次渲染都创建新函数。
# 基础示例
import { useState, useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 不使用 useCallback - 每次渲染都创建新函数
const handleClickNormal = () => {
console.log('点击了', count);
};
// 使用 useCallback - 只有依赖项变化时才创建新函数
const handleClickMemo = useCallback(() => {
console.log('点击了', count);
}, [count]);
return (
<div>
<h2>useCallback 示例</h2>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入文本"
/>
<Child onClick={handleClickMemo} />
</div>
);
}
function Child({ onClick }) {
console.log('Child 渲染了');
return <button onClick={onClick}>子组件按钮</button>;
}
export default Parent;
# 实用示例:搜索功能
import { useState, useCallback, memo } from 'react';
function SearchApp() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// 模拟搜索 API
const performSearch = useCallback((searchQuery) => {
console.log('执行搜索:', searchQuery);
// 模拟异步搜索
setTimeout(() => {
const mockResults = [
'React 教程',
'React Hooks',
'React Router',
'React Native'
].filter(item => item.toLowerCase().includes(searchQuery.toLowerCase()));
setResults(mockResults);
}, 300);
}, []); // 空依赖:函数永远不变
return (
<div>
<h2>搜索功能</h2>
<SearchInput onSearch={performSearch} />
<SearchResults results={results} />
</div>
);
}
// 使用 memo 优化子组件
const SearchInput = memo(({ onSearch }) => {
const [value, setValue] = useState('');
console.log('SearchInput 渲染了');
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
onSearch(newValue);
};
return (
<input
value={value}
onChange={handleChange}
placeholder="输入搜索词"
/>
);
});
const SearchResults = memo(({ results }) => {
console.log('SearchResults 渲染了');
return (
<ul>
{results.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
);
});
export default SearchApp;
# 自定义 Hooks
自定义 Hooks 让你可以提取组件逻辑,实现代码复用。
# 自定义 Hook:useLocalStorage
import { useState, useEffect } from 'react';
// 自定义 Hook:持久化状态到 localStorage
function useLocalStorage(key, initialValue) {
// 从 localStorage 读取初始值
const [value, setValue] = 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(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
// 使用自定义 Hook
function App() {
const [name, setName] = useLocalStorage('name', '');
const [age, setAge] = useLocalStorage('age', 0);
return (
<div>
<h2>个人信息(会保存到浏览器)</h2>
<div>
<label>姓名:</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label>年龄:</label>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
/>
</div>
<p>刷新页面,数据仍然存在!</p>
</div>
);
}
export default App;
# 自定义 Hook:useToggle
import { useState } from 'react';
// 自定义 Hook:切换布尔值
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
// 使用自定义 Hook
function App() {
const [isVisible, { toggle, setTrue, setFalse }] = useToggle(false);
const [isEnabled, toggleEnabled] = useToggle(true);
return (
<div>
<h2>useToggle 示例</h2>
<div>
<button onClick={toggle}>切换显示</button>
<button onClick={setTrue}>显示</button>
<button onClick={setFalse}>隐藏</button>
{isVisible && <p>我是可见的内容</p>}
</div>
<div>
<button onClick={toggleEnabled.toggle}>
{isEnabled ? '禁用' : '启用'}功能
</button>
<p>功能状态:{isEnabled ? '启用' : '禁用'}</p>
</div>
</div>
);
}
export default App;
# 自定义 Hook:useFetch
import { useState, useEffect } from 'react';
// 自定义 Hook:数据获取
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// 使用自定义 Hook
function UserList() {
const { data, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/users'
);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误:{error}</div>;
return (
<div>
<h2>用户列表</h2>
<ul>
{data?.slice(0, 5).map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default UserList;
# Hooks 使用规则
- 只在顶层调用:不要在循环、条件或嵌套函数中调用
// ❌ 错误
function Bad() {
if (condition) {
const [state, setState] = useState(0); // 不能在条件中
}
for (let i = 0; i < 10; i++) {
useEffect(() => {}); // 不能在循环中
}
}
// ✅ 正确
function Good() {
const [state, setState] = useState(0);
useEffect(() => {
if (condition) {
// 条件逻辑放在 Hook 内部
}
});
}
- 只在 React 函数中调用:只在函数组件或自定义 Hooks 中调用
// ❌ 错误
function normalFunction() {
const [state, setState] = useState(0); // 不能在普通函数中
}
// ✅ 正确
function MyComponent() {
const [state, setState] = useState(0); // 可以在组件中
}
function useCustomHook() {
const [state, setState] = useState(0); // 可以在自定义 Hook 中
}
# 五、实战示例:Todo 应用
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
}
};
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 remaining = todos.filter(t => !t.completed).length;
return (
<div className="todo-app">
<h1>待办事项</h1>
<div className="input-group">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="添加新任务..."
/>
<button onClick={addTodo}>添加</button>
</div>
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
<div className="footer">
还有 {remaining} 个任务未完成
</div>
</div>
);
}
export default TodoApp;
配套的 CSS:
.todo-app {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
.input-group button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.todo-list button {
margin-left: auto;
padding: 5px 10px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.footer {
margin-top: 20px;
padding: 10px;
background: #f8f9fa;
text-align: center;
border-radius: 4px;
}
# 六、最佳实践
# 1. 组件设计原则
- 单一职责:每个组件只做一件事
- 可复用性:设计通用的组件
- Props 明确:清晰的接口定义
- 保持简单:避免过度抽象
// ✅ 好的组件设计
function UserCard({ user, onEdit }) {
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
<button onClick={() => onEdit(user)}>编辑</button>
</div>
);
}
// ❌ 不好的设计(职责太多)
function UserCardWithEditModal({ user }) {
const [showModal, setShowModal] = useState(false);
// 混合了展示和编辑逻辑
}
# 2. 状态管理
- 状态提升:共享状态放到公共父组件
- 本地状态优先:不是所有状态都需要全局管理
- 状态最小化:避免冗余状态
// 状态提升示例
function Parent() {
const [selectedId, setSelectedId] = useState(null);
return (
<>
<List items={items} onSelect={setSelectedId} />
<Detail id={selectedId} />
</>
);
}
# 3. 性能优化
- 避免不必要的渲染:使用
React.memo - 使用 key:列表渲染时提供稳定的 key
- 懒加载:按需加载组件
- 虚拟化长列表:使用 react-window
import { memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
// 只有 data 变化时才重新渲染
return <div>{/* 复杂渲染逻辑 */}</div>;
});
# 4. 代码组织
src/
├── components/ # 通用组件
│ ├── Button/
│ │ ├── Button.jsx
│ │ └── Button.css
│ └── Card/
├── features/ # 功能模块
│ ├── auth/
│ └── todos/
├── hooks/ # 自定义 Hooks
│ └── useLocalStorage.js
├── utils/ # 工具函数
└── App.jsx
# 七、常用工具和库
# 路由
npm install react-router-dom
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
# 状态管理
常用的状态管理库:
- Redux Toolkit:复杂应用的状态管理
- Zustand:轻量级状态管理
- Jotai / Recoil:原子化状态管理
# UI 组件库
- Ant Design:企业级 UI 组件库
- Material-UI:Google Material Design
- Chakra UI:现代化组件库
- shadcn/ui:基于 Radix UI 的组件集合
# 开发工具
- React DevTools:浏览器扩展,调试 React 应用
- ESLint:代码质量检查
- Prettier:代码格式化
# 八、学习资源
# 官方文档
# 进阶主题
- TypeScript 集成
- 服务端渲染(Next.js)
- 性能优化技巧
- 测试(Jest、React Testing Library)
- 状态管理模式
# 实践项目
建议从以下项目开始练习:
- Todo 应用(本文示例)
- 计算器
- 天气查询应用
- 博客系统
- 电商购物车
# 九、总结
React 的核心概念:
- 组件:构建 UI 的基本单元
- JSX:在 JavaScript 中编写标记
- Props:组件间传递数据
- State:组件内部状态
- Hooks:函数组件的能力扩展
开始使用 React 的步骤:
- 创建项目:
npm create vite@latest my-app -- --template react - 学习 JSX 和组件
- 掌握 useState 和 useEffect
- 实践小项目
- 深入学习其他 Hooks 和高级特性
React 强大的生态系统和活跃的社区使它成为构建现代 Web 应用的绝佳选择。持续实践和探索,你会发现 React 的更多可能性!
祝你变得更强!