⑧ React Redux 基本用法
查看专栏其它文章:
① React 介绍及JSX简单使用
② React 面向组件编程(state、props、refs)、事件处理
③ React 条件渲染、组件生命周期、表单与事件
④ React 列表与Keys、虚拟DOM相关说明、AJAX
⑤ React 基于react脚手架构建简单项目
⑥ React 项目中的AJAX请求、组件间通信的2种方式(props、消息订阅和发布)
⑦ React 路由解决方案 react-router
React
- Redux 介绍
- 基础用法
- React 版
- Redux 版(相关用法)
- React-Redux 版(相关用法)
- Redux 异步编程
- 其它用法
本人是个新手,写下博客用于自我复习、自我总结。
如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷
Redux 介绍
Redux 内容有点难于总结,因此本文写的比较杂乱。如有需求可直接前往中文文档学习:https://www.redux.org.cn/
redux 是什么?
- redux 是一个独立专门用于做状态管理的 JS 库(不是 react 插件库)
- 它可以用在 react, angular, vue 等项目中, 但基本与 react 配合使用
- 作用: 集中式管理 react 应用中多个组件共享的状态
为什么需要 redux?
从之前的小项目来看,我们其实也实现了 “集中式管理”。我们把相关内容都集中在根组件 App 上,只不过每个子组件中会存放各自的状态(state),修改这些状态的行为也就在各自的组件中。但是对于大型项目来说,会有很多组件 和 路由组件,路由组件甚至也会有子路由组件,也许这些组件有共享的状态,又或者是相关状态,都存放在各自的组件中,寻找和修改时就会比较繁琐又费事费力,还需要组件通信。因此就考虑到:专门设计一个库,将部分状态交给它来管理,再提供一些修改状态的方法,之后哪个组件需要就去调用它即可。
更多内容可参考: 动机
因此根据上述的思路,Redux 的工作流程也可略知一二:对于状态一共就有两种行为,一个是读状态显示,一个是更新状态。所以组件首先是从 Redux 的存储状态区域读取状态,随后遇到更新状态事件,就去进行一系列操作,最后再存入到存储状态区域,给相关组件读取。
那在这其中最关键的两个问题就是:组件如何和Redux交互、Redux内部如何接收事件并实现状态更新。
现在看一下具体的 redux 工作流程:( 相关用法在文章下方就会提及 )
总结来说整体流程是:React 组件 Components 从 Redux 状态存储 Store 中,获取到状态 state。随后更新状态需要通过 Redux 的 Action Creators 分发 dispatch 事件 action,但是这个事件不能直接去修改 Store 中状态,需要先去 Reducers 中。在 Reducers 中,就会根据事件 action 对之前的状态 previousState 进行操作,随后将新状态 newState 存储进 Store 中。
什么情况下需要使用 redux?
- 总体原则:能不用就不用,如果不用比较吃力才考虑使用
- 某个组件的状态需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
基础用法
在本例中将实现下图功能:次数的值需要通过加减按钮进行变化,加减数量由左侧选择框决定。且当次数为奇数时,点击 increment if odd 按钮才会变化;点击 increment async 将会通过 定时器 setTimeout 异步增加次数。
为了比对 React 和 Redux 的区别,将会编写这两种代码。
React 版
内容比较简单,项目结构如下:
(如果对基本用法感到困惑,可参考我的 ① ~ ④ 测试语法文章)
index.js:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/app'
ReactDOM.render(<App/>, document.getElementById('root'))
app.jsx:
import React, { Component} from 'react'
export default class App extends Component {
state = {
count: 0
}
increment = () => {
const num = this.refs.numSelect.value*1
const count = this.state.count + num
this.setState({ count})
}
decrement = () => {
const num = this.refs.numSelect.value*1
const count = this.state.count - num
this.setState({ count})
}
incrementIfOdd = () => {
let count = this.state.count
if(count%2==1) {
const num = this.refs.numSelect.value*1
count += num
this.setState({ count})
}
}
incrementAsync = () => {
setTimeout(() => {
const num = this.refs.numSelect.value*1
const count = this.state.count + num
this.setState({ count})
}, 1000)
}
render () {
const { count} = this.state
return (
<div>
<p>
click { count} times
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{ ' '}
<button onClick={ this.increment}>+</button>{ ' '}
<button onClick={ this.decrement}>-</button>{ ' '}
<button onClick={ this.incrementIfOdd}>increment if odd</button>{ ' '}
<button onClick={ this.incrementAsync}>increment async</button>
</div>
)
}
}
Redux 版(相关用法)
在具体看代码前,先了解一下 Redux 的相关概念和用法。
之后如果突然对某步骤感到困惑,可参照该图记忆:
为了方便说明,在这里展示本例的项目结构:
redux 的三个核心概念:
(1)Action:
定义:Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch(action)
将 action 传到 store。( 但是需要注意,Action并没有直接修改 state,想要修改需要 Reducers)
而 dispatch 中的 action 应该需要包含两个方面的属性。
- type:用来表示将要执行的动作,其值为字符串,且是必要的唯一属性。多数情况下,type 会被定义成字符串常量。
- xxx:数据属性,值类型任意,是可选属性。
例:
store.dispatch({ type: INCREMENT, data: number})
当应用规模越来越大时,建议使用单独的模块或文件来存放 action。因此在项目结构中创建 actions.js 和 action-types.js。综上所述,在 actions.js 中就是这样的内容:(这就是 Action Creators 模块:创建 Action 的工厂函数)
/*action creator模块*/
import { INCREMENT, DECREMENT} from './action-types'
export const increment = number => ({ type: INCREMENT, number})
export const decrement = number => ({ type: DECREMENT, number})
action-types.js 中是这样的内容:
/*Action对象的type常量名称模块*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
在这里的 action-types.js 是样板文件。像这样使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。
改进后,store.dispatch 这么用:
import * as actions from '../redux/actions'
store.dispatch(actions.increment(number)
也就是:我像 store 传递了一个 increment 标识 和 number 参数,之后 Reducers 就可以根据这个标识和参数,对原本的状态进行更新。
(2)Reducers:
定义:Reducers 指定了应用状态的变化如何响应 Action 并发送到 Store 的,即:根据原本的 State 和 Action,去产生新的 State。
reducers.js 中的内容:
/*根据原本的state和指定action, 处理返回一个新的state*/
import { INCREMENT, DECREMENT} from './action-types'
export function counter(state = 0, action) {
console.log('counter', state, action)
switch (action.type) {
case INCREMENT:
return state + action.number
case DECREMENT:
return state - action.number
default:
return state
}
}
在这里,Reducers 也就是 counter,共有 2 个参数。其中 state 就是存储的状态,action 就是从 Actions 传递过来的内容。传递过来的内容如下:
我们可以看到,Reducers 部分 接收到了 type标识 和传递过来的参数。现在我们需要根据标识来对状态进行更新,但是在这里需要注意,虽然我们可以调用 state,但是不要修改原来的这个状态,需要通过 return 的方式返回一个新的状态,就像上面的代码所示。为了方便判断标识,才选择使用 switch case。
(3)Stores:
在上面的 Reducers 之所以能够接收到这些信息,是因为有 Store 的帮助。Store 是 redux 库最核心的管理对象。在它的内部维护着 状态 state 和 reducer。( 不要忘记之前在工作流程中提到,Store 不能直接修改状态,需要通过 Reducers )
如何得到此对象:
import { createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
此时,Store,Actions,Reducers 就能够真正串联起来了。
Store 的核心方法:
store.getState() :得到 state
store.dispatch(action):分发 action, 触发 reducer 调用, 产生新的 state
store.subscribe(listener):注册监听, 当产生了新的 state 时, 自动调用
完整代码:
使用前:npm install --save redux
项目结构:
index.js:
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore} from 'redux'
import App from './components/app'
import { counter} from './redux/reducers'
// 根据counter函数创建store对象
const store = createStore(counter)
// 定义渲染根组件标签的函数
const render = () => {
ReactDOM.render(
<App store={ store}/>,
document.getElementById('root')
)
}
// 初始化渲染
render()
// 注册(订阅)监听, 一旦状态发生改变, 自动重新渲染
store.subscribe(render)
action-types.js:
/* Action对象的type常量名称模块 */
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
actions.js:
/* action creator模块 */
import { INCREMENT, DECREMENT} from './action-types'
export const increment = number => ({ type: INCREMENT, number})
export const decrement = number => ({ type: DECREMENT, number})
reducers.js:
/* 根据老的state和指定action, 处理返回一个新的state */
import { INCREMENT, DECREMENT} from './action-types'
export function counter(state = 0, action) {
console.log('counter', state, action)
switch (action.type) {
case INCREMENT:
return state + action.number
case DECREMENT:
return state - action.number
default:
return state
}
}
app.jsx:
/* 应用组件 */
import React, { Component} from 'react'
import PropTypes from 'prop-types'
import * as actions from '../redux/actions'
export default class App extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
}
increment = () => {
const number = this.refs.numSelect.value * 1
this.props.store.dispatch(actions.increment(number))
}
decrement = () => {
const number = this.refs.numSelect.value * 1
this.props.store.dispatch(actions.decrement(number))
}
incrementIfOdd = () => {
const number = this.refs.numSelect.value * 1
let count = this.props.store.getState()
if (count % 2 === 1) {
this.props.store.dispatch(actions.increment(number))
}
}
incrementAsync = () => {
const number = this.refs.numSelect.value * 1
setTimeout(() => {
this.props.store.dispatch(actions.increment(number))
}, 1000)
}
render() {
return (
<div>
<p>
click { this.props.store.getState()} times { ' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{ ' '}
<button onClick={ this.increment}>+</button>
{ ' '}
<button onClick={ this.decrement}>-</button>
{ ' '}
<button onClick={ this.incrementIfOdd}>increment if odd</button>
{ ' '}
<button onClick={ this.incrementAsync}>increment async</button>
</div>
)
}
}
其实 Redux 存在一些问题:
- Redux 与 React 组件的代码耦合度太高
- 编码不够简洁
为了解决这个问题,出现了 React-Redux。
React-Redux 版(相关用法)
React-Redux 是 React 的插件库。它专门用来简化 React 应用中使用 Redux。
使用 react-redux 下载依赖包:npm install --save react-redux
React-Redux 将所有组件分成两大类:
- UI 组件
a. 只负责 UI 的呈现,不带有任何业务逻辑
b. 通过 props 接收数据( 一般数据和函数 )
c. 不使用任何 Redux 的 API
d. 一般保存在 components 文件夹下 - 容器组件
a. 负责管理数据和业务逻辑,不负责 UI 的呈现
b. 使用 Redux 的 API
c. 一般保存在 containers 文件夹下
综上所述,我们先创建项目结构:
action-types.js 、actions.js、reducers.js 和之前没有区别。
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
/* action creator模块 */
import { INCREMENT, DECREMENT} from './action-types'
export const increment = number => ({ type: INCREMENT, number})
export const decrement = number => ({ type: DECREMENT, number})
import { INCREMENT, DECREMENT} from './action-types'
export function counter(state = 0, action) {
console.log('counter', state, action)
switch (action.type) {
case INCREMENT:
return state + action.number
case DECREMENT:
return state - action.number
default:
return state
}
}
接下来看 React-Redux 到底做了什么简化。
index.js
相比于 Redux 版本,现在不需要注册监听 store.subscribe(render)
,但是需要使用 Provider:
<Provider store={store}>
<App />
</Provider>
这将能够让所有组件都可以得到 state 数据
完整代码:
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore} from 'redux'
import { Provider} from 'react-redux'
import App from './containers/app'
import { counter} from './redux/reducers'
// 根据counter函数创建store对象
const store = createStore(counter)
// 定义渲染根组件标签的函数
ReactDOM.render(
(
<Provider store={ store}>
<App/>
</Provider>
),
document.getElementById('root')
)
app.jsx
对于 app.jsx,在这里它是容器组件,负责管理数据和业务逻辑,不负责 UI 的呈现。在这里会用到 connect。它专门用于包装 UI 组件生成容器组件。用法如下:
import { connect } from 'react-redux'
connect(
mapStateToprops,
mapDispatchToProps
)(Counter)
在这里,mapStateToprops 处就可以将外部的数据(即 state 对象)转换为 UI 组件的标签属性;mapDispatchToProps 处就可以将分发 action 的函数转换为 UI 组件的标签属性。然后将这些信息传递给 Counter。
完整代码:
/* 包含Counter组件的容器组件 */
import React from 'react'
// 引入连接函数
import { connect} from 'react-redux'
// 引入action函数
import { increment, decrement} from '../redux/actions'
import Counter from '../components/counter'
// 向外暴露连接App组件的包装组件
export default connect(
state => ({ count: state}),
{ increment, decrement}
)(Counter)
counter.jsx
随后,在 UI 组件 counter.jsx 中 就可以只负责 UI 的呈现,不带有任何业务逻辑,直接通过 props 接收数据。
完整代码:
/* UI组件: 不包含任何redux API */
import React from 'react'
import PropTypes from 'prop-types'
export default class Counter extends React.Component {
static propTypes = {
count: PropTypes.number.isRequired,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired
}
increment = () => {
const number = this.refs.numSelect.value * 1
this.props.increment(number)
}
decrement = () => {
const number = this.refs.numSelect.value * 1
this.props.decrement(number)
}
incrementIfOdd = () => {
const number = this.refs.numSelect.value * 1
let count = this.props.count
if (count % 2 === 1) {
this.props.increment(number)
}
}
incrementAsync = () => {
const number = this.refs.numSelect.value * 1
setTimeout(() => {
this.props.increment(number)
}, 1000)
}
render() {
return (
<div>
<p>
click { this.props.count} times { ' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{ ' '}
<button onClick={ this.increment}>+</button>
{ ' '}
<button onClick={ this.decrement}>-</button>
{ ' '}
<button onClick={ this.incrementIfOdd}>increment if odd</button>
{ ' '}
<button onClick={ this.incrementAsync}>increment async</button>
</div>
)
}
}
现在只剩下一个问题,Redux 默认是不能进行异步处理的,但是在我们的例子中又需要在 Redux 中执行异步任务(ajax, 定时器),所以 Redux 的异步编程就出现了。
Redux 异步编程
使用前需要下载 Redux 插件(异步中间件)npm install --save redux-thunk
index.js 中,首先需要应用上异步中间件。这里需要使用 applyMiddleware() ,它的作用就是应用上基于 redux 的中间件 ( 插件库 ),在这里就是应用 thunk。
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider} from 'react-redux'
import App from './containers/app'
import { createStore, applyMiddleware} from "redux";
import thunk from 'redux-thunk'
import reducers from "./redux/reducers";
// 根据counter函数创建store对象
const store = createStore(
reducers,
applyMiddleware(thunk) // 应用上异步中间件
)
// 定义渲染根组件标签的函数
ReactDOM.render(
(
<Provider store={ store}>
<App/>
</Provider>
),
document.getElementById('root')
)
随后将异步代码放入 action 中。
在这里需要注意,同步的action只需要返回一个对象,而对于异步的action需要返回一个函数。
/*action creator模块*/
import { INCREMENT, DECREMENT} from './action-types'
export const increment = number => ({ type: INCREMENT, number})
export const decrement = number => ({ type: DECREMENT, number})
// 异步action creator(返回一个函数)
export const incrementAsync = number => {
return dispatch => {
setTimeout(() => {
dispatch(increment(number))
}, 1000)
}
}
然后将 action 传递给 UI组件。
/* 包含Counter组件的容器组件 */
import React from 'react'
// 引入连接函数
import { connect} from 'react-redux'
// 引入action函数
import { increment, decrement, incrementAsync} from '../redux/actions'
import Counter from '../components/counter'
// 向外暴露连接App组件的包装组件
export default connect(
state => ({ count: state.counter}),
{ increment, decrement, incrementAsync}
)(Counter)
之后去 UI 组件中调用。
incrementAsync = () => {
const number = this.refs.numSelect.value*1
this.props.incrementAsync(number)
}
其它用法
Reducers 中会不止一个状态,此时为了方便创建 Store 对象,我们可以使用 combineReducers(),它用来合并多个 reducer 函数。
import { combineReducers} from 'redux'
// ....
export default combineReducers({
user, chatUser, chat
})
import { createStore} from "redux";
import reducers from "./redux/reducers";
// 创建store对象
const store = createStore(reducers)
在 chrome 浏览器上 使用 redux 调试工具,首先需要为浏览器下载 redux-devtools_2_15_1.crx 。随后在项目中还要npm install --save-dev redux-devtools-extension
。代码:
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore(
counter,
composeWithDevTools(applyMiddleware(thunk))
)
还没有评论,来说两句吧...