前言
最近学习了React Hooks 的实际应用,想分享一个项目,这不仅仅是一个简单的 Todo 应用,更是现代 React 开发的实践,包含了组件设计、状态管理、自定义 Hooks、样式优化等多个方面。
项目结构与技术栈
在开始编码之前,我们先看看整个项目的技术选型:
- React 18 - 函数式组件 + Hooks
- Vite - 现代化构建工具
- Stylus - CSS 预处理器
- 自定义 Hooks - 业务逻辑复用
项目(部分)的组件结构设计遵循单一职责原则:
csharp
src/
├── components/
│ ├── Todos/ # 主容器组件
│ ├── TodoForm/ # 新增表单组件
│ ├── TodoList/ # 列表容器组件
│ └── TodoItem/ # 单个任务组件
├── hooks/
│ └── useTodos.js # 自定义 Hooks
├────── App.jsx
└──── global.styl # 全局样式
项目展示



核心功能实现
1. 主容器组件 - 数据流管理的中心
jsx
// src/components/Todos/index.jsx
import {
useState,
useEffect
} from 'react'
import TodoForm from './TodoForm'
import TodoList from './TodoList'
import {useTodos} from '@/hooks/useTodos'
const Todos = () => {
// 通过自定义 Hook 获取 todos 相关的状态和方法
// 这种方式将业务逻辑从组件中抽离,让组件更专注于渲染
const {
todos, // 任务列表数据
addTodo, // 添加任务的方法
onToggle, // 切换任务状态的方法
onDelete, // 删除任务的方法
} = useTodos()
return (
<div className="app">
{/* 将方法作为 props 传递给子组件,实现组件间通信 */}
<TodoForm onAddTodo={addTodo}/>
<TodoList
todos={todos}
onToggle={onToggle}
onDelete={onDelete}
/>
</div>
)
}
export default Todos;
这里体现了 React 的核心思想:数据向下流动,事件向上传递。父组件通过 props 传递数据,子组件通过自定义事件通知父组件状态变化。
2. 表单组件 - 单向数据绑定的实践
jsx
// src/components/Todos/TodoForm.jsx
import { useState } from 'react'
const TodoForm = ({onAddTodo}) => {
// 表单输入的本地状态,只在当前组件内使用
const[text, setText] = useState('')
const handleSubmit = (e) => {
e.preventDefault();
const trimmedText = text.trim();
// 防止提交空内容
if(!trimmedText) return;
// 调用父组件传递的方法,实现数据向上传递
onAddTodo(trimmedText);
// 提交后清空输入框,保持界面状态一致
setText('');
}
return (
<div>
<h1 className="header">Todo List</h1>
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
required
/>
<button type="submit">Add</button>
</form>
</div>
)
}
export default TodoForm;
3. 列表组件 - 性能优化的考量
jsx
// src/components/Todos/TodoList.jsx
import TodoItem from './TodoItem'
const TodoList = (props) => {
const {
todos,
onToggle,
onDelete
} = props
return (
<ul className="todo-list">
{/* TodoList */}
{
todos.length > 0 ? (
todos.map((todo) =>
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => onToggle(todo.id)}
onDelete={() => onDelete(todo.id)}
/>
)
):(
<p>没有待办任务</p>
)
}
{/* <TodoItem/> */}
</ul>
)
}
export default TodoList;
这里的关键点是 Item 组件的设计是为了维护性能。每个 TodoItem 都有独立的 key,确保 React 能够正确地进行 diff 算法优化。
4. 单个任务组件 - 功能的原子化
jsx
// src/components/Todos/TodoItem.jsx
import { useState } from 'react'
const TodoItem = (props) => {
const {
//id,
text ,
isCompleted,
} = props.todo
const {
onToggle,
onDelete
} = props
return (
<div className="todo-item">
<input type="checkbox" checked={isCompleted} onChange={onToggle}/>
<span className={isCompleted ? 'completed' : ''}>{text}</span>
<button onClick={onDelete}>Delete</button>
</div>
)
}
export default TodoItem;
自定义 Hooks - 现代 React 的核心
自定义 Hooks 是现代 React 应用架构的重要组成部分。它不仅仅是简单函数的封装,更是响应式状态和副作用的逻辑复用。
为什么需要自定义 Hooks?
- 业务逻辑的复用 - 将 todos 相关的所有状态管理逻辑抽离
- 组件职责分离 - 组件更好地聚焦于模板渲染
- 函数式编程思想 - 体现了全面 Hooks 函数式编程的理念
useTodos Hook 的设计
js
// src/hooks/useTodos.js
import { useState, useEffect } from 'react'
export const useTodos = () => {
// 初始化任务列表,包含示例数据
const [todos, setTodos] = useState([
{
id: 1,
text: '学习react',
isCompleted: false
},
{
id: 2,
text: '不想学习啦',
isCompleted: false
}
])
// 添加新任务
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text: text,
isCompleted: false
}
setTodos(prev => [...prev, newTodo])
}
// 切换任务完成状态
const onToggle = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id
? { ...todo, isCompleted: !todo.isCompleted }
: todo
)
)
}
// 删除任务
const onDelete = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}
// 返回状态和操作方法供组件使用
return {
todos,
addTodo,
onToggle,
onDelete
}
}
自定义 Hooks 的命名规则:
- use 开头 - 这是 React 的约定
- 封装 state、effect 逻辑 - 实现响应式状态管理
- 返回对象 - 包含状态和操作函数
App.jsx 的实现
jsx
// src/App.jsx
import { useState } from 'react'
import './App.css'
import Todos from './components/Todos'
function App() {
return (
<>
{/* 开发的任务单位就是组件 */}
<div>
<Todos />
</div>
</>
)
}
export default App
样式设计 - 现代化的用户体验
Stylus 预处理器的优势
选择 Stylus 作为 CSS 预处理器,主要因为:
- CSS 超集 - 兼容原生 CSS 语法
- Vite 原生支持 - 无需额外配置,直接编译
- 语法简洁 - 减少代码量,提高开发效率
移动端适配策略
stylus
// src/global.styl
*
margin 0
padding 0
body
font-family -apple-system,Arial,snas-serif
background #f4f4f4
padding 2rem
.app
max-width 600px
margin 0 auto
background white
padding 2rem
border-radius 0.5rem
box-shadow 0 2px 6px rgba(0, 0, 0, 0.1 )
.header
text-align center
.todo-input
display flex
gap 1rem
margin-bottom 1rem
.todo-list
list-style none
padding 0
.todo-item
display flex
justify-content space-between
align-items center
padding 0.5rem
border-bottom 1px solid #ccc
.completed
text-decoration line-through
color #aaa
为什么不用 px?
- 移动端设备尺寸不固定
- rem 基于 html font-size,实现等比例缩放
- vw/vh 基于视口大小,响应式更强
- em 相对于自身 font-size
字体优化
css
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
这样设置的好处:
- apple-system - 苹果设备的系统字体
- 多字体降级 - 确保在不同设备上都有良好体验
- 用户体验 - 前端开发者需要关注字体这种用户体验细节
数据持久化 - 本地存储的选择
localStorage vs Cookie
项目中使用 localStorage 进行数据持久化:
js
// src/hooks/useTodos.js - 本地存储实现
import { useState, useEffect } from 'react'
export const useTodos = () => {
const [todos, setTodos] = useState([])
// 组件挂载时从本地存储读取数据
useEffect(() => {
const savedTodos = localStorage.getItem('todos')
if (savedTodos) {
setTodos(JSON.parse(savedTodos))
}
}, [])
// 每当 todos 变化时保存到本地存储
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos))
}, [todos])
// ... 其他方法
}
为什么选择 localStorage?
特性 | localStorage | Cookie |
---|---|---|
存储大小 | 5MB | 4KB |
性能影响 | 无 HTTP 传输 | 每次请求都传输 |
访问权限 | 仅浏览器端 | 前后端都可以 |
用途 | 客户端数据存储 | HTTP 状态管理 |
存储方案的演进
- Cookie - HTTP 无状态协议的补充,但容量小,影响性能
- localStorage - HTML5 新特性,5MB 容量,适合客户端存储
- IndexedDB - 浏览器数据库,GB 级容量,适合大数据应用
项目中的两个遗憾与解决方案
1. 路径引用的问题
js
// 优化前:复杂的相对路径引用
// src/components/deeply/nested/Component.jsx
import TodoForm from '../../components/TodoForm'
import TodoList from '../../../components/TodoList'
这种相对路径"山路十八弯"的问题,可以通过 Vite 配置 alias 解决:
js
// vite.config.js - 路径别名配置
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
2. 跨组件通信的复杂性
目前 toggle、delete 等方法需要跨越多个组件层级传递,这个问题可以通过 useContext 解决:
js
// src/context/TodoContext.js - Context 状态管理
import { createContext, useContext } from 'react'
const TodoContext = createContext()
// 在应用根组件提供全局状态
const App = () => {
const todoState = useTodos()
return (
<TodoContext.Provider value={todoState}>
<TodoForm />
<TodoList />
</TodoContext.Provider>
)
}
// 在任意子组件中直接使用,无需层层传递
const TodoItem = () => {
const { onToggle, onDelete } = useContext(TodoContext)
// ...
}
总结
通过这个 Todo 应用,我们实践了现代 React 开发的核心概念:
- 组件化开发 - 按功能划分组件粒度
- Hooks 函数式编程 - 用自定义 Hooks 管理业务逻辑
- 单向数据流 - 保持数据流向的清晰和可预测
- 现代化工具链 - Vite + Stylus 提升开发体验
- 移动端适配 - 使用相对单位和响应式设计
这个项目虽然简单,但包含了 React 开发的核心理念。从简单的状态管理到复杂的组件通信,从样式优化到性能考量,每一个细节都体现了现代前端开发的思考。