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' }}>
<List
bordered
dataSource={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>
<Input
placeholder={this.state.inputValue}
style={{ width: '300px' }}
/>
<Button type="primary">添加</Button>
</div>
<div style={{ width: '300px' }}>
<List
bordered
dataSource={this.state.list}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
</div>
);
}
}
export default TodoList;
4. TodoList 的变化引起 redux 变化
我们给输入框增加一个 onChange 事件
<Input
placeholder={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.js 里面,打印出这两个值
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.value
return 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 事件
<List
bordered
dataSource={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.value
return 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>
<Input
placeholder={this.state.inputValue}
onChange={this.changeInputValue}
value={this.inputValue}
style={{ width: '300px' }}
/>
<Button type="primary" onClick={this.clickBtn}>添加</Button>
</div>
<div style={{ width: '300px' }}>
<List
bordered
dataSource={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>
<Input
placeholder={this.state.inputValue}
onChange={this.changeInputValue}
value={this.inputValue}
style={{ width: '300px' }}
/>
<Button type="primary" onClick={this.clickBtn}>添加</Button>
</div>
<div style={{ width: '300px' }}>
<List
bordered
dataSource={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>
<Input
placeholder={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' }}>
<List
bordered
dataSource={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>
<Input
placeholder={props.inputValue}
onChange={props.changeInputValue}
value={props.inputValue}
style={{ width: '300px' }}
/>
<Button type="primary" onClick={props.clickBtn}>添加</Button>
</div>
<div style={{ width: '300px' }}>
<List
bordered
dataSource={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)
})
}
(我自己吧,也不会搭建后台啥的,所以就使用别人的接口,演示而已)😋😋😋😋😋
数据成功返回了。接下来就是与 redux 的联动了。
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.tags
return 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.data
const 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.value
return 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.value
return 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