React Hooks + 自定义Hooks 打造现代化 Todo 应用:记事本

前言

最近学习了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?

  1. 业务逻辑的复用 - 将 todos 相关的所有状态管理逻辑抽离
  2. 组件职责分离 - 组件更好地聚焦于模板渲染
  3. 函数式编程思想 - 体现了全面 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 进行数据持久化:

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 开发的核心概念:

  1. 组件化开发 - 按功能划分组件粒度
  2. Hooks 函数式编程 - 用自定义 Hooks 管理业务逻辑
  3. 单向数据流 - 保持数据流向的清晰和可预测
  4. 现代化工具链 - Vite + Stylus 提升开发体验
  5. 移动端适配 - 使用相对单位和响应式设计

这个项目虽然简单,但包含了 React 开发的核心理念。从简单的状态管理到复杂的组件通信,从样式优化到性能考量,每一个细节都体现了现代前端开发的思考。

相关推荐
chao_7895 分钟前
frame 与新窗口切换操作【selenium 】
前端·javascript·css·selenium·测试工具·自动化·html
天蓝色的鱼鱼18 分钟前
从零实现浏览器摄像头控制与视频录制:基于原生 JavaScript 的完整指南
前端·javascript
三原41 分钟前
7000块帮朋友做了2个小程序加一个后台管理系统,值不值?
前端·vue.js·微信小程序
popoxf1 小时前
在新版本的微信开发者工具中使用npm包
前端·npm·node.js
爱编程的喵1 小时前
React Router Dom 初步:从传统路由到现代前端导航
前端·react.js
每天吃饭的羊2 小时前
react中为啥使用剪头函数
前端·javascript·react.js
Nicholas682 小时前
Flutter帧定义与60-120FPS机制
前端
多啦C梦a2 小时前
【适合小白篇】什么是 SPA?前端路由到底在路由个啥?我来给你聊透!
前端·javascript·架构
薛定谔的算法2 小时前
《长安的荔枝·事件流版》——一颗荔枝引发的“冒泡惨案”
前端·javascript·编程语言
中微子2 小时前
CSS 的 position 你真的理解了吗?
前端·css