博客 / 詳情

返回

基於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只是一個數組,其實其可以是一個對象,存儲多個需要的對象,使得程序員從多次的父組件傳值解脱出來。
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.