美文网首页让前端飞前端开发笔记
通过复盘todo list小项目,学习react-redux

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

作者: SampleTape | 来源:发表于2019-02-22 18:42 被阅读0次

    前言

    自学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 (
            <div className="Item">
                <div className="ItemAttribute text">
                    {what}
                </div>
                <div className="ItemAttribute" onClick={startToDo}>
                    <img src={start} alt="start" />
                </div>
                <div className="ItemAttribute" onClick={finishToDo}>
                    <img src={finished} alt="finished" />
                </div>
                <div className="ItemAttribute" onClick={deleteToDo}>
                    <img src={deleteit} alt="deleteit" />
                </div>
            </div>
        );
    }
    
    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 (
            <div className={`List ${filter}`}>
                {todos.map((todo, key) => {
                    if (todo.status === filter) {
                        return (
                            <Item id={todo.id} what={todo.what} key={key}></Item>
                        );
                    } else {
                        return null;
                    }
                })}
            </div>
        );
    }
    
    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 (
                <div className="TabTittle">
                    <div className={`title ${this.props.filterName === 'todo' ? 'active' : ''}`}
                         id="filter-todo" 
                         onClick={this.handleClick}>To Do</div>
                    <div className={`title ${this.props.filterName === 'doing' ? 'active' : ''}`}
                         id="filter-doing" 
                         onClick={this.handleClick}>Doing</div>
                    <div className={`title ${this.props.filterName === 'done' ? 'active' : ''}`} 
                         id="filter-done" 
                         onClick={this.handleClick}>Done</div>
                </div>
            );
        }
    }
    
    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 (
                <div className={`AddDialog ${this.props.showadddialog ? 'showAddDialog' : ''}`}>
                    <div className="AddDialogTitle">Add</div>
                    <textarea value={this.state.what} onChange={this.handleWhatChanged} />
                    <div className="ButtonContainer">
                        <div className="CancelButton" onClick={this.handleCancel}>Cancel</div>
                        <div className="OKButton" onClick={this.handleAddTODO}>OK</div>
                    </div>
                </div>
            );
        }   
    }
    
    function mapStateToProps(state) {
        return {
            showadddialog: state.showadddialog,
        };
    }
    
    function mapDispatchToProps(dispatch) {
        return {
            addToDo: function(what) {
                dispatch(Actions.add(what));
            },
            showAddDialog: function(show) {
                dispatch(Actions.showAddDialog(show));
            }
        };
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(AddDialog);
    

    8.5. todolistpanel.js

    最后把所有组件组合到一起。

    import React from 'react';
    import {connect} from 'react-redux';
    import * as Actions from '../actions';
    import List from './list';
    import AddDialog from './adddialog';
    import Filter from './filter';
    import '../styles/todolistpanel.scss';
    
    class ToDoListPanel extends React.Component {
        constructor(props) {
            super(props);
            this.handleClick = this.handleClick.bind(this);
        }
        handleClick() {
            this.props.showAddDialog(true);
        }
        render() {
            return (
                <div className="ToDoListPanel">
                    <Filter></Filter>
                    <div className="TabContent">
                        <List></List>
                    </div>
                    <div className="addButton" onClick={this.handleClick}>
                        +
                        <div className="addButtonTriangle"></div>
                    </div>
                    <AddDialog></AddDialog>
                </div>
            );
        }
    }
    
    function mapStateTpProps(state) {
        return {};
    }
    
    function mapDispachToProps(dispatch) {
        return {
            showAddDialog: function(show) {
                dispatch(Actions.showAddDialog(show));
            }
        };
    }
    
    export default connect(mapStateTpProps, mapDispachToProps)(ToDoListPanel);
    

    8.6 index.js

    最后别忘了修改index.js,感谢Provider为我们提供的store。

    import React from 'react';
    import ReactDOM from 'react-dom';
    import {Provider} from 'react-redux';
    import ToDoListPanel from './components/todolistpanel';
    import store from './store.js';
    import * as serviceWorker from './serviceWorker';
    
    import './index.css';
    
    ReactDOM.render(
        <Provider store={store}>
            <ToDoListPanel />
        </Provider>
        , document.getElementById('root'));
    
    // If you want your app to work offline and load faster, you can change
    // unregister() to register() below. Note this comes with some pitfalls.
    // Learn more about service workers: http://bit.ly/CRA-PWA
    serviceWorker.unregister();
    
    

    相关文章

      网友评论

        本文标题:通过复盘todo list小项目,学习react-redux

        本文链接:https://www.haomeiwen.com/subject/uedeyqtx.html