React学习记录三:Redux


Redux引入

由前面的学习可以知道,React父组件给子组件传值是通过定义属性的方式传递的。大型项目中,当父子组件层级较深时,这种传值的方式显然不切实际,因为两个组件间传值可能依赖中间大量的组件。为方便数据的传递,React必须搭配其它数据层框架使用,而目前使用最多的便是Redux。

Redux的理念是,组件里的数据不再放在组件自身,而是放在一个公共区域(store),这样传递数据只需改变store中数据即可(需要数据的组件从store中取数据即为更新后的数据)。

Redux工作流

Redux工作流组成可以划分为4个部分:React Componet、Action Creators、Store、Reducers。

  • React Component:即组件,使用数据的发起者;
  • Action Creators:可以理解为组件如何使用数据的表达;
  • Store:存储数据的地方;
  • Reducers:可以理解为Store存储数据的记录,也是数据处理的逻辑中心;

当Component更新数据时,首先创建Action,然后传递给Store,Store将当前存储数据和接收的Action传递给Reducer处理。组件在订阅store后,每次store改变可以更新自己的state。

Redux设计原则:

  • store唯一;
  • 只有store能够改变自己的内容(并不是reducer);
  • Reducer必须是纯函数(给定固定输入就固定输出);

使用antd实现TodoList初试Redux

antdesign是React的UI组件库,可以帮助我们快速实现页面布局。查看官方文档,通过npm install antd --save进行安装。

antd使用也比较简单,首先通过import 'antd/dist/antd.css';引入样式,然后根据官网中各组件的样式及事例代码引用即可。样式代码如下:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List, Typography } from 'antd';

const data = [
    'Racing car sprays burning fuel into crowd.',
    'Japanese princess to wed commoner.',
    'Australian walks 100km after outback crash.',
    'Man charged over missing wedding girl.',
    'Los Angeles battles huge wildfires.',
];

class TodoList extends Component {
    render() {
        return (
            <div style=>
                <div>
                    <Input placeholder="todo info" style=/>
                    <Button type="primary">提交</Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={data}
                    renderItem={item => (
                        <List.Item>
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }
}

export default TodoList;

此时页面已经展示出比较好看的TodoList布局。下面结合Redux实现功能。

在github搜索redux可以查看redux官网。通过npm install --save redux进行redux安装。Redux使用步骤:

  1. 创建Store

在src目录下创建文件夹,名为store,store下创建index.js:

import { createStore } from 'redux';

const store = createStore();

export default store;

创建store时需要reducer,因此再创建reducer.js:

const defaultState = {
    inputValue: '',
    list: []
}

// 接收两个参数。state表示store存储的数据。提供默认值
export default (state = defaultState, action) => {
    return state;
};

修改store,创建时传reducer:

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

const store = createStore(reducer);  // 创建store时传递reducer

export default store;

至此store已经创建完成。

  1. 使用store里数据填充页面:
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List } from 'antd';
import store from './store';

class TodoList extends Component {
    constructor(props) {
        super(props);
        // store提供getState方法获取其中数据
        this.state = store.getState();
    }
    render() {
        return (
            <div style=>
                <div>
                    <Input value={this.state.inputValue} placeholder="todo info" style=/>
                    <Button type="primary">提交</Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.state.list}  // 使用自己的state
                    renderItem={item => (
                        <List.Item>
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }
}

export default TodoList;
  1. 改变数据:
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List } from 'antd';
import store from './store';  // 引入目录下的index.js时可以省略

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this)
    }
    render() {
        return (
            <div style=>
                <div>
                    <Input
                        value={this.state.inputValue}
                        placeholder="todo info"
                        style=
                        onChange={this.handleInputChange}
                    />
                    <Button type="primary">提交</Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.state.list}  // 使用自己的state
                    renderItem={item => (
                        <List.Item>
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    handleInputChange(e) {
        // 首先创建action
        // action是一个对象。其中type描述要做的事情
        const action = {
            type: 'change_input_value',
            value: e.target.value
        }
        // 然后将action传递给store
        // store提供dispatch方法可以实现传递action
        store.dispatch(action);
        // store会自动将当前数据和action传递给reducer
    }
}

export default TodoList;
const defaultState = {
    inputValue: '',
    list: []
}

// 此时明白,state是上次store中存储的数据,action是每次store传递过来的action
// reducer作用就是拿到当前状态数据和action后,根据action告诉store数据应该如何更新
export default (state = defaultState, action) => {
    if (action.type === 'change_input_value') {
        // 需要注意:reducer绝不能修改原state
        const newState = JSON.parse(JSON.stringify(state));  // 深拷贝
        newState.inputValue = action.value;
        // store会拿reducer返回的新数据替换原有数据
        return newState;
    }
    return state;
};

此时通过redux调试窗口可以看到store中数据会随着input值变化 ,但是页面中并不更新,这是因为组件并没有更新数据。因此需要添加订阅:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List } from 'antd';
import store from './store';

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        // 需要订阅store。store数据改变时,其中函数自动执行
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <div style=>
                <div>
                    <Input
                        value={this.state.inputValue}
                        placeholder="todo info"
                        style=
                        onChange={this.handleInputChange}
                    />
                    <Button type="primary">提交</Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.state.list}
                    renderItem={item => (
                        <List.Item>
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    handleInputChange(e) {
        const action = {
            type: 'change_input_value',
            value: e.target.value
        }
        store.dispatch(action);
    }

    handleStoreChange() {
        // 更新组件自己的store
        this.setState(store.getState());
    }
}

export default TodoList;

此时可以正常输入文本。

完成列表添加功能:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List } from 'antd';
import store from './store';

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <div style=>
                <div>
                    <Input
                        value={this.state.inputValue}
                        placeholder="todo info"
                        style=
                        onChange={this.handleInputChange}
                    />
                    <Button
                        type="primary"
                        onClick={this.handleBtnClick}
                    >
                        提交
                    </Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.state.list}
                    renderItem={item => (
                        <List.Item>
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    handleInputChange(e) {
        const action = {
            type: 'change_input_value',
            value: e.target.value
        }
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = {
            type: 'add_item',
        };
        store.dispatch(action);
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;
const defaultState = {
    inputValue: '',
    list: []
}

export default (state = defaultState, action) => {
    if (action.type === 'change_input_value') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    if (action.type === 'add_item') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list = [...newState.list, newState.inputValue];
       	// newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
    }
    return state;
};

此时TodoList已经可以正常添加item。

完成列表删除功能:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List } from 'antd';
import store from './store';

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <div style=>
                <div>
                    <Input
                        value={this.state.inputValue}
                        placeholder="todo info"
                        style=
                        onChange={this.handleInputChange}
                    />
                    <Button
                        type="primary"
                        onClick={this.handleBtnClick}
                    >
                        提交
                    </Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.state.list}
                    // 渲染元素时除item外还可以传index
                    renderItem={(item, index) => (
                        <List.Item
                            onClick={this.handleItemDelete.bind(this, index)}
                        >
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    handleInputChange(e) {
        const action = {
            type: 'change_input_value',
            value: e.target.value
        }
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = {
            type: 'add_item',
        };
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = {
            type: 'delete_item',
            index
        }
        store.dispatch(action)
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;
const defaultState = {
    inputValue: '',
    list: []
}

export default (state = defaultState, action) => {
    if (action.type === 'change_input_value') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    if (action.type === 'add_item') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
    }
    if (action.type === 'delete_item') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.splice(action.index, 1);
        return newState;
    }
    return state;
};

ActionTypes的拆分

为防止store中和reducer中actionType因为书写不一致而造成不必要的低级错误,我们将actionTypes提出。

在store文件夹下创建actionTypes.js文件:

export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_ITEM = 'add_item';
export const DELETE_ITEM = 'delete_item';

在TodoList.js和reducers.js中引入actionTypes:

import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM } from './store/actionTypes'
import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM } from './actionTypes'

使用actionCreator统一创建action

在组件代码中书写action不是一种合理的方式,推荐将创建action统一书写至单独的文件中。

在store文件夹下创建actionCreators.js文件:

import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM } from './actionTypes'

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
});

export const getAddItemAction = () => ({
    type: ADD_ITEM
});

export const getDeleteItemAction = (index) => ({
    type: DELETE_ITEM,
    index
});
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input, Button, List } from 'antd';
import store from './store';
import { getInputChangeAction, getAddItemAction, getDeleteItemAction } from './store/actionCreators'

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <div style=>
                <div>
                    <Input
                        value={this.state.inputValue}
                        placeholder="todo info"
                        style=
                        onChange={this.handleInputChange}
                    />
                    <Button
                        type="primary"
                        onClick={this.handleBtnClick}
                    >
                        提交
                    </Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.state.list}
                    // 渲染元素时除item外还可以传index
                    renderItem={(item, index) => (
                        <List.Item
                            onClick={this.handleItemDelete.bind(this, index)}
                        >
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        );
    }

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value)
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = getDeleteItemAction(index);
        store.dispatch(action)
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;

UI组件和容器组件的拆分

在之前的TodoList.js文件中,与渲染相关的元素部分与逻辑部分在同一组件,建议将比较复杂的组件拆分成负责页面渲染的UI组件和负责逻辑部分的容器组件。

新建TodoListUI.js,将原TodoList.js中render函数中内容移至该文件,进行拆分:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store';
import { getInputChangeAction, getAddItemAction, getDeleteItemAction } from './store/actionCreators'
import TodoListUI from './TodoListUI'

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.handleItemDelete = this.handleItemDelete.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <TodoListUI
                inputValue={this.state.inputValue}
                handleInputChange={this.handleInputChange}
                handleBtnClick={this.handleBtnClick}
                handleItemDelete={this.handleItemDelete}
                list={this.state.list}
            />
        );
    }

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value)
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = getDeleteItemAction(index);
        store.dispatch(action)
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;
import React, { Component } from 'react';
import { Input, Button, List } from 'antd';

class TodoListUI extends Component {
    render() {
        return (
            <div style=>
                <div>
                    <Input
                        value={this.props.inputValue}
                        placeholder="todo info"
                        style=
                        onChange={this.props.handleInputChange}
                    />
                    <Button
                        type="primary"
                        onClick={this.props.handleBtnClick}
                    >
                        提交
                    </Button>
                </div>
                <List
                    style=
                    bordered
                    dataSource={this.props.list}
                    renderItem={(item, index) => (
                        <List.Item
                            onClick={()=>{this.props.handleItemDelete(index)}}
                        >
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        )
    }
}

export default TodoListUI;

无状态组件

TodoListUI.js目前只有一个render函数。对于这种只有render函数的组件,可以通过无状态组件的方式定义。无状态组件其实就是一个函数,接收props,返回jsx:

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

const TodoListUI = (props) => {
    return (
        <div style=>
            <div>
                <Input
                    value={props.inputValue}
                    placeholder="todo info"
                    style=
                    onChange={props.handleInputChange}
                />
                <Button
                    type="primary"
                    onClick={props.handleBtnClick}
                >
                    提交
                </Button>
            </div>
            <List
                style=
                bordered
                dataSource={props.list}
                renderItem={(item, index) => (
                    <List.Item
                        onClick={()=>{props.handleItemDelete(index)}}
                    >
                        {item}
                    </List.Item>
                )}
            />
        </div>
    )
};

export default TodoListUI;

相比于普通组件,无状态组件性能更佳。因为类中实际上有生命周期函数等其它函数,而函数则只有render方法。

Redux配合axios

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store';
import { getInputChangeAction, getAddItemAction, getDeleteItemAction, initListAction } from './store/actionCreators'
import TodoListUI from './TodoListUI'
import axios from 'axios';

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.handleItemDelete = this.handleItemDelete.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <TodoListUI
                inputValue={this.state.inputValue}
                handleInputChange={this.handleInputChange}
                handleBtnClick={this.handleBtnClick}
                handleItemDelete={this.handleItemDelete}
                list={this.state.list}
            />
        );
    }

    componentDidMount() {
        axios.get('/api/todolist').then(
            (res)=>{
                const data = res.data;
                const action = initListAction(data);
                store.dispatch(action);
            }
        );
    }

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value)
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = getDeleteItemAction(index);
        store.dispatch(action)
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;
import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, INIT_LIST } from './actionTypes'

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
});

export const getAddItemAction = () => ({
    type: ADD_ITEM
});

export const getDeleteItemAction = (index) => ({
    type: DELETE_ITEM,
    index
});

export const initListAction = (data) => ({
    type: INIT_LIST,
    data
});
import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, INIT_LIST } from './actionTypes'

const defaultState = {
    inputValue: '',
    list: []
}

export default (state = defaultState, action) => {
    if (action.type === CHANGE_INPUT_VALUE) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    if (action.type === ADD_ITEM) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
    }
    if (action.type === DELETE_ITEM) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.splice(action.index, 1);
        return newState;
    }
    if (action.type === INIT_LIST) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list = action.data;
        return newState;
    }
    return state;
};
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_ITEM = 'add_item';
export const DELETE_ITEM = 'delete_item';
export const INIT_LIST = 'init_list';

使用Redux-thunk中间件发送ajax

上面redux配合axios发送ajax时,发送ajax的逻辑也放在了组件代码中,当项目复杂时,建议将异步请求及复杂逻辑至于单独文件中,redux-thunk中间件可以使得将复杂逻辑放至action中做处理。

github搜索redux-thunk,查看官方文档,使用npm install redux-thunk --save进行安装,同时修改创建store时代码:

import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
        window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

const enhancer = composeEnhancers(
    applyMiddleware(thunk),
);

const store = createStore(reducer, enhancer);

export default store;

使用redux-thunk创建store后,创建action就不仅只能够返回对象了,还可以返回函数,在store dispatch该action时,返回的函数自动执行:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store';
import { getInputChangeAction, getAddItemAction, getDeleteItemAction, getTodoList } from './store/actionCreators'
import TodoListUI from './TodoListUI'

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.handleItemDelete = this.handleItemDelete.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <TodoListUI
                inputValue={this.state.inputValue}
                handleInputChange={this.handleInputChange}
                handleBtnClick={this.handleBtnClick}
                handleItemDelete={this.handleItemDelete}
                list={this.state.list}
            />
        );
    }

    componentDidMount() {
        const action = getTodoList();
        // 这里调用dispatch方法时,action返回的函数会被调用
        store.dispatch(action);
    }

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value)
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = getDeleteItemAction(index);
        store.dispatch(action)
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;
import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, INIT_LIST } from './actionTypes'
import axios from "axios";

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
});

export const getAddItemAction = () => ({
    type: ADD_ITEM
});

export const getDeleteItemAction = (index) => ({
    type: DELETE_ITEM,
    index
});

export const initListAction = (data) => ({
    type: INIT_LIST,
    data
});

export const getTodoList = () => {
    // 返回的函数接收dispatch参数,下面直接使用dispatch方法即可
    return (dispatch) => {
        axios.get('/api/todolist').then(
            (res)=>{
                const data = res.data;
                const action = initListAction(data);
                dispatch(action);
            }
        );
    };
};

实际上,拆分代码不仅为了代码整洁性和方便管理,合理的拆分代码,有助于前端完成自动化测试。

中间件理解

在未使用中间件时,action直接由store通过dispatch方法发送给reducer,而使用中间件后,如使用redux-thunk后,action不仅可以返回对象,还可以返回函数,此时在store通过dispatch将action传递给store前还可以调用函数处理额外逻辑,因此,中间件的中间实际上指的是action和store之间。

注意中间件是Redux中的概念,而不是React。

<img width=50% src=”/Users/gp/Desktop/github_projects/AdoredU.github.io/_posts/2020-01-20-React-3.assets/image-20200121171906669.png” />

Redux-saga中间件使用

同redux-thunk一样,Redux-saga也是一个用来处理异步逻辑的中间件。

在github搜索redux-saga,查看文档,使用npm install --save redux-saga安装redux-saga。将代码恢复至使用thunk前,创建store时使用saga中间件:

import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
// 使用saga将异步逻辑单独存储于一个文件
import todoSagas from './sagas';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
        window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()

const enhancer = composeEnhancers(
    applyMiddleware(sagaMiddleware),
);

const store = createStore(reducer, enhancer);
// then run the saga
sagaMiddleware.run(todoSagas);

export default store;

Redux-saga将异步逻辑单独储存于一个文件,这里新建saga.js:

// saga中必须导出generator函数
function* mySaga() {
}

export default mySaga;

完成代码:

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store';
import {getInputChangeAction, getAddItemAction, getDeleteItemAction, getInitList} from './store/actionCreators'
import TodoListUI from './TodoListUI'

class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.handleItemDelete = this.handleItemDelete.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    render() {
        return (
            <TodoListUI
                inputValue={this.state.inputValue}
                handleInputChange={this.handleInputChange}
                handleBtnClick={this.handleBtnClick}
                handleItemDelete={this.handleItemDelete}
                list={this.state.list}
            />
        );
    }

    componentDidMount() {
        const action = getInitList();
        // 使用saga时,此时除在reducer中接收action时,还可以在sagas.js中接收action
        store.dispatch(action);
    }

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value)
        store.dispatch(action);
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = getDeleteItemAction(index);
        store.dispatch(action)
    }

    handleStoreChange() {
        this.setState(store.getState());
    }
}

export default TodoList;
import { CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, INIT_LIST, GET_INIT_LIST } from './actionTypes'

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
});

export const getAddItemAction = () => ({
    type: ADD_ITEM
});

export const getDeleteItemAction = (index) => ({
    type: DELETE_ITEM,
    index
});

export const initListAction = (data) => ({
    type: INIT_LIST,
    data
});

// 使用saga时还是返回对象
export const getInitList = () => ({
    type: GET_INIT_LIST
});
import {takeEvery, put} from 'redux-saga/effects';
import {GET_INIT_LIST} from "./actionTypes";
import axios from 'axios';
import {initListAction} from "./actionCreators";

// 这里也推荐使用generator函数
function* getInitList() {
    try {
        const res = yield axios.get('/api/todolist');
        const action = initListAction(res.data);
        yield put(action);
    }catch (e) {
        console.log('todolist接口请求失败!')
    }
}

// saga中必须导出generator函数
function* mySaga() {
    // takeEvery:捕捉每一个actionType,然后调用getInitList方法
    yield takeEvery(GET_INIT_LIST, getInitList);
}

export default mySaga;
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_ITEM = 'add_item';
export const DELETE_ITEM = 'delete_item';
export const INIT_LIST = 'init_list';
export const GET_INIT_LIST = 'get_init_list';

React-Redux的使用

React-Redux是一个第三方模块,它帮助我们在React中更加方便的使用Redux。github搜索react-redux即可查看官方文档,通过npm install --save react-redux安装React-Redux。

把src文件夹下除index.js外文件全部删除,重新编写:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'
// 引入Provider
import {Provider} from 'react-redux';
import store from './store';

const App = (
    // provider将store提供给内部所有组件(使用connect API)
    <Provider store={store}>
        <TodoList/>
    </Provider>
)

// 这里渲染App代替TodoList
ReactDOM.render(App, document.getElementById('root'));

TodoList.js:

import React, {Component} from 'react';
import {connect} from 'react-redux';

class TodoList extends Component {
    // 使用react-redux后,无需再在构造函数里
    // constructor(props) {
    //     super(props);
    //     this.state = store.getState();
    // }
    render() {
        return (
            <div>
                <div>
                    {/*映射后这里应该为props*/}
                    <input
                        value={this.props.inputValue}
                        // 定义dipatch映射后无需这种方式
                        // onChange={this.handleInputChange.bind(this)}
                        onChange={this.props.changeInputValue}
                    />
                    <button>提交</button>
                </div>
                <ul>
                    <li>hello</li>
                </ul>
            </div>
        );
    }
}

// 连接方式
// 把store数据映射到组件props。接收参数state
const mapStateToProps = (state) => {
    return {
        inputValue: state.inputValue,
    }
};

// 把store的dispatch方法挂载到props
const mapDispatchToProps = (dispatch) => {
    return {
        changeInputValue(e) {
            const action = {
                type: 'change_input_value',
                value: e.target.value,
            }
            dispatch(action);
        }
    }
};

// 不再直接导出TodoList,而是connect方法调用
// 第一个参数:参数映射
// 第二个参数:dispatch映射
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

store/index.js:

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

const store = createStore(reducer);

export default store;

store/reducer.js:

const defaultState = {
    inputValue: 'hello',
    list: []
}

export default (state = defaultState, action) => {
    if (action.type === 'change_input_value') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    return state;
};

完成TodoList功能:

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'
import {Provider} from 'react-redux';
import store from './store';

const App = (
    <Provider store={store}>
        <TodoList/>
    </Provider>
)

ReactDOM.render(App, document.getElementById('root'));

TodoList.js:

import React from 'react';
import {connect} from 'react-redux';
import {getInputChangeAction, getAddItemAction, getDeleteItemAction} from './store/actionCreator';

const TodoList = (props) => {
    const {inputValue, list, changeInputValue, handleClick, handleDelete} = props;
    return (
        <div>
            <div>
                <input
                    value={inputValue}
                    onChange={changeInputValue}
                />
                <button
                    onClick={handleClick}
                >
                    提交
                </button>
            </div>
            <ul>
                {
                    list.map((item, index) => {
                        return (
                            <li key={index}
                                onClick={() => {
                                    handleDelete(index)
                                }}
                            >
                                {item}
                            </li>
                        )
                    })
                }
            </ul>
        </div>
    );
};

const mapStateToProps = (state) => {
    return {
        inputValue: state.inputValue,
        list: state.list,
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        changeInputValue(e) {
            dispatch(getInputChangeAction(e.target.value));
        },
        handleClick() {
            dispatch(getAddItemAction());
        },
        handleDelete(index) {
            dispatch(getDeleteItemAction())
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

sotre/index.js:

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

const store = createStore(reducer);

export default store;

store/actionTypes.js:

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

const store = createStore(reducer);

export default store;

store/actionCreator.js:

import {CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM} from "./actionTypes";

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
});

export const getAddItemAction = () => ({
    type: ADD_ITEM
});

export const getDeleteItemAction = (index) => ({
    type: DELETE_ITEM,
    index
});

store/reducers.js:

import {CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM} from './actionTypes';

const defaultState = {
    inputValue: '',
    list: []
}

export default (state = defaultState, action) => {
    if (action.type === CHANGE_INPUT_VALUE) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    if (action.type === ADD_ITEM) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
    }
    if (action.type === DELETE_ITEM) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.splice(action.index, 1);
        return newState;
    }
    return state;
};