🚀 React组件化实战:用TodoList项目搭乐高式开发!🎉

🧱 第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. 错误1:忘记设置key属性
    • 解决方案:每个列表项必须用唯一的key,比如key={todo.id}
  2. 错误2:直接修改状态
    • 解决方案:使用setTodos更新状态,而不是直接修改todos数组。
  3. 错误3:CSS类名冲突
    • 解决方案:使用CSS模块化(如TodoList.module.css)。

🥰 最终呈现效果

😍 闪亮登场

🧱 项目层级目录

🧰 开发者彩蛋合集

  1. 响应式挑战 :当屏幕缩小时,min-widthmax-width如何保护布局?

    • 答案:查看.containerpaddingborder-radius,它们会自动调整以适应不同屏幕尺寸。
  2. 悬停特效:如何让按钮在悬停时有闪光效果?

    • 提示:查看.button:hoverbox-shadow魔法!

🌟 总结:React组件化的核心思想

  1. 组件是乐高积木:每个组件独立、可复用,组合后构建复杂应用。
  2. 状态是化妆师useState管理数据,驱动UI自动更新。
  3. CSS是魔法学院:模块化样式让代码更安全、美观。
  4. 组件通信是接口 :通过props和回调函数实现父子组件协作。

终极建议:多动手实践,遇到问题不要怕!React的组件化思想会让你的代码像搭乐高一样简单有趣!🎉

相关推荐
J船长几秒前
APK战争 diffoscope
前端
鱼樱前端13 分钟前
重度Cursor用户 最强 Cursor Rules 和 Cursor 配置 mcp 以及最佳实践配置方式
前端
曼陀罗14 分钟前
Path<T> 、 keyof T 什么情况下用合适
前端
锈儿海老师20 分钟前
AST 工具大PK!Biome 的 GritQL 插件 vs. ast-grep,谁是你的菜?
前端·javascript·eslint
飞龙AI22 分钟前
鸿蒙Next实现瀑布流布局
前端
快起来别睡了23 分钟前
代理模式:送花风波
前端·javascript·架构
海底火旺25 分钟前
电影应用开发:从代码细节到用户体验优化
前端·css·html
陈随易34 分钟前
Gitea v1.24.0发布,自建github神器
前端·后端·程序员
前端付豪37 分钟前
汇丰银行技术架构揭秘:全球交易稳定背后的“微服务+容灾+零信任安全体系”
前端·后端·架构
邹荣乐39 分钟前
uni-app开发微信小程序的报错[渲染层错误]排查及解决
前端·微信小程序·uni-app