基于React实现ToDoList功能

背景

学习React,并实现ToDoList功能(分为2个部分,依靠父组件传值实现,dva实现):
实现的页面

目标分析

  1. 功能确定
  2. 组件划分
  3. 代码实现
    我们可以确定大概的功能有发布事件,删除事件,显示事件内容和截止日期,统计事件等。
    依照以上功能可以做出大概的组件划分图
    组件划分图
    其中ToDoListInput为发布事件功能,List完成对事件的显示,listItem是事件,其中包括对事件的删除,勾选,内容显示等功能,Statistics完成对事件的统计等。
    代码部分采用React+antd完成

代码实现

容易从组件划分图可以得出,ToDoList,List,Statistics之间的联系应该通过ToDoListApp,即它们的父组件来完成,因此我们可以在ToDoListApp的state中设置list数组,用于储存事件,以及创建修改list的方法等,并且通过父组件传值的方式将list传入Statistics等子组件中。

为了简化代码,我并没有完全按照概念图来写组件内容,而因为功能比较简单所以我将ToDOListInPut,Statistics承担的功能均放在ToDoListAPP中,而List中就之间完成了渲染每个子组件。
分析代码需要完成的功能。

父组件ToDoList:
使用的库

import React, { Component } from 'react';
import TodoListItem from './TodoListItem';
import {nanoid} from 'nanoid'
import {DatePicker, Input,Button}from 'antd'
import moment from 'moment'
import './index.css';

其中nanoid是生成唯一标识的库,通过nanoid()调用

State设置

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [],
      finished: 0,
      inputValue: '',
      date:null
    };
  }
...
}

list用于存储发布事件,finished用于记录完成的事件数,inputValue和date则是为了获取发布事件的内容和截止日期。

添加事件。

handleAddTask = () => {
    const { inputValue,date} = this.state;
    if (!inputValue) {
      alert("输入为空,请重新输入待办事项");
      return
    }
    if(!date)
    {
      alert("截止时间为空,请重新选择截止日期")
      return
    }
    var obj = {
      content: inputValue,
      status: false,
      deadline: moment(date).format('YYYY-MM-DD HH:mm:ss'),
      nanoid:nanoid()
    };
    const { list } = this.state;
    list.push(obj);
    this.setState({
      list,
      inputValue: '',
      date:null
    });
  }

先判断发布的事件中的内容或者日期是否为空。再从state中获取inputValue,date数据,再设置事件的默认勾选状态status为false不勾选。设置好添加的事件以后,push进state中的list中,最后使用setState修改list中的值。

修改inputValue中的值

handleChangeValue = (event) => {
    const { value } = event.target;
    this.setState({
      inputValue: value
    });
  }

修改date的值

handleDateChange=(e)=>{
    this.setState({date:e})
  }

传递给子组件用于删除事件

handleClickDelete = (indexID) => {
    const { list } = this.state;
    const List = list.filter(item =>
      item.nanoid !== indexID);
    this.setState({
      list: List
    });
  }

传入事件中的nanoid,以确定事件在list中的下标,使用filter函数过滤传入的id值,最后使用setState修改list的值。

传递给子组件用于统计完成的事件

updateFinished = (indexID) => {
    const { list } = this.state;
    list.forEach((item) => {
        if (item.nanoid === indexID) {
            item.status=!item.status
        }
    })
    this.setState({list:list})
    let finishedTask = 0;
    list.forEach((item) => {
      if (item.status === true) {
        ++finishedTask;
      }
    });
    this.setState({
      finished: finishedTask
    });
  }

从state中获取list,遍历数组,并且统计其中status为true的个数,即被勾选的个数,最后使用setState修改finished的值,用于显示完成的任务数。

render函数

 render() {
    const { list,finished,inputValue,date} = this.state;
    return (
      <div className="container">
        <h1>TO DO LIST</h1>
        <hr></hr>
        <div>
          <TodoListItem
            list={list}
            handleClickDelete={this.handleClickDelete}
            updateFinished={this.updateFinished}
          />
          <div className="item-count">
            {finished}
            已完成任务/
            {list.length}
            任务总数
          </div>
          <div className="addItem">
            <Input
              placeholder="Add your item……"
              onKeyPress={this.enterPress}
              value={inputValue}
              onChange={this.handleChangeValue}
            />
            <DatePicker onChange={this.handleDateChange} showTime value={date ? moment(date, 'YYYY-MM-DD HH:mm:ss') : null}/>
            <Button className="addButton"
              onClick={this.handleAddTask}
              shape='round'
            >
              添加
          </Button>
          </div>

        </div>
      </div>
    );
  }

通过父组件传值的方式将handleClickDelete,updateFinished方法传递给子组件,并且将list对象数组传递给子组件。

子组件ToDoListItem
使用的库

import { Radio, Button, Input } from 'antd';
import React, { Component } from 'react';

父组件传入的函数

handleClickDelete(indexID) {
        this.props.handleClickDelete(indexID);
}
handleClickFinished = (indexID) => {
        this.props.updateFinished(indexID);
}

渲染每个子组件

listMap = () => {
        const { list } = this.props;
        return (
            list.map(item => {
                return (this.listMapItem(item))
            })
        )
    }

将list中的每个组件都使用listMapItem函数分别渲染,最后用render函数渲染。

渲染父组件传递进list中的每个对象

listMapItem = (item) => {
        return (
            <div className='wrapper-item'
                key={item.nanoid}
            >
                <div className="item">
                    <div className='item-select'>
                        <Radio
                            checked={item.status}
                            onClick={this.handleClickFinished.bind(this, item.nanoid)}
                        />
                        <Input
                            style={{ textDecoration: item.status ===false ? 'none' : 'line-through',flexBasis:500} }
                            value={item.content}
                        >
                        </Input>
                    </div>
                    <span>{item.deadline}</span>
                    <Button
                        style={{margin:'0px 0px 0px 20px'}}
                        onClick={this.handleClickDelete.bind(this, item.nanoid)}
                    >
                        删除
                    </Button>
                </div>
            </div>
        );
    }

最后render渲染

 render() {
        return (
            <div>
                {this.listMap()}
            </div>
        );
    }

代码遗留问题以及值得注意的地方

遗留问题:发布事件,即handleAddTask方法中,既然已经使用了react中的组件,这里可以用message组件进行替代。
值得注意的地方:在开始的代码编写,使用map函数对list中每个对象进行渲染时的key值采用的默认id值,这是不可取的,因为如果其他地方也使用了map函数,就会出现重复的id值。这里可以使用nanoid生成唯一标识符来设置key值。
因为此次的代码较为简单,组件划分的层数也不多,依靠父组件传值就没有什么问题,但是在实际的工程项目中,往往一个项目往往有几十层,这时候一层一层的传递数据是低效的,且每增添一个函数,程序员就需要在每一层的代码中进行改动,难以维护、我们意识到仅仅靠父组件传值的方式,我们很难在大型的工程项目中应用。因此,我经过实验室老师指导,通过dva.js重新构建了此次的代码,并且修复了遗留的问题。

dva部分

背景

同上,安装dva的具体方法参考dva官网

目标分析

同上

与父组件传值方式差异性分析

有差别的是,此次通过dva来构建代码。dva中通过model层来管理需要共用的state部分,省去了一层一层传值的时间。并且dva是基于redux、redux-saga 和 react-router的基础上建立的,可以方便的进行页面跳转,异步处理等。而与父组件传值方式相同的是,当model中的state值修改后,关联了该model的组件也会重新渲染
dva程序运行图解

model层分析

dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions。(由于本次实现的功能较为简单不用subscriptions)
我们可以将原来ToDoListApp中State中的数据全部放入ToDoList.js这个model中

export default {
    namespace: 'list',
    state: {list:[],finished:0,inputValue:'',date:null},
    reducers:{}
    effect:{}
    }

namespace 表示在全局 state 上的 key
state 是初始值,在这里是一个对象。
reducers 等同于 redux 里的 reducer,接收 action,同步更新 state

index.js

import dva from 'dva';
const app=dva();
app.model(require('./models/todoList').default);
app.router(require('./router').default);
app.start('#root');

router.js

import React from 'react';
import Products from './routes/Products';
import { Router, Route, Switch } from 'dva/router';
import IndexPage from './routes/IndexPage';
import TodoList from './routes/TodoList';
function RouterConfig({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/products" exact component={Products}  />
        <Route path="/index" exact component={IndexPage}/>
        <Route path="/todoList" exact component={TodoList}/>
      </Switch>
    </Router>
  );
}

export default RouterConfig;

此处每个Route都是不同的页面,path中为浏览器访问的路径,exact,component={}是要显示的组件。

Routes/ToDoList.js

使用的库

import React, { Component } from 'react';
import TodoListItem from './TodoListItem';
import {nanoid} from 'nanoid'
import {DatePicker, Input,Button,message}from 'antd'
import moment from 'moment'
import {connect} from 'dva'
import styles from './index.css';

connect可以理解为是组件与model层的桥梁

connect修改和获取model中的信息

//用于获取model中的list信息
const mapStateToProps=({list})=>{
    return {list:list}
}
//用于修改model中的list信息
const mapDispatchToProps=(dispatch)=>{
  return{
      handleClickAdd:({status,deadline,content,nanoid})=>{
        dispatch({type:'list/addListItem',obj:{status,deadline,content,nanoid}})
      },
      handleChangeInputValue:(value)=>{
         dispatch({type:'list/handleChangeValue',value:value})
      },
      handleChangeDate:(date)=>{
         dispatch({type:'list/handleChangeDate',date})
      },
      handleChangeInputValueAndDate:(inputValue,date)=>{
        dispatch({type:'list/handleChangeDateAndInputValue',value:inputValue,date})
      },
    }
}
export default connect(mapStateToProps,mapDispatchToProps)(TodoList)

与redux的使用基本无差异,mapStateToProps是获取model中的state,其参数为model中的namespace,mapDisPatchToProps则是用于修改model的state。两者都通过高阶组件将对象传递给自身,所以可以用this.props来使用。dispatch中type匹配model中effect的关键字来实现修改model中的state参数,除了type外的其他元素均可以当做传递给了model相匹配的一个函数一个action对象。

添加任务

handleAddTask = () => {
    const { inputValue,date} = this.props.list;
    if (!inputValue) {
      message.info("输入为空,请重新输入待办事项");
      return
    }
    if(!date)
    {
     message.info("截止时间为空,请重新选择截止日期");
      return
    }
    var obj = {
      content: inputValue,
      status: false,
      deadline: moment(date).format('YYYY-MM-DD HH:mm:ss'),
      nanoid:nanoid()
    };
   this.props.handleClickAdd(obj)
   const nullInputValue=''
   const nullDate=null
   this.props.handleChangeInputValueAndDate(nullInputValue,nullDate)
  }

其主要逻辑与父组件传值的逻辑基本一致,不同的是,此处是修改model中的state数据。

修改日期

 handleDateChange=(e)=>{
    this.props.handleChangeDate(e)
  }

修改inputValue

 handleChangeValue = (event) => {
    const { value } = event.target;
    this.props.handleChangeInputValue(value)
  }

render函数

  render() {
    const { finished,inputValue,date,list} = this.props.list;
    return (
      <div className={styles.container}>
        <h1>TO DO LIST</h1>
        <hr></hr>
        <div>
          <TodoListItem/>
          <div className={styles.itemCount}>
            {finished}
            已完成任务/
            {list.length}
            任务总数
          </div>
          <div className={styles.addItem}>
            <Input
              placeholder="Add your item……"
              onKeyPress={this.enterPress}
              value={inputValue}
              onChange={this.handleChangeValue}
            />
            <DatePicker onChange={this.handleDateChange} showTime value={date ? moment(date, 'YYYY-MM-DD HH:mm:ss') : null}/>
            <Button className={styles.addButton}
              onClick={this.handleAddTask}
              shape='round'
            >
              添加
          </Button>
          </div>

        </div>
      </div>
    );
  }

与父组件传值不同的是,由于state已经在model层中,其是共用的,所以不再需要通过父组件来传递修改方法,而是在子组件中写即可。

Route/ToDoListItem.js

connect

const mapStateToProps=({list})=>{
    return {list:list}
}
const mapDispatchToProps=(dispatch)=>{
    return{
        handleClickDelete:(id)=>{
            dispatch({type:'list/deleteListItem',id:id})
        },
        handleUpdate:(list)=>{
            dispatch({type:'list/updateList',list:list})
         },
        handleChangeFinished:(finishedTask)=>{
            dispatch({type:'list/handleChangeFinished',finishedTask})
          }
      }
}

其余的方法格式与ToDoList一致,不再赘述

Model/ToDoList.js

reducers

update(state,action){
        return action.newState
      }

reduce是纯函数,所以只负责刷新状态即可

effect

effects:{
      *deleteListItem(action,{put,select}){
        const {list}=yield select(_=>_.list)
        const newList=[...list.filter(item=>item.nanoid!==action.id)]
        const newState={...yield select(_=>_.list),list:newList}
        console.log(newState)
        yield put({type:'update',newState})
      },
      *updateList(action,{put,select}){
        const newList=[...action.list]
        const newState={...yield select(_=>_.list),list:newList}
        yield put({type:'unpdate',newState})
      },
      *addListItem(action,{put,select}){
        const {list}=yield select(_=>_.list)
        const newList=[...list,action.obj]
        const newState={...yield select(_=>_.list),list:newList}
        yield put({type:'update',newState})
      },
      *handleChangeValue(action,{put,select}){
        let newState={...yield select(_=>_.list)}
        newState['inputValue']=action.value
        yield put({type:'update',newState})
      },
      *handleChangeDate(action,{put,select}){
        let newState={...yield select(_=>_.list)}
        newState['date']=action.date
        yield put({type:'update',newState})
      },
      *handleChangeDateAndInputValue(action,{put,select}){
       let newState={...yield select(_=>_.list)}
       newState['inputValue']=action.value
       newState['date']=action.date
       yield put({type:'update',newState})
      },
      *handleChangeFinished(action,{put,select}){
        let newState={...yield select(_=>_.list)}
        newState['finished']=action.finishedTask
        yield put({type:'update',newState})
       },
    }

effect负责业务逻辑的处理,因此在组件中dispatch应该匹配effct中相应的名称进行操作,其中action为dispatch中除了type以外的对象。而后面的对象为yield需要的操作,通常有call,select,put。此处用到select选取state中的数据,最后操作完后,通过type匹配reducer中相应的名称,完成对state的更新。

值得注意的地方

  1. model中的reducers是纯函数,不能在其内部处理业务逻辑,相关的操作应该在effects中处理
  2. 组件刷新与否取决于model层的state是否刷新,而state刷新则需要判断state是否改变,而仅仅改变state中的值是不会刷新的,因为state对象的地址并没有改变,所以在effect中我们采用{...对象名}来深拷贝state,使得组件可以正常刷新
  3. dva官方文档中state只是一个数组,其实其可以是一个对象,存储多个需要的对象,使得程序员从多次的父组件传值解脱出来。
3 声望
0 粉丝
0 条评论
推荐阅读
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木150阅读 12.3k评论 10

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 7.9k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy46阅读 6k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.2k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.3k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木44阅读 7.4k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan43阅读 3k评论 14

封面图
3 声望
0 粉丝
宣传栏