入门React——项目实战TodoList
在做这个实战前我们得了解一下React组件的数据流向
单向数据流
React 的单向数据流是指数据在应用中只能从一个方向流动,即从父组件传递到子组件。这种数据流动方式有助于提高应用程序的可预测性和可维护性。
在React中,单向数据流的主要特点如下:
- 父组件向子组件传递数据:父组件通过props将数据传递给子组件。子组件不能直接修改父组件的状态,只能通过回调函数通知父组件进行状态更新。
- 组件状态管理:状态(state)通常由拥有它的组件管理。子组件可以通过调用父组件传递的回调函数来请求状态更新,但不能直接更改状态。
- 数据流动路径明确:由于数据流动是单向的,从父组件到子组件,数据的来源和去向都很明确,易于追踪和调试。
- 组件的独立性:每个组件只关注自己负责的数据和行为,其他部分的数据流动不影响其内部逻辑。这样可以让组件更独立、可复用。
- 数据变更驱动视图更新:当组件的状态或props发生变化时,React会重新渲染这个组件及其子组件,从而更新视图。这种方式确保了视图与数据的一致性。
为什么要使用单向数据流
-
可预测性和易于调试:
- 在单向数据流中,数据只能从父组件流向子组件,数据的流向是确定的。这种确定性使得应用程序的状态和行为更容易预测和理解。
- 当发生问题时,开发者只需检查数据是如何在组件层次结构中流动的,而不需要处理复杂的双向绑定或其他不可预测的数据流动方式。
-
提高组件复用性和可维护性:
- 组件通过props接收数据,通过回调函数向上传递事件。这种方式使得组件更加独立和可复用,因为每个组件只需关注其自身的状态和数据。
- 组件的职责明确,使得代码更易于维护和扩展。
-
简化状态管理:
- 单向数据流使得状态管理更加简单和清晰。所有状态变化都集中在拥有状态的组件中,这使得状态的管理和调试更加容易。
- 当需要共享状态时,可以将状态提升到最近的公共父组件,然后通过props传递给子组件。
-
性能优化:
- React可以更高效地进行性能优化,例如通过
shouldComponentUpdate
生命周期方法或React.memo来减少不必要的重新渲染。单向数据流使得这些优化更容易实现,因为数据变化的源头和影响范围是明确的。
- React可以更高效地进行性能优化,例如通过
-
与Flux/Redux等架构的兼容性:
- 单向数据流与Flux、Redux等状态管理库的理念相一致。这些库利用单向数据流的优势,通过集中管理应用状态,进一步提升了大型应用的可维护性和可扩展性。
-
简化开发模型:
- 在单向数据流中,组件之间的通信变得简单而直观。父组件向子组件传递数据,子组件通过回调函数向父组件发送消息,这种模式降低了理解和使用React的门槛。
开始
这次我们换一个脚手架使用vite,因为vite
比create-react-app
速度快。
npm init vite
后续配置和create-react-app
差不多。
分析布局
- App.jsx 作为父组件在里面写项目标题
- TodoFrom.jsx 子组件配置输入框和Add按钮
- TodoList.jsx 子组件用来配置需要渲染的列表
- TodoItem.jsx 与TodoList.jsx又是父子组件在TodoItem.jsx处理数据然后在TodoList.jsx列表渲染
ok,分析完了来继续下面的操作吧
配置App.jsx
App.jsx作为父组件我们要在里面配置好对数据的增删改的功能,将数据通过单例模式将数据存储在本地文件。先直接展示代码吧
import { Component } from "react";
import TodoFrom from "./components/TodoFrom"
import TodoList from "./components/TodoList"
import './App.css'
import Storage from "./utils/storage";
const instance = Storage.getInstance()
class App extends Component {
constructor(props) {
super(props);
const savedTodos = JSON.parse(instance.getItem('todos')) || []
this.state = {
todos: savedTodos
}
}
componentDidUpdate() {
instance.setItem('todos', JSON.stringify(this.state.todos))
}
addTodo = (text) => {
this.setState({
todos: [
...this.state.todos,
{
text,
completed: false
}
]
})
}
deleteTodo = (index) => {
// focus 数据,不再理底层的API
const newTodos = [...this.state.todos]
newTodos.splice(index, 1)
this.setState({
todos: newTodos
})
}
toggleTodo = (index) => {
const newTodos = [...this.state.todos]
newTodos[index].completed = !newTodos[index].completed
this.setState({
todos: newTodos
})
}
editTodo = (index, newText) => {
const newTodos = [...this.state.todos]
newTodos[index].text = newText
this.setState({
todos: newTodos
})
}
render() {
const { todos } = this.state
return (
<div className="todo-app">
<h1 className="todo-app__title">Todo List</h1>
<TodoFrom addTodo={this.addTodo} />
<TodoList
todos={todos}
toggleTodo={this.toggleTodo}
deleteTodo={this.deleteTodo}
editTodo={this.editTodo}
/>
</div>
)
}
}
export default App;
componentDidUpdate()
这是一个生命周期方法,在组件更新后立即调用。在这个方法中,组件将当前的todos
state转换为JSON字符串并存储在localStorage中,以确保即使页面刷新,数据也能得到保留。
addTodo(text)
这个方法用于向todos
state中添加一个新的待办事项。它接收一个参数text
,表示待办事项的文本内容。方法通过扩展运算符(...
)复制当前的todos
数组,然后在数组末尾添加一个新的对象,该对象包含text
和completed
属性,其中completed
默认为false
。最后,使用setState
方法更新状态。
deleteTodo(index)
这个方法用于从todos
state中删除指定索引位置的待办事项。它首先复制当前的todos
数组,然后使用splice
方法移除对应索引的元素,最后更新状态。
toggleTodo(index)
这个方法用于切换特定待办事项的完成状态。它同样先复制todos
数组,然后修改指定索引处的对象的completed
属性,将其值反转,最后更新状态。
editTodo(index, newText)
这个方法用于编辑特定待办事项的文本内容。它接收两个参数:index
和newText
。方法首先复制当前的todos
数组,然后修改指定索引处的对象的text
属性为newText
,最后更新状态。
配置TodoFrom.jsx
这里主要是要处理对输入框的文本变化更新状态和处理表单的提交事件。这里也上代码
import { Component } from "react"
import './TodoFrom.css'
class TodoFrom extends Component {
constructor(props) {
super(props);
// 私有
this.state = {
inputText: ''
}
}
handleChange = (event) => {
this.setState({
inputText: event.target.value
})
}
handleSubmit = (e) => {
e.preventDefault()
if (this.state.inputText.trim()) {
this.props.addTodo(this.state.inputText)
this.setState({
inputText: ''
})
}
}
render() {
return (
<form className="todo-Form" onSubmit={this.handleSubmit}>
<input type="text"
value={this.state.inputText}
className="todo-form__input"
onChange={this.handleChange}
/>
<button type="submit" className="todo-form__button">Add</button>
</form>
)
}
}
export default TodoFrom;
handleChange(event)
这个方法是一个事件处理器,用于响应输入框中的文本变化。每当输入框的内容发生变化时,这个方法会被调用。它接收一个event
参数,通过event.target.value
获取新的输入值,并使用setState
方法更新组件的inputText
状态。
handleSubmit(e)
这个方法也是一个事件处理器,用于处理表单提交事件。它首先阻止了表单的默认提交行为(e.preventDefault()
),防止页面刷新。接着,它检查输入框的值是否非空(去除首尾空白字符后),如果非空,则调用父组件传递过来的addTodo
方法,将当前的inputText
值作为参数传递。之后,它将inputText
状态重置为空字符串,清空输入框。
render()
render
方法返回了组件的UI结构,即一个包含输入框和提交按钮的表单。表单的onSubmit
事件监听器被设置为handleSubmit
方法,这意味着当用户提交表单时,handleSubmit
方法会被调用。输入框的value
属性被设置为组件的inputText
状态,实现了输入框值与组件状态的双向绑定。此外,输入框还绑定了onChange
事件监听器,当输入框内容变化时,会触发handleChange
方法。
配置TodoList.jsx
这里主要要实现从父组件接收一个待办事项列表(todos
),并为列表中的每一项创建一个TodoItem
组件实例。TodoList
组件扮演的是一个容器角色,它不直接参与数据的处理,而是作为中间层,将数据和操作方法传递给它的子组件TodoItem
。这样简化了数据的收集和处理逻辑,提高了开发效率。然后我们也直接上代码
import { Component } from "react"
import TodoItem from "./TodoItem";
import "./TodoList.css"
// 容器组件
class TodoList extends Component {
constructor(props) {
super(props);
}
render() {
const { todos, deleteTodo, toggleTodo, editTodo } = this.props
return (
<ul className="todo-list">
{todos.map((todo, index) => (
<TodoItem
key={index}
index={index}
todo={todo}
deleteTodo={deleteTodo}
toggleTodo={toggleTodo}
editTodo={editTodo}
/>
))}
</ul>
)
}
}
export default TodoList;
render
方法是组件的核心,它定义了组件的UI结构。在这个方法中:
- 首先,通过解构赋值从
this.props
中提取出todos
、deleteTodo
、toggleTodo
和editTodo
这四个方法,这些方法是由父组件传递下来的。 - 接着,使用
map
函数遍历todos
数组。对于数组中的每一项(todo
)及其对应的索引(index
),都会创建一个TodoItem
组件实例。这里的关键点是key
属性的设置,它被设置为index
,这有助于React高效地识别哪些项已经被添加、删除或移动。 - 在创建
TodoItem
组件实例时,它接收了index
、todo
以及之前提取的三个方法(deleteTodo
、toggleTodo
和editTodo
)作为props。这意味着每一个TodoItem
组件都可以访问它所代表的待办事项的具体信息,以及对这个待办事项进行操作的能力。 - 所有的
TodoItem
组件实例都被包裹在一个<ul>
标签内,形成了一个待办事项列表。
配置TodoItem
这里主要要完成渲染和处理单个待办事项的显示及交互。处理提交的文本。上代码
import { Component } from "react"
import './TodoItem.css'
class TodoItem extends Component {
constructor(props) {
super(props);
this.state = {
isEditing: false,
editText: this.props.todo.text
}
}
handleEditChange = (e) => {
this.setState({
editText: e.target.value
})
}
handleEditSave = (e) => {
this.props.editTodo(this.props.index, this.state.editText)
this.setState({
isEditing: false,
})
}
render() {
const { todo, toggleTodo, index, deleteTodo, editTodo } = this.props;
const { isEditing, editText } = this.state;
const { text, completed } = todo
return (
<li className={`todo-item ${completed ? 'todo-item--completed' : ''}`}>
{isEditing ? (
<div>
<input type="text"
value={editText}
onChange={this.handleEditChange}
className="todo-item__edit-input"
/>
<button className="todo-item__save-btn" onClick={this.handleEditSave}>Save</button>
</div>
) : <div>
<span className="todo-item__text" onClick={() => toggleTodo(index)}>
{text}
</span>
<button className="todo-item__edit" onClick={() => this.setState({ isEditing: true })}>
Edit
</button>
<button className="todo-item__delete" onClick={() => this.props.deleteTodo(index)}>
×
</button>
</div>}
</li>
)
}
}
export default TodoItem;
constructor(props)
构造函数初始化组件的局部状态(state)。这里有两个状态属性:
isEditing
: 控制是否处于编辑模式,默认为false
。editText
: 当组件处于编辑模式时,用于存储编辑后的文本内容,默认初始化为当前待办事项的text
属性。
handleEditChange(e)
这是一个事件处理器,用于响应编辑模式下输入框的文本变化。每当输入框内容变化时,这个方法会被调用,它将更新editText
状态,使其与输入框的当前值同步。
handleEditSave(e)
当用户点击“Save”按钮时,这个方法会被调用。它首先调用父组件传递过来的editTodo
方法,将编辑后的文本内容和当前待办事项的索引作为参数传递,以更新待办事项列表中的对应项。之后,它将isEditing
状态重置为false
,退出编辑模式。
render()
render
方法定义了组件的渲染逻辑。组件根据isEditing
状态的不同,呈现不同的UI:
- 如果
isEditing
为true
,则显示一个输入框和一个“Save”按钮,允许用户编辑待办事项的文本内容。输入框的value
属性绑定到editText
状态,确保输入框的值与状态同步。 - 如果
isEditing
为false
,则显示待办事项的文本、一个切换完成状态的按钮、一个进入编辑模式的“Edit”按钮,以及一个删除待办事项的“×”按钮。 - 待办事项的文本被包裹在一个可点击的
<span>
标签中,点击时会触发toggleTodo
方法,以切换待办事项的完成状态。 - “Edit”按钮用于切换组件进入编辑模式,点击时会更新
isEditing
状态为true
。 - “×”按钮用于删除待办事项,点击时会调用父组件传递过来的
deleteTodo
方法。
并且在这里使用解构赋值的方法使得数据处理更方便。
总结
在项目开发中我们得学会React和Vue等框架的组件化思想,它能极大地提升了代码的可读性、可复用性和可维护性。 使得我们开发的效率提升,思维不会混乱
转载自:https://juejin.cn/post/7389651944254079011