运行
npx create-react-app todolist
等待安装
npm start
开启3000端口
修改目录
index.js
入口文件
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList.js';
// import App from './App.js';
ReactDOM.render(
<React.StrictMode>
<TodoList />
</React.StrictMode>,
document.getElementById('root')
);
TodoList.js
import React, { Component, Fragment } from 'react';
import TodoItem from './TodoItem';
import axios from 'axios'
import './style.css';
class TodoList extends Component {
// constructor 初始化的时候会被调用的生命周期函数钩子
constructor(props) {
super(props);
// 当组件的state或者props里面的数据发生变换的时候,render函数就会重新执行
// 存放数据的地方
this.state = {
inputValue: '',
list: []
}
// 改变函数中 this 的指向。不然使用的时候,函数中 this 的指向是 undefined
// 在 constructor 中改变是出于性能方面的考虑
this.handleInputChange = this.handleInputChange.bind(this)
this.handleBtnClick = this.handleBtnClick.bind(this)
this.handleItemDelete = this.handleItemDelete.bind(this)
}
// 组件即将被挂载到页面的时刻即将执行
componentWillMount() {
// console.log('componentWillMount')
}
render() {
// 当父组件的render函数执行的时候。子组件的render组件都将被重新运行
console.log('parent render')
return (
// react 渲染的时候,只允许有一个根节点,所以可以使用 Fragment,这样渲染到界面的时候,不会产生对于的标签
<Fragment>
<div>
// 支持渲染 HTML 标签
<label htmlFor="insertArea">输入内容</label>
// class 在 react 中是代表一个类,所以使用 className 来代替
// value={this.state.inputValue} 花括号里面的是变量,constructor 中的 state 的变量
// onChange={this.handleInputChange} 绑定方法 handleInputChange
// ref={(input) => { this.input = input }} react 是虚拟 DOM, 但是有时候需要操作 DOM
<input
id="insertArea"
className="input"
value={this.state.inputValue}
onChange={this.handleInputChange}
ref={(input) => { this.input = input }}
/>
<button onClick={this.handleBtnClick}>提交</button>
</div>
<ul ref={(ul) => this.ul = ul}>
{this.getTodoItem()}
</ul>
</Fragment>
)
}
// 页面被挂载之后
// componentDidMount() {
// // console.log('componentDidMount')
// }
// 请求数据
componentDidMount() {
// console.log('componentDidMount')
axios.get('/api/todoList').then((res) => {
console.log(res)
this.setState(() => {
return {
list: [...res.data]
}
})
})
}
// 只会在页面挂载的时候被调用,之后数据更新也不会被调用
// 组件被更新之前,会自动执行
// 询问组件是否需要更新,true 需要更新, false 不需要更新
shouldComponentUpdate() {
// console.log('shouldComponentUpdate')
return true
}
// 组件被更新之前,会自动执行, shouldComponentUpdate 之后执行,
// 是否执行,取决于 shouldComponentUpdate 的值, true 执行, false 不执行
componentWillUpdate() {
// console.log('componentWillUpdate')
}
componentDidUpdate() {
// console.log('componentDidUpdate')
}
// 循环渲染数据
getTodoItem() {
return this.state.list.map((item, index) => {
return (
// TodoItem 自定义组件
<TodoItem
key={index}
content={item}
index={index}
deleteItem={this.handleItemDelete}
/>
)
})
}
handleInputChange(e) {
console.log(e.target.value)
// const value = e.target.value
// react 不允许直接改变值,需要借助一个变量存储
const value = this.input.value
// 改变 state 里面的值
this.setState(() => {
return {
inputValue: value
}
})
}
handleBtnClick() {
// prevState 改变之前的数据
this.setState((prevState) => {
return {
// 将之前未改变的数据,与添加的数值整合到一个数组之中
list: [...prevState.list, prevState.inputValue],
inputValue: ''
}
},
() => {
console.log(this.ul.querySelectorAll('div').length)
}
)
}
handleItemDelete(index) {
this.setState((prevState) => {
const list = [...prevState.list]
list.splice(index, 1)
return {
list
}
})
}
}
export default TodoList
TodoItem.js
import React, { Component } from 'react';
import PropsTypes from 'prop-types'
class TodoItem extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
render() {
console.log('child render')
// TodoItem 是 TodoList 的子组件,接收了来自父组件的 content 的传值
const { content } = this.props
return (
<div onClick={this.handleClick}>
{content}
</div>
)
}
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.content !== this.props.content) {
return true;
} else {
return false;
}
}
// 1.一个组件要从父组件接受参数
// 2.如果这个组件第一次存在于父组件中,不会被执行
// 3.如果这个组件已经存在于父组件中,才会执行
componentWillReceiveProps() {
// console.log('child componentWillReceiveProps')
}
// 组件即将从页面中移除的时候会被执行
componentWillUnmount() {
// console.log('child componentWillUnmount')
}
handleClick() {
const { deleteItem, index } = this.props
deleteItem(index)
}
}
// 对父组件传入的数据做校验
TodoItem.propsTypes = {
// 如果父组件没有传入参数,校验不会生效,此时也不会报错,
// isRequired 规定该参数必须传
// test: PropsTypes.string.isRequired,
// content 的类型可以是其中的一个
content: PropsTypes.oneOfType([PropsTypes.number, PropsTypes.string]),
deleteItem: PropsTypes.func,
index: PropsTypes.number
}
TodoItem.defaultProps = {
test: 'hello world'
}
export default TodoItem;
对 TodoList.js 做动画处理
import React, { Component, Fragment } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import TodoItem from './TodoItem';
import './style.css';
class TodoList extends Component {
constructor(props) {
.....
}
....
render() {
.....
}
......
getTodoItem() {
return (
<Fragment>
<TransitionGroup> {
// 一组动画
this.state.list.map((item, index) => {
return (
// 给每一个加上动画
<CSSTransition
// 时间 1s
timeout={1000}
// 动画的名字
classNames="fade"
unmountOnExit
onEntered={(el) => { el.style.color = 'blue' }}
// 初始化的时候也添加动画
appear={true}
>
<TodoItem
key={index}
content={item}
index={index}
deleteItem={this.handleItemDelete}
/>
</CSSTransition>
)
})
}
</TransitionGroup>
</Fragment>
)
}
.....
export default TodoList
style.css
// 出场动画
.fade-enter, .fade-appear {
opacity: 0;
}
.fade-enter-active, .fade-appear-active {
opacity: 1;
transition: opacity 1s ease-in;
}
.fade-enter-done {
opacity: 1;
/* color: aquamarine; */
}
// 退出动画
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 1s ease-out;
}
.fade-exit-done {
opacity: 0;
}
使用 react-hooks 来改写 todoList
还是使用 刚刚的项目结构,将 App.js 改为 App.jsx ,index.js 里面的引入做出相同的修改
import React, { useEffect, useRef, useState, memo, useMemo, useCallback } from 'react';
import './App.css'
// 使用时间戳作为 id
let idSeq = Date.now()
function Control(props) {
const { addTodo } = props
const inputRef = useRef()
const onSubmit = (e) => {
e.preventDefault();
const newText = inputRef.current.value.trim()
if (newText.length === 0) {
return;
}
addTodo({
id: ++idSeq,
text: newText,
complete: false
})
inputRef.current.value = ''
}
return (
<div className="control">
<h1>TODOS</h1>
<form onSubmit={onSubmit}>
<input
type="text"
className="new-todo"
placeholder="todos"
ref={inputRef}
/>
</form>
</div>
)
}
function TodoItem(props) {
const {
todo: {
id,
text,
complete
},
toggleTodo,
removeTodo
} = props
const onChange = () => {
toggleTodo(id)
}
const onRemove = () => {
removeTodo(id)
}
return (
<li className="todo-item">
<input
type="checkbox"
onChange={onChange}
checked={complete}
/>
<label className={complete ? 'complete' : ''}>{text}</label>
<button onClick={onRemove}>×</button>
</li>
)
}
function Todos(props) {
const { todos, toggleTodo, removeTodo } = props;
return (
<ul>
{
todos.map(todo => {
return (
<TodoItem
key={todo.id}
todo={todo}
toggleTodo={toggleTodo}
removeTodo={removeTodo}
/>
)
})
}
</ul>
)
}
const KEY = 'todo'
function TodoList() {
// 定义 todos 状态
const [todos, setTodos] = useState([]);
// addTodo 方法
const addTodo = (todo) => {
setTodos(todos => [...todos, todo])
}
// remove 方法
const removeTodo = useCallback((id) => {
setTodos(todos => todos.filter(todo => {
return todo.id !== id
}))
}, [])
const toggleTodo = useCallback((id) => {
setTodos(todos => todos.map(todo => {
return todo.id === id ? {
...todo,
complete: !todo.complete
} : todo
}))
}, [])
useEffect(() => {
const todos = JSON.parse(localStorage.getItem(KEY))
setTodos(todos)
}, [])
useEffect(() => {
localStorage.setItem(KEY, JSON.stringify(todos))
}, [todos])
return (
<div className="todo-list">
<Control addTodo={addTodo} />
<Todos removeTodo={removeTodo} toggleTodo={toggleTodo} todos={todos} />
</div>
)
}
export default TodoList;
App.css
.todo-list {
width: 550px;
margin: 130px auto;
background: #ccc;
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
}
.control h1 {
width: 100%;
font-size: 100px;
text-align: center;
margin: 0;
color: rgba(175, 47, 47, 0.15);
}
.control .new-todo {
padding: 16px 16px 16px 60px;
border: 0;
outline: none;
font-size: 24px;
box-sizing: border-box;
width: 100%;
line-height: 1.4rem;
box-shadow: rgba(73, 59, 59, 0.15);
}
.todos {
margin: 0;
padding: 0;
list-style: none;
}
.todo-item {
margin: 0;
padding: 0;
list-style: none;
font-size: 24px;
display: flex;
align-items: center;
}
.todo-item input {
display: block;
width: 20px;
height: 20px;
margin: 0 20px;
}
.todo-item label {
flex: 1;
padding: 15px 15px 15px 0;
line-height: 1.2;
display: block;
}
.todo-item label.complete {
text-decoration: line-through;
}
.todo-item button {
border: 0;
outline: 0;
display: block;
width: 40px;
text-align: center;
font-size: 24px;
color: red;
}