Redux is a predictable state container for JavaScript apps.
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
这是 redux 官网的原话
我最早接触的框架是 Vue,这个图我自己觉得和 vuex 好像有一些相似的地方。
我们今天就写一个TODOList 的小 demo ,来入门学习一下 redux
(为了效果好看,使用 antd-ui 组件库)
1. 初始化项目
安装官方脚手架之后
mkdir ReduxDemo
cd ReduxDemo
create-react-app demo
cd demo
npm start
对的,就是这么简单
不出意外,就能看到平常你看见的样子了。
项目精简
src 目录下只留下一个 index.js,新建一个 TodoList.js
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'ReactDOM.render(<TodoList />,document.getElementById('root')
);
TodoList.js
import React, { Component } from 'react';
class TodoList extends Component {render() {return (<div>Hello World</div>);}
}
export default TodoList;
安装UI组件库
(不想安装也行,就是样子很不好看,功能没有多大影响)
官网地址 https://ant.design/docs/react/use-with-create-react-app-cn
npm install antd --save
2. 编写基本界面
TodoList.js 引入
import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input } from 'antd'class TodoList extends Component {render() {return (<div><div><Input placeholder='请输入' style={{ width: '250px' }} /></div></div>);}
}
export default TodoList;
不出意外,你得页面上面,会出现一个 input 输入框
增加按钮
import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button } from 'antd'class TodoList extends Component {render() {return (<div style={{margin:'10px'}}><div><Input placeholder='请输入' style={{ width: '250px' }} /><Button type="primary">添加</Button></div></div>);}
}
export default TodoList;
一个按钮就可以了。接下来是 List 列表(先使用假数据)
在 class 外面声明一个 list 数组
const data=['react','vue','ang'
]
增加 List 组件
import { Input, Button, List } from 'antd'
<div style={{ margin: '10px' }}><div><Input placeholder='请输入' style={{ width: '300px' }} /><Button type="primary">添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={data}renderItem={item => (<List.Item>{item}</List.Item>)}/></div>
</div>
3. 正式进入 redux,创建 store 和 reducer
Redux工作流程中有四个部分,最重要的就是store这个部分,因为它把所有的数据都放到了store中进行管理。在编写代码的时候,因为重要,所以要优先编写store。
想使用,先安装
npm install --save redux
在 src 目录下,创建 store 文件夹。文件夹下创建一个index.js文件。index.js就是整个项目的store文件
index.js
import { createStore } from 'redux'
const store = createStore()
export default store
从 redux 中导出 createStore ,使用 createStore方法创建一个 store仓库,之后进行导出
这个仓库创建好了,需要一位管理员来管理这个仓库,Reducers 就是这么一位天选之子。同级目录下创建 reducer.js 文件
reducer.js
const defaultState = {}
export default (state = defaultState, action) => {return state
}
这位管理员有一个本子 state,里面记载着仓库的一切。同时我们给这个本子添加一个初始的内容 defaultState,返回这个本子上面的内容
把reducer引入到store中,再创建store时,以参数的形式传递给store。
import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store
转移数据
把刚刚在页面中定义的 list 数组迁移到 reducer 中,给这个记录着仓库一切的本子赋上初始值
const defaultState = {inputValue: '请输入',list: ['react','vue','ang']
}
export default (state = defaultState, action) => {return state
}
获取数据
仓库里有东西,就可以获取了。
todoList
组件要使用store
,就在src/TodoList.js
文件夹中,进行引入。在constructor
中打印store
里面的数据
constructor(props) {super(props);console.log(store.getState())}
import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'
import store from './store/index'class TodoList extends Component {constructor(props) {super(props);console.log(store.getState())this.state = store.getState()}render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.state.inputValue} style={{ width: '300px' }}/><Button type="primary">添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.state.list}renderItem={item => (<List.Item>{item}</List.Item>)}/></div></div>);}
}
export default TodoList;
4. TodoList 的变化引起 redux 变化
我们给输入框增加一个 onChange 事件
<Inputplaceholder={this.state.inputValue}onChange={this.changeInputValue}style={{ width: '300px' }}
/>
定义这个事件,同时在 constructor 中进行this的绑定,修改this的指向。
constructor(props) {super(props);this.state = store.getState()this.changeInputValue = this.changeInputValue.bind(this)
}
...changeInputValue(e) {console.log(e.target.value)
}
在输入框中,输入文字,可以看到控制台输出的相应的文字。好的,这就成功了。
创建 Action
想改变 Redux 里边 State 的值就要创建 Action 了。Action 就是一个对象,这个对象一般有两个属性,第一个是对 Action 的描述,第二个是要改变的值。
changeInputValue(e) {const action = {type: 'change_input_value',value: e.target.value}
}
派发 dispatch
action就创建好了,通过dispatch()方法传递给store
changeInputValue(e) {const action = {type: 'change_input_value',value: e.target.value}store.dispatch(action)
}
(这就相当于我要做一件事,做什么事呢?我想告诉仓库我想改变 input 输入框里面的值,然后我把值传递给你,怎么弄那是你的事,之后通过 dispatch 把这件事告诉仓库)
😌😌😌😌
store 的处理
store 只是一个仓库,它并没有管理能力,它会把接收到的 action 自动转发给 Reducer。
Reducer 已经拿到了原来的数据和新传递过来的数据,现在要作的就是改变store里的值。我们先判断type是不是正确的,如果正确,我们需要从新声明一个变量newState。
(记住:Reducer里只能接收state,不能改变state。)
, 所以我们声明了一个新变量,然后再次用return返回回去。
const defaultState = {inputValue: '请输入',list: ['react','vue','ang']
}
export default (state = defaultState, action) => {if (action.type === 'changeInput') {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}return state
}
(当reducer 知道你改变 input 输入框的值,当判断类型正确的时候,它会先拷贝出一份,然后把你传递给它的值赋值给仓库里面的值, 同时返回)
组件更新
TodoList.js
constructor(props) {super(props);this.state = store.getState()this.changeInputValue = this.changeInputValue.bind(this)this.storeChange = this.storeChange.bind(this)//订阅Redux的状态store.subscribe(this.storeChange)
}
storeChange() {this.setState(store.getState())
}
给 button 添加事件
<Button type="primary" onClick={this.clickBtn}>添加</Button>
clickBtn() {const action = {type: 'addItem'}store.dispatch(action)
}
if (action.type === 'addItem') {let newState = JSON.parse(JSON.stringify(state))newState.list.push(newState.inputValue)newState.inputValue = ''return newState
}
不出意外的话,当你输入文字,点击添加的时候,下面的 list 就会出现相应的文字
删除 TodoList-item
给每一个 ListItem 添加一个 deleteItem 事件
<ListbordereddataSource={this.state.list}renderItem={(item, index) => (<List.Item onClick={this.deleteItem.bind(this, index)}>{item}</List.Item>)}
/>
deleteItem(index) {console.log(index)const action = {type: 'deleteItem',index}store.dispatch(action)
}
点击出现相对应的序号,就是成功了。
if (action.type === 'deleteItem') {let newState = JSON.parse(JSON.stringify(state))newState.list.splice(action.index,1)return newState
}
点击各项,成功删除
来到这一步,我们就成功实现了。完美撒花 🎉🎉🎉🎉🎉🎉
5.代码优化
写 Redux Action 的时候,我们写了很多 Action 的派发,产生了很多 Action Types,如果需要Action的地方我们就自己命名一个Type,会出现两个基本问题:
- 这些 Types 如果不统一管理,不利于大型项目的服用,设置会长生冗余代码。
- 因为 Action 里的 Type,一定要和 Reducer 里的 type一一对应在,所以这部分代码或字母写错后,浏览器里并没有明确的报错,这给调试带来了极大的困难。
store 文件夹下,新建一个 actionTypes.js
export const CHANGE_INPUT = 'changeInput'
export const ADD_ITEM = 'addItem'
export const DELETE_ITEM = 'deleteItem'
TodoList.js
//..
import {CHANGE_INPUT,ADD_ITEM,DELETE_ITEM,
} from './store/actionTypes'class TodoList extends Component {constructor(props) {// ...}render() {//....}storeChange() {this.setState(store.getState())}changeInputValue(e) {const action = {type: CHANGE_INPUT,value: e.target.value}store.dispatch(action)}clickBtn() {const action = {type: ADD_ITEM}store.dispatch(action)}deleteItem(index) {console.log(index)const action = {type: DELETE_ITEM,index}store.dispatch(action)}
}
export default TodoList;
reducer.js
import {CHANGE_INPUT,ADD_ITEM,DELETE_ITEM
} from './actionTypes'const defaultState = {inputValue: '请输入',list: ['react','vue','ang']
}
export default (state = defaultState, action) => {if (action.type === CHANGE_INPUT) {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}if (action.type === ADD_ITEM) {let newState = JSON.parse(JSON.stringify(state))newState.list.push(newState.inputValue)newState.inputValue = ''return newState}if (action.type === DELETE_ITEM) {let newState = JSON.parse(JSON.stringify(state))newState.list.splice(action.index,1)return newState}return state
}
把所有的Redux Action放到一个文件里进行管理。
在 store 文件夹下面,建立 actionCreators.js
import {CHANGE_INPUT,ADD_ITEM,DELETE_ITEM
} from './actionTypes'export default changeInputAction = (value) => {type: CHANGE_INPUT,value
}
TodoList.js
import {changeInputAction
} from './store/actionCreators'
changeInputValue(e) {const action = changeInputAction(e.target.value)store.dispatch(action)
}
改造剩下的两个
import {CHANGE_INPUT, ADD_ITEM,DELETE_ITEM
} from './actionTypes'export const changeInputAction = (value)=>({type:CHANGE_INPUT,value
})export const addItemAction = ()=>({type:ADD_ITEM
})export const deleteItemAction = (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/index'import {changeInputAction,addItemAction,deleteItemAction
} from './store/actionCreators'class TodoList extends Component {constructor(props) {super(props);this.state = store.getState()this.changeInputValue = this.changeInputValue.bind(this)this.storeChange = this.storeChange.bind(this)store.subscribe(this.storeChange)}render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.state.inputValue}onChange={this.changeInputValue}value={this.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={this.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.state.list}renderItem={(item, index) => (<List.Item onClick={this.deleteItem.bind(this, index)}>{item}</List.Item>)}/></div></div>);}storeChange() {this.setState(store.getState())}changeInputValue(e) {const action = changeInputAction(e.target.value)store.dispatch(action)}clickBtn() {const action = addItemAction()store.dispatch(action)}deleteItem(index) {const action = deleteItemAction(index)store.dispatch(action)}
}
export default TodoList;
改造到这里代码依然正常运行。此时我们可以进一步改造,将 UI部分与逻辑部分开
src 目录下新建 TodoListUI.js
import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'class TodoListUI extends Component {render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.state.inputValue}onChange={this.changeInputValue}value={this.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={this.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.state.list}renderItem={(item, index) => (<List.Item onClick={this.deleteItem.bind(this, index)}>{item}</List.Item>)}/></div></div>);}
}
export default TodoListUI;
TodoList.js
import TodoListUI from './TodoListUI'
render() {return (<TodoListUI />);
}
当然现在页面报错,我们接着进行改造
TodoList.js
<TodoListUI inputValue={this.state.inputValue}list={this.state.list}changeInputValue={this.changeInputValue}clickBtn={this.clickBtn}deleteItem={this.deleteItem}
/>
TodoListUI.js
render() {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={this.props.inputValue}onChange={this.props.changeInputValue}value={this.props.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={this.props.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={this.props.list}renderItem={(item, index) => (<List.Item onClick={this.props.deleteItem.bind(this, index)}>{item}</List.Item>)}/></div></div>);
}
把UI组件改成无状态组件可以提高程序性能
TodoListUI.js
import React, { Component } from 'react';
import 'antd/dist/antd.css'
import { Input, Button, List } from 'antd'const TodoListUI = (props) => {return (<div style={{ margin: '10px' }}><div><Inputplaceholder={props.inputValue}onChange={props.changeInputValue}value={props.inputValue}style={{ width: '300px' }}/><Button type="primary" onClick={props.clickBtn}>添加</Button></div><div style={{ width: '300px' }}><ListbordereddataSource={props.list}renderItem={(item, index) => (<List.Item onClick={() => {props.deleteItem(index)}}>{item}</List.Item>)}/></div></div>);
}export default TodoListUI;
这样,我们的组件就改造好了。
6.请求数据与 redux 结合
请求方式我们使用大家非常熟悉的 axios
npm install --save axios
TodoList.js
import axios from 'axios'
componentDidMount() {axios.get('http://musicapi.leanapp.cn/playlist/hot').then((res) => {console.log(res)})
}
(我自己吧,也不会搭建后台啥的,所以就使用别人的接口,演示而已)😋😋😋😋😋
actionTypes.js
export const GET_LIST = 'getList'
actionCreators.js
import { GET_LIST } from './actionTypes'export const getListAction = (data) =>({type: GET_LIST,data
})
TodoList.js
import {getListAction } from './store/actionCreators'componentDidMount() {axios.get('http://musicapi.leanapp.cn/playlist/hot').then((res) => {const action = getListAction(res.data)store.dispatch(action)})
}
reducer.js
import { GET_LIST } from './actionTypes'const defaultState = {inputValue: '请输入',list: []
}
export default (state = defaultState, action) => {if (action.type === GET_LIST) {let newState = JSON.parse(JSON.stringify(state))console.log(action.data.tags)newState.list = action.data.tagsreturn newState}return state
}
此时我们需要对 TodoListUI.js 做一些变化,
<List.Item onClick={() => {props.deleteItem(index)}}>{item.createTime}
</List.Item>
不出意外的话,你就在页面上面看到一大堆数字。
7. redux-thunk 中间件
Redux-thunk
是这个Redux
最常用的插件。什么时候会用到这个插件?比如在Dispatch
一个Action
之后,到达reducer
之前,进行一些额外的操作,就需要用到middleware
(中间件)。在实际工作中你可以使用中间件来进行日志记录、创建崩溃报告,调用异步接口或者路由。 这个中间件可以使用是Redux-thunk
来进行增强(当然你也可以使用其它的),它是对Redux
中dispatch
的加强,
想使用,先安装
npm install --save redux-thunk
store / index.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const store = createStore(reducer,applyMiddleware(thunk)
)
export default store
以前 actionCreators.js 都是定义好的 action,根本没办法写业务逻辑,有了 Redux-thunk 之后,可以把 TodoList.js 中的 componentDidMount 业务逻辑放到这里来编写。也就是把向后台请求数据的代码放到 actionCreators.js 文件里。那我们需要引入 axios ,并写一个新的函数方法。(以前的action是对象,现在的action可以是函数了,这就是redux-thunk带来的好处)
actionCreators.js
import axios from 'axios'export const getTodoList = () => {return (dispatch) => {axios.get('http://musicapi.leanapp.cn/playlist/hot').then((res) => {const data = res.dataconst action = getListAction(data)dispatch(action)})}
}
TodoList.js
import { getTodoList } from './store/actionCreators'componentDidMount() {const action = getTodoList()store.dispatch(action)
}
8. React-Redux这是一个React生态中常用组件,它可以简化Redux流程
想使用,先安装
npm install --save react-redux
我们删除所有的代码,留下的东西就是我们最初简化之后的代码,src 目录下仅仅剩余一个 index.js
index.js
import React, { Component } from 'react';
class TodoList extends Component {render() {return (<div><div><input /><button>提交</button></div><ul><li>react</li></ul></div>)}
}
export default TodoList;
src 目录下新建 store 文件夹,index.js,reducer.js
index.js
import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store
reducer.js
const defalutState = {inputValue: '请输入',list: []
}
export default (state = defalutState, action) => {return state
}
页面我们已经建好了。接下来就是使用 react-redux
<Provider>
是一个提供器,只要使用了这个组件,组件里边的其它所有组件都可以使用store
了,这也是React-redux
的核心组件了
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'));
这样我们就与 react-redux 建立了连接
使用, connect 连接器的使用
映射关系就是把原来的 state 映射成组件中的 props 属性,比如我们想映射 inputValue 就可以写成如下代码。
import React, { Component } from 'react';
import store from './store'
import { connect } from 'react-redux'class TodoList extends Component {constructor(props) {super(props)this.state = store.getState()}render() {// ....
}const stateToProps = (state) => {return {inputValue: state.inputValue}
}export default connect(stateToProps, null)(TodoList);
修改 store 中的数据
React-redux
顺利的拿到 Store中 数据了。如何改变Store中的数据呢?也就是当我们修改<input>
中的值时,去改变store
数据,UI界面也随之进行改变。
TodoList.js
(绑定 onChange 事件)
<input value={this.props.inputValue} onChange={this.props.inputChange} />
inputChange(e) {console.log(e.target.value)
}
export default connect(stateToProps, null)(TodoList);
DispatchToProps
就是要传递的第二个参数,通过这个参数才能改变store中的值。
const dispatchToProps = (dispatch) => {return {inputChange(e) {console.log(e.target.value)}}
}
修改 input
<input value={this.props.inputValue} onChange={this.props.inputChange} />
把 connect 第二个参数传递过去。
export default connect(stateToProps,dispatchToProps)(TodoList);
TodoList.js
(完整代码)
import React, { Component } from 'react';
import store from './store'
import { connect } from 'react-redux'class TodoList extends Component {constructor(props) {super(props)this.state = store.getState()}render() {return (<div><div><input value={this.props.inputValue} onChange={this.props.inputChange} /><button>提交</button></div><ul><li>react redux</li></ul></div>);}
}
const stateToProps = (state) => {return {inputValue: state.inputValue}
}const dispatchToProps = (dispatch) => {return {inputChange(e) {console.log(e.target.value)}}
}export default connect(stateToProps, dispatchToProps)(TodoList);
派发 store
const dispatchToProps = (dispatch) =>{return {inputChange(e){let action = {type:'change_input',value:e.target.value}dispatch(action)}}
}
reducer.js
const defalutState = {inputValue: '请输入',list: []
}
export default (state = defalutState, action) => {if (action.type === 'change_input') {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}return state
}
不出意外,当你输入文字时,控制台会输出相对应的文字。
我们给 button 添加点击事件
<button onClick={this.props.clickButton}>提交</button>
const dispatchToProps = (dispatch) => {return {inputChange(e) {let action = {type: 'change_input',value: e.target.value}dispatch(action)},clickButton() {let action = { type: 'add_item' }dispatch(action)}}
}
const defalutState = {inputValue: '请输入',list: []
}
export default (state = defalutState, action) => {if (action.type === 'change_input') {let newState = JSON.parse(JSON.stringify(state))newState.inputValue = action.valuereturn newState}if (action.type === 'add_item') {let newState = JSON.parse(JSON.stringify(state))newState.list.push(newState.inputValue)newState.inputValue = ''return newState}return state
}
映射关系
const stateToProps = (state) => {return {inputValue: state.inputValue,list: state.list}
}
界面渲染
<ul>{this.props.list.map((item, index) => {return (<li key={index}>{item}</li>)})}
</ul>
TodoList.js
import React, { Component } from 'react';
import store from './store'
import { connect } from 'react-redux'class TodoList extends Component {constructor(props) {super(props)this.state = store.getState()}render() {return (<div><div><input value={this.props.inputValue} onChange={this.props.inputChange} /><button onClick={this.props.clickButton}>提交</button></div><ul>{this.props.list.map((item, index) => {return (<li key={index}>{item}</li>)})}</ul></div>);}
}
const stateToProps = (state) => {return {inputValue: state.inputValue,list: state.list}
}const dispatchToProps = (dispatch) => {return {inputChange(e) {let action = {type: 'change_input',value: e.target.value}dispatch(action)},clickButton() {let action = { type: 'add_item' }dispatch(action)}}
}export default connect(stateToProps, dispatchToProps)(TodoList);
实现一个简单的 redux
function createStore(reducer) {let currentState;let listeners = [];function getState() {return currentState;}function subscribe(listener) {listeners.push(listener)return function unsubscribe() {const index = listeners.indexOf(listener);listeners.splice(index, 1);}}function dispatch(action) {currentState = reducer(currentState, action)for (let i = 0; i < listeners.length; i++) {const lisenter = listeners[i];lisenter()}}return {getState,subscribe,dispatch}
}
完美撒花 🎉🎉🎉🎉🎉
参考文章 https://jspang.com/detailed?id=48#toc261