🧱 第1章:组件是什么?✨
什么是组件?
组件就像是乐高积木的最小单元!每一块乐高都有自己的形状和功能,拼在一起就能变成城堡、飞船甚至迪士尼乐园!而在React中,组件是HTML、CSS、JS的组合单元,负责封装业务逻辑,让你不再需要手动操作DOM(告别document.getElementById
的时代啦!🎉)。

代码示例:
jsx
// TodoForm.jsx
import { useState } from 'react'
function TodoForm(props) {
const { onAdd, categories } = props
const [text, setText] = useState('')
const [selectedCategory, setSelectedCategory] = useState('其他')
const [error, setError] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
if (!text.trim()) {
setError('请输入待办事项内容')
return
}
onAdd(text, selectedCategory)
setText('')
setSelectedCategory('其他')
setError('')
}
const handleChange = (e) => {
setText(e.target.value)
if (error) {
setError('')
}
}
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<input
type="text"
placeholder="请输入待办事项"
value={text}
onChange={handleChange}
className={error ? 'error' : ''}
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="category-select"
>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
{error && <div className="error-message">{error}</div>}
</div>
<button type="submit">添加</button>
</form>
)
}
export default TodoForm
✨ 小贴士 :TodoForm
组件就像一个乐高积木块,它负责接收用户输入并触发addTodo
回调函数(通过props传递给父组件)。是不是像在搭积木时把小块拼到大块上?🧩
🔧 第2章:TodoList实战拆解
TodoList组件的奥秘
TodoList组件是整个应用的"总指挥",它管理待办事项列表、状态(如编辑/删除),并通过子组件Todos
来渲染具体任务。让我们看看它是如何工作的!

代码示例:
jsx
// TodoList.jsx
// 内置的hook 函数
import { useState } from 'react'
import '../TodoList.css'
import TodoForm from './TodoForm'
import Todos from './Todos'
function TodoList() {
// 数据驱动的界面
// 静态页面
// DOM 数组 -> map -> join('') -> innerHTML 底层API 编程
// 缺点 低效、面向API
// 面向业务 懂业务
// 数据 -> 变化 -> 数据状态 -> 自动刷新页面 -> **数据** **驱动**页面
// 数组,第一项是数据变量,第二项函数 执行,并传入新的todos
// 页面会自动更新
// 挂载点 tbody
// { todos.map }
// setTodos DOM 及动态更新
// 响应式界面开发
// hi 数据状态 setHi 修改数据状态的方法
// ES6 解构
const [title] = useState('我的待办事项🎈')
const [todos, setTodos] = useState([
{
id: 1,
text: '学习 React',
completed: false,
category: '学习'
},
{
id: 2,
text: '完成项目',
completed: false,
category: '工作'
}
])
// 新增状态:搜索关键词和当前筛选的分类
const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState('全部')
// 预定义的分类选项
const categories = ['全部', '工作', '学习', '生活', '其他']
// 添加待办事项
const handleAdd = (text, category) => {
setTodos([
...todos,
{
id: Date.now(), // 使用时间戳作为唯一ID
text,
completed: false,
category: category || '其他'
}
])
}
// 切换待办事项完成状态
const handleToggle = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
))
}
// 删除待办事项
const handleDelete = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
// 编辑待办事项
const handleEdit = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
))
}
// 过滤显示的待办事项(搜索 + 分类筛选)
const filteredTodos = todos.filter(todo => {
const matchesSearch = todo.text.toLowerCase().includes(searchTerm.toLowerCase())
const matchesCategory = selectedCategory === '全部' || todo.category === selectedCategory
return matchesSearch && matchesCategory
})
return (
<div className="container">
<h1 className="title">{title}</h1>
{/* 表单组件 */}
<TodoForm onAdd={handleAdd} categories={categories.slice(1)} />
{/* 搜索和筛选区域 */}
<div className="filter-section">
<input
type="text"
placeholder="搜索待办事项..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="category-filter"
>
{categories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
{/* 待办事项列表组件 */}
<Todos
todos={filteredTodos}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
</div>
)
}
export default TodoList
💡 小思考 :为什么key={todo.id}
如此重要?如果去掉它会发生什么?(答案:React会警告你,因为key
帮助React识别列表项的变化!)
🧩 第3章:Todos组件特攻队
Todos组件的魔法操作
Todos
组件是待办事项的具体实现者,它负责显示任务内容、编辑模式切换、删除操作等。它的核心在于条件渲染和事件处理,就像一个灵活的"橡皮擦"------需要的时候出现,不需要的时候消失!✨

代码示例:
jsx
// Todos.jsx
import { useState } from 'react'
function Todos(props) {
// console.log(props, '/////')
const { todos, onToggle, onDelete, onEdit } = props
const [editingId, setEditingId] = useState(null)
const [editText, setEditText] = useState('')
// 开始编辑
const handleEditStart = (id, currentText) => {
setEditingId(id)
setEditText(currentText || '')
}
// 保存编辑
const handleEditSave = (id) => {
if (editText.trim()) {
onEdit(id, editText.trim())
}
setEditingId(null)
setEditText('')
}
// 取消编辑
const handleEditCancel = () => {
setEditingId(null)
setEditText('')
}
// 获取分类对应的颜色
const getCategoryColor = (category) => {
const colors = {
'工作': '#dc3545',
'学习': '#007bff',
'生活': '#28a745',
'其他': '#6c757d'
}
return colors[category] || '#6c757d'
}
return (
<div className="todos-area">
<ul>
{
todos.map(todo => (
<li key={todo.id}>
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{/* 分类标签 */}
<span
className="category-tag"
style={{ backgroundColor: getCategoryColor(todo.category) }}
>
{todo.category}
</span>
{/* 待办事项文本或编辑框 */}
{editingId === todo.id ? (
<>
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleEditSave(todo.id)
} else if (e.key === 'Escape') {
e.preventDefault()
handleEditCancel()
}
}}
style={{
background: 'white',
color: '#222',
border: '2px solid #368be6',
padding: '0.5rem 0.7rem',
fontSize: '1rem',
fontFamily: 'Arial, sans-serif',
borderRadius: '6px',
flex: 1,
boxSizing: 'border-box',
outline: 'none'
}}
autoFocus
/>
</>
) : (
<span>{todo.text}</span>
)}
</div>
<div className="todo-actions">
{editingId === todo.id ? (
<>
<button
className="save-btn"
onClick={() => handleEditSave(todo.id)}
>
保存
</button>
<button
className="cancel-btn"
onClick={handleEditCancel}
>
取消
</button>
</>
) : (
<>
<button
className="edit-btn"
onClick={() => handleEditStart(todo.id, todo.text)}
>
编辑
</button>
<button
className="complete-btn"
onClick={() => onToggle(todo.id)}
>
{todo.completed ? '取消完成' : '完成'}
</button>
<button
className="delete-btn"
onClick={() => onDelete(todo.id)}
>
删除
</button>
</>
)}
</div>
</li>
))
}
</ul>
{todos.length === 0 && (
<div className="empty-tip">暂无待办事项</div>
)}
</div>
)
}
export default Todos
🚀 开发者彩蛋 :在Todos
组件中,isEditing
是一个开关(布尔值),控制编辑模式的显示与隐藏。你可以尝试修改这个值,看看如何影响UI!
🎨 第4章:TodoList CSS炼金术
样式模块化:给组件化妆!💄
每个组件都有专属的CSS文件,比如TodoList.css
,它负责定义容器布局、颜色、动画等。通过CSS模块化,我们可以避免类名冲突,让样式更安全!

代码示例:
css
/* TodoList.css*/
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #eaf4fb;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background-color: white;
padding: 2.5rem 3rem;
border-radius: 18px;
box-shadow: 0 8px 32px rgba(74, 144, 226, 0.15);
width: 100%;
max-width: 480px;
min-width: 340px;
min-height: 520px;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
color: #368be6;
text-align: center;
margin-bottom: 2.5rem;
font-size: 2.3rem;
font-weight: bold;
letter-spacing: 2px;
}
/* 表单样式 */
form {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
width: 100%;
align-items: flex-start;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
}
input {
flex: 1;
padding: 1rem;
border: 2px solid #e0e0e0;
border-radius: 7px;
font-size: 1.1rem;
transition: border-color 0.3s, box-shadow 0.3s;
background: #f6fbff;
color: #222;
}
input::placeholder {
color: #b0b8c1;
opacity: 1;
}
input:focus {
outline: none;
border-color: #368be6;
box-shadow: 0 0 0 2px #b3d8fd;
}
button {
padding: 0.9rem 1.7rem;
background-color: #368be6;
color: white;
border: none;
border-radius: 7px;
cursor: pointer;
font-size: 1.1rem;
font-weight: bold;
transition: background-color 0.3s, box-shadow 0.3s;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.08);
}
button:hover {
background-color: #2566b3;
}
/* 列表样式 */
ul {
list-style: none;
width: 100%;
padding: 0;
}
li {
background-color: #f4faff;
padding: 1.5rem 2rem;
margin-bottom: 1.2rem;
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.3s, box-shadow 0.3s;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.06);
min-height: 70px;
}
li:hover {
background-color: #e3f0fa;
}
.todo-item {
display: flex;
align-items: center;
gap: 1rem;
font-size: 1.1rem;
font-weight: 500;
color: #222;
flex: 1;
min-height: 40px;
margin-right: 1.5rem;
min-width: 0;
max-width: calc(100% - 320px);
}
.todo-item.completed {
text-decoration: line-through;
color: #7bb6f9 !important;
opacity: 1;
font-weight: 500;
}
.todo-actions {
display: flex;
gap: 0.6rem;
flex-shrink: 0;
align-items: center;
width: 300px;
justify-content: flex-end;
}
.todo-actions button {
padding: 0.5rem 0.8rem;
font-size: 0.85rem;
border-radius: 6px;
font-weight: 500;
min-width: 56px;
white-space: nowrap;
height: 34px;
}
.delete-btn {
background-color: #f45b69;
}
.delete-btn:hover {
background-color: #c82333;
}
.complete-btn {
background-color: #3ec28f;
}
.complete-btn:hover {
background-color: #218838;
}
/* 错误提示样式 */
.error-message {
color: #f45b69;
font-size: 0.95rem;
margin-top: 0.3rem;
margin-bottom: 0.2rem;
}
input.error {
border-color: #f45b69;
background: #fff0f2;
}
/* 搜索和筛选区域 */
.filter-section {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
width: 100%;
}
.search-input {
flex: 1;
padding: 0.8rem;
border: 2px solid #e0e0e0;
border-radius: 7px;
font-size: 1rem;
background: #f6fbff;
color: #222;
}
.search-input::placeholder {
color: #b0b8c1;
opacity: 1;
}
.search-input:focus {
outline: none;
border-color: #368be6;
box-shadow: 0 0 0 2px #b3d8fd;
}
.category-filter, .category-select {
padding: 0.8rem;
border: 2px solid #e0e0e0;
border-radius: 7px;
font-size: 1rem;
background: #f6fbff;
color: #222;
cursor: pointer;
}
.category-filter:focus, .category-select:focus {
outline: none;
border-color: #368be6;
box-shadow: 0 0 0 2px #b3d8fd;
}
/* 分类标签样式 */
.category-tag {
color: white;
padding: 0.3rem 0.6rem;
border-radius: 14px;
font-size: 0.8rem;
font-weight: bold;
margin-right: 0.6rem;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
white-space: nowrap;
}
/* 复选框样式 */
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
flex-shrink: 0;
}
/* 待办事项文本样式 */
.todo-item span:last-child {
flex: 1;
line-height: 1.3;
word-break: break-word;
display: block;
min-width: 0;
overflow-wrap: break-word;
white-space: normal;
max-width: 100%;
}
/* 保存和取消按钮的特殊样式 */
.save-btn, .cancel-btn {
min-width: 64px;
flex-shrink: 0;
}
.edit-btn {
background-color: #ffc107;
}
.edit-btn:hover {
background-color: #e0a800;
}
.save-btn {
background-color: #28a745;
}
.save-btn:hover {
background-color: #218838;
}
.cancel-btn {
background-color: #6c757d;
}
.cancel-btn:hover {
background-color: #5a6268;
}
/* 空提示样式 */
.empty-tip {
text-align: center;
color: #999;
font-size: 1rem;
margin-top: 2rem;
}
✨ 色彩炼金术 :分类标签的颜色(如.category-tag
)通过动态类名实现。比如:
css
.category-tag.工作 {
background-color: #ef4444; /* 红色警报! */
}
.category-tag.学习 {
background-color: #3b82f6; /* 蓝色智慧! */
}
🧪 第5章:组件通信的秘密
Props与回调函数:父子组件的对话术
组件间通信的核心是props
和回调函数。父组件通过props
传递数据给子组件,子组件通过回调函数通知父组件状态变化。就像乐高积木的接口,精准连接!🔌

代码示例:
jsx
// 父组件 TodoList.jsx(第25行)
<Todos
key={todo.id}
todo={todo} // 传递数据
onDelete={deleteTodo} // 传递回调
onEdit={saveEdit}
onStartEdit={startEdit}
isEditing={editingId === todo.id}
/>
🎉 第6章:新手避坑指南
常见错误及解决方案
- 错误1:忘记设置
key
属性- 解决方案:每个列表项必须用唯一的
key
,比如key={todo.id}
。
- 解决方案:每个列表项必须用唯一的
- 错误2:直接修改状态
- 解决方案:使用
setTodos
更新状态,而不是直接修改todos
数组。
- 解决方案:使用
- 错误3:CSS类名冲突
- 解决方案:使用CSS模块化(如
TodoList.module.css
)。
- 解决方案:使用CSS模块化(如
🥰 最终呈现效果
😍 闪亮登场

🧱 项目层级目录

🧰 开发者彩蛋合集
-
响应式挑战 :当屏幕缩小时,
min-width
和max-width
如何保护布局?- 答案:查看
.container
的padding
和border-radius
,它们会自动调整以适应不同屏幕尺寸。
- 答案:查看
-
悬停特效:如何让按钮在悬停时有闪光效果?
- 提示:查看
.button:hover
的box-shadow
魔法!
- 提示:查看
🌟 总结:React组件化的核心思想
- 组件是乐高积木:每个组件独立、可复用,组合后构建复杂应用。
- 状态是化妆师 :
useState
管理数据,驱动UI自动更新。 - CSS是魔法学院:模块化样式让代码更安全、美观。
- 组件通信是接口 :通过
props
和回调函数实现父子组件协作。
✨ 终极建议:多动手实践,遇到问题不要怕!React的组件化思想会让你的代码像搭乐高一样简单有趣!🎉