引言
最近小编忙于学习React 项目实战,其中React 智能学单词板块的文章可能来不及写,智能实战我将会在抽空在周末休息的时候,花费一天的时间将这两篇文章写出来。好了最近也是沉迷于学习React 学习历程当中,现在小编最近学到了React 自定义Hooks ,这块,今天就让我们基于TodoList项目的实际案例深入探讨一下自定义Hooks。
项目结构和技术栈
项目结构分析
TodoList 作为React 生态中经久不衰的入门标杆,其经典组件化架构堪称理解React设计哲学的活教材。从语义层面拆解,整个应用如同一个精密协作的 "任务管理系统",每个组件都承担着边界清晰的职责:
js
src/
├── App.jsx # 根组件
├── main.jsx # 应用入口
└── components/
└── Todos/
├── index.jsx # 主要状态管理
├── TodoForm.jsx # 表单组件
├── TodoList.jsx # 列表组件
└── TodoItem.jsx # 单项组件
技术栈
主要在css这边使用新的技术栈: Stylus 是一款富有表现力的 CSS 预处理器,它旨在简化样式编写流程,同时赋予开发者更灵活的样式组织能力。那么如何在React当中引入Stylus,在main.js当中直接引入。
js
import './global.styl'

二、任务单元就是组件
2.1组件Todos
在App组件当中引入Todos,所要开发的任务组件之后就在Todos文件下开发,创建componments组件文件夹,将以后所需要写的组件均放入该文件夹当中。根据项目功能划分为TodoForm.jsx表单组件,TodoList.jsx列表组件,再添加一个TodoItem.jsx单项组件。

Todos是整体的功能组件,需要将按照其功能细化,form表单组件,list列表组件,一般会在list列表组件下划分一个Item组件,使其更具有维护和性能。
在创建好各个具体组件页面之后将创建好的组件引入到父组件当中
js
import TodoForm from "./TodoForm"
import TodoList from "./TodoList"
const Todos = () =>{
return(
<>
<TodoForm />
<TodoList />
</>
)
}
export default Todos
到这里界面的初步构建和功能组件的划分就基本完成了,现在需要引入Hooks
函数useState
来解决函数的响应式状态。其中父组件持有管理数据,再通过props
传递数据,子组件通过props
函数来通知父组件(报告老板)。之所以子组件要调用父组件的函数来通知状态发生改变,就是要维护子组件是不能修改状态的,只能通过父组件来修改。
js
import {
useState,
} from 'react'
import TodoForm from "./TodoForm"
import TodoList from "./TodoList"
const Todos = () =>{
// 数据流管理
// 父组件持有管理数据 props 传递数据 子组件通过props 自定义函数
// 通知父组件
const [todos,setTodos] = useState([
{
id: 1,
text: 'AI自习室',
isCompleted: false,
},
{
id: 2,
text: 'AI雀魂',
isCompleted: false
}
])
return(
<div>
Todos
<TodoForm />
<TodoList />
</div>
)
}
export default Todos
isCompleted
是TodoItem的布尔状态属性,记录任务完成状态,驱动视图中复选框状态和文本样式变化,是连接数据与 UI 的关键标识。
现在需要在表单当中传递函数onAddTodo,porps当中包含自定义事件和数据传递。
js
import {
useState,
} from 'react'
import TodoForm from "./TodoForm"
import TodoList from "./TodoList"
const Todos = () =>{
// 数据流管理
// 父组件持有管理数据 props 传递数据 子组件通过props 自定义函数
// 通知父组件
const [todos,setTodos] = useState([
{
id: 1,
text: 'AI自习室',
isCompleted: false,
},
{
id: 2,
text: 'AI雀魂',
isCompleted: false
}
])
// 新增todo
const addTodo = () =>{
// setTodo
}
return(
<div>
Todos
{/* {自定义事件} */}
<TodoForm onAddTodo={addTodo} />
<TodoList todos={todos} />
</div>
)
}
export default Todos
让我们看到当前React开发组件的效果图:

当我们写完TodoForm当中的表单提交函数书写之后,更新 todos 状态。它创建了一个新数组,先将当前 todos 数组里的所有元素展开到新数组中,后续代码通常会添加新的待办事项。这种做法遵循了 React 中状态不可变的原则,即不直接修改原状态,而是创建新状态来更新,这样 React 才能检测到状态变化并重新渲染组件。
js
setTodos([
...todos,
{
id: Date.now(),
text,
isCompleted:false
}
- 状态管理 :todos状态一般是在 Todos 组件中通过
useState Hook
创建和管理的。setTodos
是更新该状 态的函数,因此操作todos
状态的onToggle
和onDelete
函数也定义在同一组件中,方便访问和修改状态。- 组件职责:Todos 组件作为管理待办事项列表的父组件,负责处理待办事项的添加、切换状态和删除等核心逻辑。将这些操作函数定义在该组件中,符合单一职责原则,让组件的功能更加内聚。
- 属性传递:Todos组件可以将onToggle和onDelete函数通过属性传递给子组件(如TodoItem),子组件在触发相应事件(如点击完成按钮、删除按钮)时调用这些函数,实现与父组件的状态同步。
来看看index.jsx最终的代码吧❗❗❗
js
import {
useState,
} from 'react'
import TodoForm from "./TodoForm"
import TodoList from "./TodoList"
const Todos = () =>{
// 数据流管理
// 父组件持有管理数据 props 传递数据 子组件通过props 自定义函数
// 通知父组件
const [todos,setTodos] = useState([
{
id: 1,
text: 'AI自习室',
isCompleted: false,
},
{
id: 2,
text: 'AI雀魂',
isCompleted: false
}
])
// 新增todo
const addTodo = (text) =>{
// setTodo
// 数据状态是对象的时候
setTodos([
...todos,
{
id: Date.now(),
text,
isCompleted:false
}
])
}
const onToggle = (id) =>{
// todos 数组找到id 为id, isComplete !isComplete
// 响应式? 返回一个全新的todos
setTodos(todos.map(
todo => todo.id === id ? {...todo,
isCompleted: !todo.isCompleted} : todo
))
}
const onDelete =(id)=>{
setTodos(todos.filter(todo => todo.id !== id))
}
return(
<div className='app'>
{/* {自定义事件} */}
<TodoForm onAddTodo={addTodo} />
<TodoList
todos={todos}
onToggle={onToggle}
onDelete={onDelete}
/>
</div>
)
}
export default Todos
2.2组件TodoForm
在表单当中设计样式,其中在JSX当中一定得有唯一的最外层元素数来解析JSX
js
import{
useState // 私有状态
} from 'react'
const TodoForm = () =>{
// JSX 一定得有唯一的最外层元素 树来编译解析JSX,
return(
<>
<h1 className='header'>TodoList</h1>
<form>
</form>
</>
)
}
export default TodoForm
在TodoForm当中如何优化解构从父组件当中拿过来的数据,让我们来看看下面的代码:
js
const TodoForm = (props) =>{
const {onAddTodo} = props
// 由于传递就只有一个函数可以简约书写
const TodoForm = ({ onAddTodo }) =>{
const {text,setText} = useState('')
在下面代码当中为何text当中要有私有状态数据呢,首先在子组件当中私有状态不多。但在表单中,表单数据绝对是自己拥有的,不需要受到外部控制。在书写代码当中不要重复书写重复的代码,其中trim()
是一个常用的字符串处理方法,其核心作用是去除字符串首尾的空白字符。来看看其最终代码所示:
js
import{
useState // 私有状态
} from 'react'
const TodoForm = ({onAddTodo}) =>{
// 数据
// props 参数数据
// state 私有的数据
// 单向数据流
const [text, setText] = useState('')
const handleSubmit = (e) =>{
e.preventDefault()
const result = text.trim()
if(!result) return
onAddTodo(result)
setText('') // 数据状态和界面状态一致要敏感
}
// JSX 一定得有唯一的最外层元素 树来编译解析JSX,
return(
<>
<h1 className='header'>TodoList</h1>
<form className='todo-input' onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e=> setText(e.target.value)} // 数据绑定
placeholder='请输入'
required
/>
</form>
</>
)
}
export default TodoForm
2.3组件TodoList
同理可得在TodoList当中同样引入TodoItem使其子组件引入到其父组件当中。
js
import TodoItem from "./TodoItem"
const TodoList = ()=>{
return(
<>
TodoList
<TodoItem />
</>
)
}
export default TodoList
在TodoList中通过todos.length > 0
判断待办事项数组是否为空:
若不为空,使用map方法遍历
todos
数组,为每个待办事项创建一个TodoItem
组件实例。key={todo.id}
为每个组件提供唯一标识,帮助React 高效更新列表。todo={todo}
将当前待办事项对象传递给TodoItem组件。
js
import TodoItem from "./TodoItem"
const TodoList = (props)=>{
const{
todos
} = props
return(
<>
{/* TodoList */}
{
todos.length > 0 ? (
todos.map((todo)=>
<TodoItem key={todo.id} todo={todo} />
)
):(
<p>暂无待办事项</p>
)
}
</>
)
}
export default TodoList
使用箭头函数() => onToggle(todo.id)
和()=>onDelete(todo.id)
是为了延迟函数的执行。若直接写成 onToggle(todo.id)
和onDelete(todo.id)
,函数会在组件渲染时立即执行,而不是在用户触发相应事件(如点击按钮)时执行。通过箭头函数包装,只有当用户触发事件时,才会调用对应的函数并传入正确的todo.id
。
js
import TodoItem from "./TodoItem"
const TodoList = (props)=>{
const{
todos,
onToggle,
onDelete
} = props
return(
<div 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>
)
}
</div>
)
}
export default TodoList
2.3.1 组件TodoItem
TodoItem 组件的主要作用是接收单个待办事项的数据(id 、text 、isComplete),并将待办事项的内容(text )渲染出来。此组件通常会在 TodoList 组件里被循环调用,用于渲染待办事项列表中的每一项。
js
const TodoItem =(props)=>{
const{
id,
text,
isComplete
} = props.todo
return(
<>
{text}
</>
)
}
export default TodoItem
在TodoList上拆分一个TodoItem在性能上可以避免全列表重渲染:当单个任务状态(如isCompleted)变化时,仅对应 TodoItem 会重新渲染,而非整个 TodoList。配合React.memo可进一步拦截无意义的重渲染,尤其在任务数量较多时效果显著。
js
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
效果展示

可以看到实现的效果还是非常的Beautiful的,视觉效果也是非常OK的!!! 在下一篇文章当中将会继续续写React Hooks 的相关的localStorage