# Part3-Redux与React Redux

## 预习视频

[【最新】零基础快速入门React 17.x](https://www.bilibili.com/video/BV1U5411T7hY)

> 视频P38-P55

## 今日学习目标

1、掌握redux概念与使用

2、掌握react-redux的使用

## 一、何时用Redux？【了解】

首先明确一点，Redux 是一个有用的架构，但不是非用不可。事实上，大多数情况，你可以不用它，只用 React 就够了。

曾经有人说过这样一句话：

```
"如果你不知道是否需要 Redux，那就是不需要它。"
```

Redux 的创造者 Dan Abramov 又补充了一句：

```
"只有遇到 React 实在解决不了的问题，你才需要 Redux 。"
```

简单说，如果你的UI层非常简单，没有很多互动，Redux 就是不必要的，用了反而增加复杂性。

从项目角度看，如果你出现了以下情况，就可以考虑使用Redux：

```
* 用户的使用方式复杂
* 不同身份的用户有不同的使用方式（比如普通用户和管理员）
* 多个用户之间可以协作
* 与服务器大量交互，或者使用了WebSocket
* View要从多个来源获取数据
```

从组件角度看，如果你的应用有以下场景，可以考虑使用 Redux:

```
* 某个组件的状态，需要共享
* 某个状态需要在任何地方都可以拿到
* 一个组件需要改变全局状态
* 一个组件需要改变另一个组件的状态
```

发生上面情况时，如果不使用 Redux 或者其他状态管理工具，不按照一定规律处理状态的读写，代码很快就会变成一团乱麻。你需要一种机制，可以在同一个地方查询状态、改变状态、传播状态的变化。

总之，不要把 Redux 当作万灵丹，如果你的应用没那么复杂，就没必要用它。另一方面，Redux 只是 Web 架构的一种解决方案，也可以选择其他方案。

## 二、Redux设计思想【了解】

Redux 的设计思想很简单，请记住这两句话：

```
* Web应用是一个状态机，视图与状态是一一对应的。
* 所有的状态，保存在一个对象里面。
```

Redux三大原则：

* 唯一数据源
* 保持只读状态
* 数据改变只能通过纯函数来执行

## 三、流程图剖析【了解】

来看看Redux流程图（Redux Flow）：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63rtbb5j30m70bjt9m.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63rtbb5j30m70bjt9m.jpg)

## 四、案例与API结合【重要】

我们来完成一个TodoList：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63qu7dkj30ak069q2y.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63qu7dkj30ak069q2y.jpg)

### 1、创建项目+安装redux与antd

```js
// 创建项目
npx create-react-app todolist

// 安装redux
yarn add redux

// 安装antd
yarn add antd
```

### 2、页面基本结构

src目录下，只保留：`index.js` 与 `TodoList.js` 两个文件，其余全部删掉。

`index.js` 中：

```jsx
import React from 'react'
import ReactDOM from 'react-dom'
import TodoList from './TodoList'

ReactDOM.render(
    <TodoList />,
    document.getElementById('root')
)
```

`TodoList.js` 中：

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

const inputVal = "写点文字";
const list = [
    "来了来了",
    "第二条信息很刺激",
    "这一条也不错"
]

export default class TodoList extends Component {
    render() {
        return (
            <div>
                <div style={{ margin: '20px' }}>
                    <Input 
                        placeholder="请输入文字" 
                        value={inputVal} 
                        style={{ width: '250px', marginRight: "10px" }} 
                    />
                    <Button type="primary">增加</Button>
                </div>
                <div style={{ margin: '10px', width: '300px' }}>
                    <List bordered dataSource={list} renderItem={item => (<List.Item>{item}</List.Item>)}></List>
                </div>
            </div>
        )
    }
}
```

效果如图：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63sz1pvj30b506gmx7.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63sz1pvj30b506gmx7.jpg)

### 3、Store与Reducer

src目录下创建store文件夹，作为仓库使用。在其中新建 `index.js` 与 `reducer.js` ，分别写：

`store/index.js` 中：

```js
// 引入createStore对象
import { createStore } from 'redux'

// 引入reducer
import reducer from './reducer'

const store = createStore(reducer);
export default store;
```

`reducer.js` 中：

```js
// 定义默认状态值（即默认数据）
const defaultState = {
    // input的文字
    inputVal: "写点文字",
    // 列表项数组
    list: [
        "来了来了",
        "第二条信息很刺激",
        "这一条也不错"
    ]
}

// 导出一个函数，用于返回state
export default (state = defaultState, action) => {
    return state;
}
```

> 这里做个补充：
>
> * **state**: 指的是原始仓库里的状态。
> * **action**: 指的是action新传递的状态。

以上代码中，将原本写在 `TodoList.js` 文件中的数据，拿到reducer中，此时 `TodoList.js` 中就会缺少数据，这时候，我们对它进行修改：

`TodoList.js` 中：

```jsx
...
// 引入store
import store from './store'

export default class TodoList extends Component {
    constructor(props) {
        super(props)
        // 获取仓库中的状态
        this.state = store.getState()
    }
    render() {
        return (
            <div>
                ...
                    <Input 
                        ...
                		{/* 这里修改value的值从仓库中获取 */}
                        value={this.state.inputVal}
                    />
                    ...
                    <List 
                        ...
                		{/* 这里修改dataSource的值从仓库中获取 */}
                        dataSource={this.state.list}
                    ></List>
                ...
            </div>
        )
    }
}
```

> 以上代码中，为了简洁，做了很多省略，目的是方便大家看到核心代码。

### 4、安装Redux DevTools

通常我们会希望在浏览器中调试Redux状态值，因此，将这个程序包直接拽入Chrome的扩展程序：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp65a88ypj31hb0q5wi1.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp65a88ypj31hb0q5wi1.jpg)

就是这个压缩包（注意：不要解压，直接拽入即可！！！）：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63qd7wvj308507vq38.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63qd7wvj308507vq38.jpg)

安装好插件后，关闭当前项目页面，再重新打开页面，然后打开控制台，能看到这个界面，就算安装成功：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp65k0sdej31hc0ka74j.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp65k0sdej31hc0ka74j.jpg)

最后，在 `store/index.js` 中添加这句：

```js
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
```

添加后的 `store/index.js` 整体为：

```jsx
// 引入createStore对象
import { createStore } from 'redux'

// 引入reducer
import reducer from './reducer'

const store = createStore(
    reducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
```

## 五、事件驱动【重要】

### 1、Input的输入事件

当input输入时，我们需要修改输入框的值，于是：

`TodoList.js` 中：

```jsx
export default class TodoList extends Component {
    // 获取仓库中的状态
    state = store.getState()
    render() {
        return (
            ...
                    <Input
                        ...
                        value={this.state.inputVal}
                        onChange={this.changeInput.bind(this)}
                    />
            ...
        )
    }
	// 输入框输入事件
    changeInput(e){
        console.log(e.target.value);	// 得到值
    }
}
```

当然，我们要修改的是store中的值，但唯一能触发store修改值的，是通过Action，因此，我们需要在输入事件中，创建一个action对象：

```js
// 输入框输入事件
changeInput(e){
    console.log(e.target.value);	// 得到值
    // 创建action对象
    const action = {
        type: "changeInputValue",       // type属性是必须要写的，用于校验
        value: e.target.value,          // value代表要修改为什么值
    }
    // 将action用dispatch方法传递给store
    store.dispatch(action);
}
```

此时，由于store只是一个仓库，它会自动将action转发给reducer。我们可以在reducer中打印一下：

```js
// 导出一个函数，用于返回state
export default (state = defaultState, action) => {
    console.log(state, action)	// 对传入进来的值进行打印
    return state;
}
```

结果如下：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63u54a3j30vz0ie411.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp63u54a3j30vz0ie411.jpg)

如此，我们成功了。但我们只是打印了出来，我们真正要修改的是输入框可以看到的文字，于是：

> 我们先判断`type`是不是正确的，如果正确，我们需要从新声明一个变量`newState`。（**记住：Reducer里只能接收state，不能改变state。**）,所以我们声明了一个新变量，然后再次用`return`返回回去。

来看看代码实现：

```js
export default (state = defaultState, action) => {
    console.log(state, action)
    // reducer只能接收state，不能直接对它进行改变
    if(action.type === "changeInputValue"){
        let newState = JSON.parse(JSON.stringify(state));    // 对原本的state做一次深拷贝
        newState.inputVal = action.value;                    // 重新赋值action过来的value
        return newState;
    }
    return state;
}
```

此时，来看看效果：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp65tdkwdg311c0iljv4.gif)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp65tdkwdg311c0iljv4.gif)

可以看到，我在修改输入框的值，控制台显示的状态是改变了，但输入框的文字没变。因为我们没有订阅 。

### 2、订阅

使用订阅可以解决以上问题：

`TodoList.js` 中：

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

// 引入store
import store from './store'

export default class TodoList extends Component {
    constructor(props) {
        super(props)
        // 获取仓库中的状态
        this.state = store.getState()
        store.subscribe(this.storeChange.bind(this)) //订阅Redux的状态
    }
    ...
    storeChange() {
        this.setState(store.getState())
    }
    ...
}
```

现在我们搞定了！

**But！**

这样做说实话不太方便，于是从4.0.5版本开始，非受控组件也可以不用写订阅了。

既然是非受控组件，那么input身上就不能绑定value值。所以，删掉input标签的value属性，以及刚写的订阅，也可以实现。但这就不能给input提供初始值，只能将初始值挂靠在placeholder身上。所以这里不太建议大家删掉这个value和订阅。

### 3、按钮点击事件（增加列表项）

`TodoList.js` 中：

```jsx
export default class TodoList extends Component {
	...
    render(){
        return (
            ...
        	<Button type="primary" onClick={this.handleClick.bind(this)}>增加</Button>
            ...
        )
    }
    // 点击事件
    handleClick(){
        const action = {
            type: "click_fn"
        }
        store.dispatch(action);
    }
	...
}
```

`reducer.js` 中：

```jsx
// 导出一个函数，用于返回state
export default (state = defaultState, action) => {
    // reducer只能接收state，不能直接对它进行改变
    if(action.type === "changeInputValue"){
        ...
    }

    if(action.type === "click_fn"){
        let newState = JSON.parse(JSON.stringify(state));    // 对原本的state做一次深拷贝
        newState.list.unshift(newState.inputVal);            // 插入列表中的第一项
        newState.inputVal = '';                              // 清空输入框
        return newState;
    }
    return state;
}
```

### 4、双击删除列表项

接下来，我们实现一个功能：

> 通过双击列表项，删除该列表项

`TodoList.js` 中：

```jsx
<List 
    bordered 
    dataSource={this.state.list} 
    renderItem={
        (item, index) => (
            {/* 
            	这里注意：下面这个方法可以使用箭头函数，避免this的指向问题，同时如果后期有需要做组件拆分，那就必须使用箭头函数。 
            */}
        	<List.Item onDoubleClick={()=>this.delListItem(index)}>{item}</List.Item>
        )
    }>
</List>

// 双击删除列表项
delListItem(index){
    // 创建action对象
    const action = {
        type: "delListItem",       // type属性是必须要写的，用于校验
        value: index
    }
    // 将action用dispatch方法传递给store
    store.dispatch(action);
}
```

`reducer.js` 中：

```js
// 导出一个函数，用于返回state
export default (state = defaultState, action) => {
    if(action.type === "delListItem"){
        let newState = JSON.parse(JSON.stringify(state));    // 对原本的state做一次深拷贝
        newState.list.splice(action.value, 1)      // 删除指定项
        return newState;
    }
    return state;
}
```

此时，我们实现了双击删除的功能，但我们的reducer有很多if判断，所以我们可以酌情改为 `switch...case`，这完全看你个人心情。

`reducer.js` 的改写：

```js
// 定义默认状态值（即默认数据）
const defaultState = {
    // input的文字
    inputVal: "写点文字",
    // 列表项数组
    list: [
        "来了来了",
        "第二条信息很刺激",
        "这一条也不错"
    ]
}

// 导出一个函数，用于返回state
export default (state = defaultState, action) => {
    let newState = JSON.parse(JSON.stringify(state));    // 对原本的state做一次深拷贝
    // reducer只能接收state，不能直接对它进行改变
    switch (action.type) {
        case "changeInputValue":
            newState.inputVal = action.value;    		// 重新赋值action过来的value
            return newState;
        case "click_fn":
            newState.list.unshift(newState.inputVal);   // 插入列表中的第一项
            newState.inputVal = '';                     // 清空输入框
            return newState;
        case "delListItem":
            newState.list.splice(action.value, 1);      // 删除指定项
            return newState;
        default: 
            break;
    }

    return state;
}
```

### 5、课堂练习

> 使用Redux完成累加功能

Add.js：

```js
import React, { Component } from 'react'
import store from './store'

export default class Add extends Component {
    constructor(p){
        super(p)
        this.state = store.getState()
        store.subscribe(this.storeChange.bind(this))
    }
    render() {
        return (
            <div>
                <h2>{this.state.num}</h2>
                <button onClick={this.handleClick.bind(this)}>按钮</button>
            </div>
        )
    }
    storeChange(){
        this.setState(store.getState())
    }
    handleClick(){
        const action = {
            type: "add_num",
            value: 1
        }
        store.dispatch(action)
    }
}
```

store.js：

```js
import { createStore } from 'redux'
import Reducer from './reducer'

const store = createStore(
    Reducer, 
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)

export default store;
```

reducer.js：

```js
/* 
    reducer只能读取state，不能修改state
*/

// 定义默认的state数据
const defaultState = {
    // 数字
    num: 0
}

// 导出（一开始的时候，state作为形参，其实并没有值，所以让它等于defaultState）
export default (state = defaultState, action) => {
    // 进行一步深拷贝
    var newState = JSON.parse(JSON.stringify(state));

    // 通过判断action中的type，来看是否需要修改
    switch (action.type) {
        case "add_num":
            newState.num+=action.value;
            break;
        default:
            break;
    }
    return newState;
}
```

## 六、ActionTypes【熟悉】

实际开发中，我们会写很多个action，其中的type就会出现很多个，因为每个action中必带一个type，这样就导致我们要找一个bug会比较难，而且不可复用。所以我们将action的type抽离出来，成为 `actionTypes.js` 文件：

```js
export const CLICK_FN = "click_fn";
export const CHANGE_INPUT_VALUE = "changeInputValue";
export const DEL_LIST_ITEM = "delListItem";
```

然后在 `reducer.js` 和 `TodoList.js` 两个文件中引入并修改：

`reducer.js` 中：

```jsx
import { CLICK_FN, CHANGE_INPUT_VALUE, DEL_LIST_ITEM } from './actionTypes'

...

// 导出一个函数，用于返回state
export default (state = defaultState, action) => {
    let newState = JSON.parse(JSON.stringify(state));    // 对原本的state做一次深拷贝
    // reducer只能接收state，不能直接对它进行改变
    switch (action.type) {
        case CHANGE_INPUT_VALUE:
            newState.inputVal = action.value;                    // 重新赋值action过来的value
            return newState;
        case CLICK_FN:
            newState.list.unshift(newState.inputVal);                 // 插入列表中的第一项
            newState.inputVal = '';                              // 清空输入框
            return newState;
        case DEL_LIST_ITEM:
            newState.list.splice(action.value, 1)      // 删除指定项
            return newState;
        default: 
            break;
    }

    return state;
}
```

`TodoList.js` 中：

```jsx
...
import { CLICK_FN, CHANGE_INPUT_VALUE, DEL_LIST_ITEM } from './store/actionTypes'

export default class TodoList extends Component {
    ...
    // 点击事件
    handleClick(){
        const action = {
            type: CLICK_FN
        }
        store.dispatch(action);
    }
    // 订阅调用的事件
    storeChange() {
        this.setState(store.getState())
    }
    // 输入框事件
    changeInput(e) {
        console.log(e.target.value);
        // 创建action对象
        const action = {
            type: CHANGE_INPUT_VALUE,       // type属性是必须要写的，用于校验
            value: e.target.value,          // value代表要修改为什么值
        }
        // 将action用dispatch方法传递给store
        store.dispatch(action);
    }
    // 双击删除列表项
    delListItem(index){
        // 创建action对象
        const action = {
            type: DEL_LIST_ITEM,       // type属性是必须要写的，用于校验
            value: index
        }
        // 将action用dispatch方法传递给store
        store.dispatch(action);
    }
}
```

当然，这还是不够简洁，毕竟有很多重复性的action堆在每个页面中，所以我们需要创建一个 `store/actionCreator.js` ，专门用来写action。

## 七、ActionCreator【熟悉】

`store/actionCreator.js` 中：

```jsx
import { CLICK_FN, CHANGE_INPUT_VALUE, DEL_LIST_ITEM } from './actionTypes'

// 点击事件
export const clickFnAction = () => {
    return {
        type: CLICK_FN       // type属性是必须要写的，用于校验
    }
}

// 输入框事件
export const changeInputValueAction = (val) => {
    return {
        type: CHANGE_INPUT_VALUE,       // type属性是必须要写的，用于校验
        value: val
    }
}

// 双击删除列表项
export const delListItemAction = (val) => {
    return {
        type: DEL_LIST_ITEM,       // type属性是必须要写的，用于校验
        value: val
    }
}
```

将原本所有定义action的代码都搬到这份文件，然后在 `TodoList.js` 中引入这份文件：

```jsx
...
import { clickFnAction, changeInputValueAction, delListItemAction } from './store/actionCreator'

export default class TodoList extends Component {
    ...
    // 点击事件
    handleClick(){
        const action = clickFnAction();
        store.dispatch(action);
    }
    // 订阅调用的事件
    storeChange() {
        this.setState(store.getState())
    }
    // 输入框事件
    changeInput(e) {
        const action = changeInputValueAction(e.target.value);
        // 将action用dispatch方法传递给store
        store.dispatch(action);
    }
    // 双击删除列表项
    delListItem(index){
        const action = delListItemAction(index);
        // 将action用dispatch方法传递给store
        store.dispatch(action);
    }
}
```

## 八、Redux总结

我们来对Redux进行一个总结：

```jsx
1、store必须是唯一的，多个store是坚决不允许，只能有一个store空间
2、只有store能改变自己的内容，Reducer不能改变
3、Reducer必须是纯函数
```

其中，我们解释第三点：

> 很多新手会在reducer每个判断中，去增加Axios请求，但类似于请求这个东西，返回出来的结果都是由后台工程师决定的，你也不知道返回出来的结果是不是函数，所以请大家不要在这些判断中写任何请求、获取时间戳等事件。

## 九、Redux-thunk中间件【了解】

`Redux-thunk` 是Redux最常用的插件。什么时候会用到这个插件呢？比如在`Dispatch`一个`Action`之后，到达`reducer`之前，进行一些额外的操作，就需要用到`middleware`（中间件）。在实际工作中你可以使用中间件来进行日志记录、创建崩溃报告，调用异步接口或者路由。 这个中间件可以使用是`Redux-thunk`来进行增强(当然你也可以使用其它的)，它就是对Redux中`dispatch`的加强。

[![preview](https://tva1.sinaimg.cn/large/008eGmZEgy1gppzasqi37j31400u0tb2.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gppzasqi37j31400u0tb2.jpg)

首先 thunk 来源自 think 的”过去式“ -- 作者非常特别的幽默感。主要意思就是声明一个函数来代替表达式，这样就可以将执行求值操作（evaluation）延迟到所需要的时刻。

### 1、安装插件

```js
npm install --save redux-thunk
```

### 2、配置

> 这里注意，按照官方文档的配置，是没法成功的，这里提供正确的配置方法。

`store/index.js` 中，做如下修改：

```js
// 引入createStore对象
import { createStore, applyMiddleware ,compose } from 'redux'
import thunk from 'redux-thunk'

// 引入reducer
import reducer from './reducer'

// 利用compose创造一个增强函数
const composeEnhancers =   window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}):compose

// 通过增强函数，把thunk引入进来
const enhancer = composeEnhancers(applyMiddleware(thunk))

const store = createStore(reducer, enhancer);    // 创建数据存储仓库
export default store;
```

### 3、使用方法

以往我们在页面中的异步行为（数据请求或定时器），可以迁移到 `actionCreator.js` 中：

```jsx
// 这里以异步累加举例：
export const asyncAddNumFn = (data) => {
  return {type: 'AsyncAddNumFn', value: data}
}
```

页面中：

```js
import {asyncAddNumFn} from './store/actionCreator'

export default class Count extends Component {
    ...
    render() {
        return (
            <div style={{textAlign: 'center'}}>
                <h2>{this.state.num}</h2>
                <Button type="primary" onClick={this.addNumFn.bind(this)}>累加</Button>
            </div>
        )
    }
    addNumFn(){
        // 把action转移到了actionCreator中
        // const action = {
        //     type: "addNumFn",
        //     value: num
        // }
        // 异步得到的数字
        setTimeout(()=>{
          store.dispatch(asyncAddNumFn(num))
        }, 2000)
    }
		...
}
```

这里其实有个比较麻烦的点，我还得先套一个promise，这样代码比较多。如果我可以将这个setTimeout也转移到actionCreator中，那么代码就比较少了。

`actionCreator.js` 中：

```js
// 以高阶函数的形式书写
export const asyncAddNumFn = (data) => (
    return (dispatch) => { 
        setTimeout(() => dispatch({ type: 'AsyncAddNumFn', value: data }), 2000)
    }
)
```

页面中：

```js
addNumFn(){
  // redux-thunk会自动注入dispatch给actionCreators
  store.dispatch(asyncAddNumFn(2))
}
```

除了redux-thunk之外，还有redux-saga等中间件。

## 十、Redux-saga中间件（扩展）

github地址：<https://github.com/axelav/redux-saga>

saga 是英语 **传奇** 的意思。它的思想是 **拦截**。redux-saga 是 redux 一个中间件，用于解决异步问题。redux-saga基于ES6的Generator，大家可以先预习：[Generator与function\*](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*)。

Saga的 `redux-saga/effects` 中有几个关键字：

* fork：创建一个新的进程或者线程，并发发送请求。
* call：发送 api 请求
* put：发送对应的 dispatch，触发对应的 action
* takeEvery：监听对应的 action，每一次 dispatch 都会触发
* takeLatest：监听对应的 action，只会触发最后一次 dispatch
* all：跟 fork 一样，同时并发多个 action，没有顺序。

由于redux-thunk与redux-saga两者作用大致相同，但redux-saga需要基于generator，写起来也较为复杂，这里只做个概念普及，有兴趣的同学可以自行查阅文档学习。

## 十一、React-Redux是什么？【了解】

`React-Redux` 这是一个React生态中常用组件，它可以简化 `Redux` 流程，其实就是简化版的 `Redux`。

## 十二、安装与引入【熟悉】

### 1、安装

```js
npm install --save react-redux
npm install --save redux
```

### 2、引入并使用redux

我们新建一个项目，保留 `App.js` 、 `index.js` 、`store/index.js` 和 `store/reducer.js`，我们通过React-redux来实现累加。

`App.js` 中：

```jsx
import React, { Component } from 'react'
import store from './store'

export default class App extends Component {
    state = store.getState()
    render() {
        return (
            <div>
                <h2>{this.state.count}</h2>
                <button>增加</button>
            </div>
        )
    }
}
```

`index.js` 中：

```js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
    <App />,
    document.getElementById('root')
)
```

`store/index.js` 中：

```js
import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)
export default store
```

`store/reducer.js` 中：

```js
const defaultState = {
    count: 22
}

export default (state = defaultState, action) => {
    return state;
}
```

## 十三、提供器与连接器【重要】

### 1、Provider提供器

`<Provider>` 是一个提供器，只要使用了这个组件，组件里边的其它所有组件都可以使用 `store` 了，这也是`React-redux`的核心组件了。

我们可以对 `index.js` 进行如下修改：

```jsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

import { Provider } from 'react-redux'
import store from './store'

// 定义一个app，来返回Provider组件
const app = (
    <Provider store={store}>
        <App />
    </Provider>
)

ReactDOM.render(
    app,
    document.getElementById('root')
)
```

以上代码中，凡是放在 `<Provider>` 中的组件，都可以获取到store中的数据。

### 2、connect连接器

我们已经可以获取到数据，但需要在组件中设置连接器。

接下来，我们需要到 `App.js` 中进行修改：

```jsx
import React, { Component } from 'react'
// import store from './store'
import {connect} from 'react-redux'  //引入连接器

class App extends Component {
    // state = store.getState()
    render() {
        return (
            <div>
                <h2>{this.props.count}</h2>
                <button>增加</button>
            </div>
        )
    }
}

// stateToProps是一种映射关系，把原来的state映射成组件中的props属性
const stateToProps = (state)=>{
    return {
        count : state.count
    }
}

// 这里不再是导出App，而是导出连接器
export default connect(stateToProps,null)(App);
```

以上主要删除了store的引入，增加了连接器与映射关系。

### 3、事件派发

接下来我们需要点击按钮增加count，其实就是修改store中的count。

`App.js` 中：

```jsx
import React, { Component } from 'react'
// import store from './store'
import {connect} from 'react-redux'  //引入连接器

class App extends Component {
    // state = store.getState()
    render() {
        return (
            <div>
                <h2>{this.props.count}</h2>
                <button onClick={this.props.addCount}>增加</button>
            </div>
        )
    }
}

// stateToProps是一种映射关系，把原来的state映射成组件中的props属性
const stateToProps = (state)=>{
    return {
        count : state.count
    }
}

// dispatchToProps也是一种映射，用于传递并修改数据，这里要返回一个对象并包含一个事件
const dispatchToProps = (dispatch) => {
    return {
        addCount(){
            const action = {
                type: "add_count",
                value: 1
            }
            dispatch(action)
        }
    }
}

// 这里不再是导出App，而是导出连接器
export default connect(stateToProps, dispatchToProps)(App);
```

`reducer.js` 中：

```js
const defaultState = {
    count: 22
}

export default (state = defaultState, action) => {
    if(action.type === "add_count"){
        const newState = JSON.parse(JSON.stringify(state));
        newState.count += action.value;
        return newState;
    }
    return state;
}
```

如此，我们就成功实现了对count的累加。

## 十四、ReactRedux流程图【了解】

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp69hggimj30ya0i1tdp.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gpp69hggimj30ya0i1tdp.jpg)

## 十五、课堂练习【重要】

使用React-redux完成TodoList。

代码：

src/index.js：

```jsx
import React from 'react'
import ReactDOM from 'react-dom'
import Add from './Add'
import TodoList from './TodoList'
import store from './store'
import { Provider } from 'react-redux'
import 'antd/dist/antd.css';

const app = <Provider store={store}>
    <Add />
    <TodoList />
</Provider>

ReactDOM.render(
    app
    , document.getElementById('root')
)
```

src/TodoList.js：

```jsx
import React, { Component } from 'react'
import { Input, Button, List } from 'antd';
import { connect } from 'react-redux'

class TodoList extends Component {
    render() {
        return (
            <div style={{ padding: '20px' }}>
                <Input placeholder="请输入" value={this.props.iptVal} onChange={this.props.handleChange} style={{ width: '400px', marginRight: '10px' }} />
                <Button type="primary" onClick={this.props.handleClick}>添加</Button>
                <List
                    bordered
                    dataSource={this.props.data}
                    style={{width: '470px', marginTop: '20px'}}
                    renderItem={(item, index) => (
                        <List.Item onDoubleClick={this.props.dblClick.bind(this, index)}> {item} </List.Item>
                        // 或者是：
                        <List.Item onDoubleClick={() => this.props.dblClick(index)}> {item} </List.Item>
                    )}
                />
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        data: state.data,
        iptVal: state.iptVal
    }
}

const mapDispatchToProps = (dispatch) => {
    return {
        handleChange(e){
            const action = {
                type: "change_ipt_val",
                value: e.target.value
            }
            dispatch(action)
        },
        handleClick(){
            const action = {
                type: "get_ipt_val"
            }
            dispatch(action)
        },
        dblClick(index){
            const action = {
                type: "del_arr_item",
                value: index
            }
            dispatch(action)
        }
    }
}

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

store/index.js：

```js
import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

export default store;
```

store/reducer.js：

```js
const defaultState = {
    num: 0,
    data: [
        '第一条信息',
        '第二条信息'
    ],
    iptVal: ""
}

export default (state = defaultState, action) => {
    var newState = JSON.parse(JSON.stringify(state))
    switch (action.type) {
        case "add_num":
            newState.num += action.value;
            break;
        case "change_ipt_val":
            newState.iptVal = action.value;
            break;
        case "get_ipt_val":
            newState.data.push(newState.iptVal);
            newState.iptVal = "";
            break;
        case "del_arr_item":
            newState.data.splice(action.value, 1);
            break;
        default:
            break;
    }
    return newState;
}
```

## 十六、作业

使用 React-Redux 完成 TodoList。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gb.akanote.cn/react/chapter3.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
