从 0 到 1:用 React 实现一个TodoList
前言
在当今数字化的时代,用户对于网页和应用的交互体验要求越来越高。为了满足这些需求,开发者们不断探索和创新,而 React 框架的出现,为构建高效、动态且用户友好的前端界面提供了强大的支持。
了解如何创建和实现一个React项目有助于让我们拥有React项目的经验,成为面试中的加分项。
具体步骤
项目准备阶段
-
初始化一个项目。
npm init vite
-
输入项目名称。
-
选择项目使用的框架。
-
选择项目的开发语言。
-
完成项目的创建
-
执行以下指令:
cd TodoList npm install npm run dev
cd TodoList
:改变当前工作目录至TodoList
。npm install
:进入TodoList
目录后执行这个指令后会根据package.json
文件中描述的所需要的依赖进行下载。npm run dev
:启动项目的指令,它是package.json
中的scripts
字段定义的dev
脚本。
创建组件树
组件树是组件按照它们之间的嵌套关系形成的一颗树状结构。
在项目开始搭建的第一步就是将项目划分为多个组件,然后创建一个组件树。这样有利于让我们专注于某些特定的组件而不是整个代码库,并且有助于状态管理,每个组件可以拥有自己的状态,也可以接收来自父组件的状态。这种状态的隔离减少了组件间的耦合度,使应用更加健壮。
我将整个项目划分为四个组件,分别是App
、TodosForm
、TodosList
和TodosItem
,并且把它们打造成通过继承 React.Component
类的类组件。在类组件中要一定要有render
方法,它是类组件的核心方法,负责返回要渲染的 JSX 结构,描述组件在页面上的呈现方式。
tips:为了实现数据的持久化(即数据在页面刷新、关闭浏览器甚至设备重启后仍然存在),可以利用浏览器提供的存储机制。
App
组件:App
组件从浏览器本地存储中获取待办事项数据,并且为子组件定义了增加、修改、删除和标记等数据操作方法,然后整合并管理着TodosForm
和TodosList
这两个子组件,构建出完整的应用界面结构。TodosForm
组件:在页面上展示输入框和添加按钮实现添加待做事项。TodosList
组件:在页面上显示TodosItem
组件内的列表TodosItem
组件:在页面上显示具体的待做事项数据,并且提供修改、删除和标记功能。
以下是这个项目的组件树:
App
/ \
/ \
TodosForm TodosList
\
\
TodosItem
tip:可以在浏览器上安装React Developer Tools
扩展,可以快速查看项目的组件树和数据。
数据流向
父组件向子组件传输数据:父组件通过属性(props)将数据传递给子组件。子组件通过接收父组件传递的属性来获取数据,并在其内部进行使用和处理。
子组件向父组件通讯:子组件不能直接修改父组件的状态,但是可以通过父组件传递的回调函数来实现。父组件将一个函数作为属性传递给子组件,子组件在特定情况下调用这个回调函数,将数据传递回父组件。
类组件
在 React 中,类组件是一种创建组件的方式,是通过继承 React.Component
类来实现的。
类组件由以下部分组成:
导入模块:在类数组中需要引入必要的模块,其中必须引用的 是React.Component
类。
import { Component } from "react";
类的定义:使用class
定义成一个类,并使其继承自Component
类。
class 组件名 extends Component {
}
构造函数:然后在定义的类中的构造函数。在构造函数中必须通过 super(props)
来调用父类(Component
)的构造函数,以正确初始化组件。
constructor(props) {
super(props)
}
状态:状态(state)是组件的核心部分,是一个对象,其中的属性值可以在组件的生命周期中发生变化,并触发组件的重新渲染。state
与props
不同,state
是组件私有的,不会被外部组件访问,并且state
是可以改变的。可以通过调用setState
方法来实现state
在改变后React重新渲染组件。
方法:在类中可以定义各种方法用于处理组件的逻辑、实现组件的功能,并且可以成为父组件与子组件进行数据通讯的方式。
函数名 = (event)=>{}
render方法:render
方法是类组件的核心,是一定要有的。它用于返回描述组件UI的结构的JSX表达式。根据组件的状态和属性,决定要渲染的具体内容。
render() {
return (
<div>
<子组件/>
</div>
)
}
各个组件的完成
根组件
引入模块:引入React.Component
类,以及根组件的两个子组件。
import { Component } from "react";
import TodosForm from './components/TodosForm';
import TodosList from './components/TodosList';
定义类:定义App
类并且继承与Component
。
class App extends Component {}
构造函数:获取浏览器本地存储的待做事项数据,然后没有则默认为一个空数组,然后存放在组件自己的属性todos
中。
constructor(props) {
super(props)
const savedTodos = JSON.parse(localStorage.getItem('todos')) || [];
//组件的状态
this.state = {
todos: savedTodos
}
}
在类中定义方法:
这些方法都是通过使用扩展运算符的浅拷贝方法拷贝待做事项数据数组到一个新数组中,然后再新数组中进行操作,再将操作后数组赋值给了todos
,并且通过setState
方法将该todos
打造成响应式的,在发生变化时会重新渲染组件。
addTodo
方法:根据待做事项参数将待做事项数据添加到todos
中。deleteTodo
方法:根据下标参数删除todos
中指定的数据。toggleTodo
方法:根据下标参数对标记数据进行取反,然后重新赋值给todos
。editTodo
方法:根据下标参数和新的待做事项参数对todos
中的具体数据进行修改。
addTodo = (text) => {
this.setState({
todos: [
...this.state.todos,
{
text,
completed: false
}
]
})
}
deleteTodo = (index) => {
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 newTodo = [...this.state.todos]
newTodo[index].text = newtext;
this.setState({
todos: newTodo
})
}
为了保持数据的持久性,需要定义一个生命周期方法componentDidUpdate
,并在其中进行将当前todos
内的数据存储回浏览器本地存储的操作。componentDidUpdate
方法会在组件更新后被自动调用。
componentDidUpdate() {
console.log('updata...');
localStorage.setItem('todos', JSON.stringify(this.state.todos))
}
render
方法:在页面上呈现出TodosForm
和TodosList
组件,并且将addTodo
方法传递给TodosForm
组件,将组件当前状态中的todos
数组、deleteTodo
方法、toggleTodo
方法和editTodo
方法传递给TodosList
组件。
render() {
return (
<div>
<h1>TodoList</h1>
<div>
<TodosForm addTodo={this.addTodo} />
<TodosList
todos={this.state.todos}
deleteTodo={this.deleteTodo}
toggleTodo={this.toggleTodo}
editTodo={this.editTodo}
/>
</div>
</div>
);
}
最后抛出组件。
export default App;
TodosForm组件
import { Component } from "react";
import './TodosForm.css'
class TodosForm extends Component {
constructor(props) {
super(props);
this.state = {
inputText: ''
}
}
handleChange = (e) => {
this.setState({
inputText: e.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"
placeholder="请输入待办事项"
className="todo-form_input"
value={this.state.inputText}
onChange={this.handleChange}
/>
<button type="submit" className="todo-form_button">Add</button>
</form>
)
}
}
export default TodosForm;
在这个组件中实现的功能如果用Vue实现,可以通过v-model
的双向绑定实现,都是在React在不使用双向绑定,因为每次改变都会造成重新渲染从而影响性能,所以在React中只存在数据的单向绑定。
在组件状态中创建一个inputText
变量配合被文本框的onChange
事件触发的handleChange
方法一起获取到用户输入的数据。然后再由按钮的提交事件和handleSubmit
方法进行配合,在inputText
不为空的情况下调用父组件传递的addTodo
方法将新增的待做事项数据传递回父组件。
TodosList组件
import { Component } from "react";
import TodosItem from "./TodosItem";
import './TodosList.css'
class TodosList extends Component {
render() {
const { todos, deleteTodo, toggleTodo, editTodo } = this.props;
return (
<div>
<h3>List:</h3>
{
todos.map((todo, index) => {
return <TodosItem
key={index}
index={index}
todo={todo}
deleteTodo={deleteTodo}
toggleTodo={toggleTodo}
editTodo={editTodo}
/>
})
}
</div>
)
}
}
export default TodosList;
在TodosList
组件中解构从组件的属性(props
)中获取了 todos
数组以及 deleteTodo
、toggleTodo
和 editTodo
这几个操作函数。
然后在render
方法用花括号在 JSX 中嵌入 JavaScript 表达式,通过数组的map
方法生成一系列的TodosItem
组件,并且将一系列的数据传递给子组件。
TodosItem组件
import { Component } from "react";
import './TodosItem.css'
class TodosItem extends Component {
constructor(props) {
super(props);
this.state = {
isEditing: false,
editText: this.props.todo.text
}
}
handleChange = (e) => {
this.setState({
editText: e.target.value,
})
}
handleEditSave = () => {
this.props.editTodo(this.props.index, this.state.editText);
this.setState({ isEditing: false })
}
render() {
const { todo, index, deleteTodo, toggleTodo, editTodo } = this.props;
const { text, completed } = todo
return (
<li className={`todo-item ${completed ? 'todo-item--completed' : ''}`}>
{
this.state.isEditing ?
(
<div>
<input type="text" value={this.state.editText} onChange={this.handleChange} />
<button onClick={this.handleEditSave}>Save</button>
</div>
) :
(
< div >
<span className="todo-item__text" onClick={() => toggleTodo(index)}>{todo.text}</span>
<button onClick={() => this.setState({ isEditing: true })}>Edit</button>
<button className="todo-item_delete-btn" onClick={() => deleteTodo(index)}>Delete</button>
</div>
)
}
</li >
)
}
}
export default TodosItem;
//TodosItem.css文件内容
.todo-item--completed .todo-item__text {
text-decoration: line-through;
color: #888;
}
在TodosItem
组件中的render
方法中通过花括号引入JavaScript表达式,JavaScript表达式是一个三元运算符:
- 当需要修改在页面展示的待做事项列表里的待做事项时则在页面渲染出一个文本输入框和保持按钮,通过
TodosItem
组件状态中的editText
变量将需要修改的原待做事项内容显示在文本输入框上,然后再通过handleChange
方法和文本框的onChange
事件获取修改后的文本框内容,最后在保存按钮的点击事件触发的handleEditSave
方法中将修改后的数据传递给根组件,并且对todos
数组进行修改。 - 当不需要修改在页面展示的待做事项列表里的待做事项时则在页面展示出待做事项和一个修改和删除按钮。当点击待做事项文章后则会对其进行标记,通过在
li
标签中动态增加todo-item--completed
类名,待做事项出现被标记的样式。当点击修改按钮后触发步骤一。当点击删除按钮后触发deleteTodo(index)
回调函数,通过传递下标让根组件删除掉对应的数据。
效果展示
转载自:https://juejin.cn/post/7387999151412690979