通过复盘todo list小项目,学习react-redux

前言

自学react的时候看的是程墨的《深入浅出React和Redux》,这本书很适合初学者,使用create-react-app创建项目,引用一句书中原话:

用这种最简单的方式创建可运行的应用,必要的时候才会介绍底层技术拢的细节,毕竟,没有什么比一个能运行的应用更加增强开发者的信心。

过多的配置会消磨掉学习者的大部分耐心,甚至失去学习react的兴趣,尽早开始实际的react开发是最高效的学习方法。

虽然很喜欢这本书,但是本文的todo list和《深入浅出React和Redux》中的实例不同,是我自己摸索着完全自行设计开发的,一条todo记录可以有3种状态,未开始(todo),正在进行(doing)和已完成(done)。当然,如果遇到问题,这本书一直都是我的参考书。

预备知识

《深入浅出React和Redux》前4章内容,react和redux相关知识这里不再赘述,本文适合"书里说的我都懂,但是从来没有动手实践过的仔"。

项目链接

这也是注册github这么久第一个阶段性完成的项目=(:з」∠)_
欢迎加星!
https://github.com/SampleTape/todolist/

项目截图

项目截图.PNG

组件划分

ToDoListPanel是最外层的容器组件,其中包含着Filter,List,AddDialog,而List中包含着多个Item。

  • Filter用于通过todo的状态进行过滤,是顶部的tab选项卡。
  • List就是todo列表,包含多个Item,目前作用是根据filter种类更换背景色。
  • Item是一条todo,可以更换todo状态,或删除本条todo。
  • AddDialog是下方新建一条todo时的对话框,只在用户点击"+"按钮时显示。
ToDoListPanel
  |_Filter
  |_List
      |_Item
  |_AddDialog

项目目录

项目目录.PNG

看着这么多文件,从哪里入手刚开始的确让人头疼。

1. 首先你需要在电脑中找到一个地方,使用上文提到的命令快速创建todolist项目文件夹,其中的大部分文件,这个指令会为你自动生成:

C:\Users\zuoy\Desktop\react> creact-react-app todolist

2. 既然用到react-redux,别忘了安装它:

C:\Users\zuoy\Desktop\react\todolist> npm install react-redux

3. 然后呢,我们可以先创建几个空文件夹和空文件:

  • components
  • images
  • styles
  • actions.js
  • actionTypes.js
  • reducer.js
  • store.js

以上就是这个项目中,我们需要集中精力的几个主要地方,已有的index.js稍后需要稍作修改,其中images中放图片,styles中放样式文件,这些我们先忽略,是不是比截图中看起来感觉轻松些(✿◡‿◡)

4. 接下来呢,从store.js开始吧,这个文件的主要作用是定义数据结构,并且通过createStore生成store,并且把reducer回调函数和初始值传给它,store中总是保存着最新鲜的数据:

import {createStore} from 'redux';
import reducer from './reducer';

const initialList = {
    todos: [
        {
            id: 0,
            what: 'dream',
            status: 'todo',
        }
    ],
    filter: 'todo',
    showadddialog: false,
    counter: 1,
};

const store = createStore(reducer, initialList);

export default store;

todos是个数组,用来储存todo记录,每个todo记录中包含id,what,status几个字段。filter表示筛选项,showadddialog用来控制添加新todo对话框的显示隐藏,counter表示todo记录计数器,将来会被赋值给id,只增加不减少。

5. 接下来要写的时actionTypes.js,用来定义action的种类,这里需要根据你设计的功能点来确定需要几种action。

export const ADD = 'add';

export const DELETE = 'delete';

export const START = 'start';

export const FINISHED = 'finished';

export const FILTER = 'filter';

export const SHOWADDDIALOG = 'showadddialog';

6. 既然actionTypes.js 都已经写好,现在开始写actions.js吧,这里需要确定不同action种类需要传递给reducer什么样参数,所以返回值除了包含type这个字段以外,还要包含传递的参数:

import * as ActionTypes from './actionTypes';

export const add = (what) => {
    return {
        type: ActionTypes.ADD,
        id: 0,
        what,
    };
};

export const deleteit = (id) => {
    return {
        type: ActionTypes.DELETE,
        id,
    };
};

export const start = (id) => {
    return {
        type: ActionTypes.START,
        id,
    };
};

export const finished = (id) => {
    return {
        type: ActionTypes.FINISHED,
        id,
    };
};

export const filter = (filter) => {
    return {
        type: ActionTypes.FILTER,
        filter,
    };
}

export const showAddDialog = (showadddialog) => {
    return {
        type: ActionTypes.SHOWADDDIALOG,
        showadddialog,
    }
}

7. 刚刚提到了reducer,现在我们开始写reducer.js,reducer负责根据action种类和参数更新state,记住不要直接修改state,而是返回一个新的对象:

import * as ActionTypes from './actionTypes';

export default (state, action) => {
    let newTodos = [...state.todos];
    let index = newTodos.findIndex(todo => todo.id === action.id);
    switch(action.type) {
        case ActionTypes.ADD:
            let todo = {
                id: state.counter,
                what: action.what,
                status: 'todo',
            };
            newTodos.push(todo);
            return {...state, todos: newTodos, counter: state.counter + 1};
        case ActionTypes.DELETE: 
            newTodos.splice(index,1);
            return {...state, todos: newTodos};
        case ActionTypes.START:
            newTodos[index].status = 'doing';
            return {...state, todos: newTodos};
        case ActionTypes.FINISHED:
            newTodos[index].status = 'done';
            return {...state, todos: newTodos};
        case ActionTypes.FILTER:
            return {...state, filter: action.filter};
        case ActionTypes.SHOWADDDIALOG:
            return {...state, showadddialog: action.showadddialog};
        default:
            return state;
    }
}

8. 基础已经打好了,可以开发组件了!

  • adddialog.js
  • filter.js
  • item.js
  • list.js
  • todolistpanel.js

8.1. item.js

与其他组件一样,其中包含着一个纯函数作为傻瓜组件(展示组件)。它不需要追踪最新的state所以mapStateToProps函数只返回一个空对象;当鼠标点击相应按钮会触发一个动作,这时需要dispatch一个action,使store更新状态,需要在mapDispatchToProps函数中定义一系列函数 ,这些方法会被传递给傻瓜组件使用;connect可以根据提供的参数将傻瓜组建转换成容器组建。

import React from 'react';
import * as Actions from '../actions';
import {connect} from 'react-redux';
import start from '../images/start.png';
import finished from '../images/finished.png';
import deleteit from '../images/deleteit.png';

import '../styles/item.scss';

function Item({id, what, startToDo, finishToDo,deleteToDo}) {
    return (
        
{what}
start
finished
deleteit
); } function mapStateToProps(state, ownProps) { return {}; } function mapDispatchToProps(dispatch, ownProps) { return { startToDo: () => { dispatch(Actions.start(ownProps.id)); }, finishToDo: () => { dispatch(Actions.finished(ownProps.id)); }, deleteToDo: () => { dispatch(Actions.deleteit(ownProps.id)); } }; } export default connect(mapStateToProps, mapDispatchToProps)(Item);

8.2. list.js

List这个组件与Item刚好相反,它仅读取state数据而不去修改它,所以mapDispatchToProps函数返回空对象,mapStateToProps函数返回对象中的字段将会传递给他的傻瓜组件。

import React from 'react';
import {connect} from 'react-redux';
import Item from './item';

import '../styles/list.scss';

function List({todos, filter}){
    return (
        
{todos.map((todo, key) => { if (todo.status === filter) { return ( ); } else { return null; } })}
); } function mapStateToProps(state) { return { todos: state.todos, filter: state.filter, }; } function mapDispatchToProps(dispatch) { return {}; } export default connect(mapStateToProps,mapDispatchToProps)(List);

8.3. filter.js

Filter有点特别, 他的傻瓜组件需要给容器组建传参,从而点击不同的按钮显示不同的todo记录。这个时候傻瓜组建就没有写成纯函数的形式,而是写成了标准的React组件,并且写了handleClick这个方法,通过点击事件目标对象的id号传递不同的参数给filter这个方法。

import React from 'react';
import {connect} from 'react-redux';
import * as Actions from '../actions';

import '../styles/filter.scss';

class Filter extends React.Component {
    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick(e) {
        this.props.filter(e.target.id.split('-')[1]);
    }
    render() {
        return (
            
To Do
Doing
Done
); } } function mapStateToProps(state, ownProps) { return { filterName: state.filter, }; } function mapDispatchToProps(dispatch) { return { filter: function(status) { dispatch(Actions.filter(status)); } }; } export default connect(mapStateToProps, mapDispatchToProps)(Filter);

8.4. adddialog.js

AddDialog这个组件也很特别,它需要自己暂存一个state,表示用户当前输入的内容,等待用户点击"OK"这个按钮才把数据传出去。所以在这个组件的定义中,你会看到傻瓜组建也在维护自己的state。

import React from 'react';
import * as Actions from '../actions';
import {connect} from 'react-redux';

import '../styles/adddialog.scss';

class AddDialog extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            what: '',
        };
        this.handleAddTODO = this.handleAddTODO.bind(this);
        this.handleWhatChanged = this.handleWhatChanged.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
    }
    handleAddTODO() {
        this.props.addToDo(this.state.what);
        this.props.showAddDialog(false);
        this.setState({
            what: '',
        });
    }
    handleWhatChanged(e) {
        this.setState({
            what: e.target.value,
        });
    }
    handleCancel() {
        this.props.showAddDialog(false);
    }
    render() {
        return (
            
Add