一、从一个计数器开始
1.1 用 class 声明一个计数器
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Computed from './Computed'
class Counter extends Component {
constructor (props, context) {
super()
this.state = {
num: 0
}
}
render () {
return (<div className="container">
<button onClick={() => this.setState({num: this.state.num + 1})}>+</button>
<span>{this.state.num}</span>
<button onClick={() => this.setState({num: this.state.num - 1})}>-</button>
<Computed num={this.state.num} />
</div>)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'))
1.2 声明一个计算当前计数器的数据是奇数还是偶数的 Computed 组件
import React, { Component } from 'react'
export default class Computed extends Component {
render () {
return (<div>
<h2>{this.props.num % 2 === 0 ? '偶数' : '奇数'}</h2>
</div>)
}
}
父子组件来回传,而且兄弟组件无能为力,所以我们需要使用 redux 全局托管数据
二、使用 redux
redux 是一种数据管理模式,它不仅可以用于 react 还可以配合其他库或者框架使用;我们把整个应用的状态交给 redux 托管,redux 会导出一个 store,其中包含获取状态的方法,以及变更状态的方法
2.1 安装 redux
yarn add redux --save
2.2 创建 store
2.2.1. 创建 store 需要使用 redux 的 createStore 方法,使用前需要导入:
import { createStore } from 'redux'
2.2.2. createStore 创建 store 需要 reducer函数
使用 redux 我们不能直接修改状态,我们需要定义修改状态的函数,这个函数称为 reducer
reducer 函数都会接收两个参数
- state 就是当前 redux 托管的数据对象,在创建 reducer 时给 state 设置的默认值就是 state 的初始值
- action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: ‘ADD’, …payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
在定义 reducer 时,我们需要初始化需要交给 redux 托管的状态设置初始值;在创建reducer时给 state 设置的默认值就是 state 的初始值
- 示例:
function reducer(state = { num: 0 }, action) {
// state 就是当前 redux 托管的数据对象,在创建 reducer 时给 state 设置的默认值就是 state 的初始值
// action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: 'ADD', ...payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
switch (action.type) {
case 'ADD':
return {
num: state.num + action.amount
}
case 'MINUS':
return {
num: state.num - action.amount
}
}
// 使用 reducer 首先需要返回一个默认状态
return state
}
2.2.3. 创建一个 store 的完整示例:
import { createStore } from 'redux'
// 使用 redux 我们不能直接修改状态,我们需要定义修改状态的函数,这个函数称为 reducer
// 一个用来管理状态的函数
function reducer(state = { num: 0 }, action) {
// state 就是当前 redux 托管的数据对象,在创建 reducer 时给 state 设置的默认值就是 state 的初始值
// action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: 'ADD', ...payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
switch (action.type) {
case 'ADD':
return {
num: state.num + action.amount
}
case 'MINUS':
return {
num: state.num - action.amount
}
}
// 使用 reducer 首先需要返回一个默认状态
return state
}
// 创建 store,只需要把 reducer 传递给 createStore
let store = createStore(reducer)
export default store
2.3 在组件中导入 store
有了 store,你可以:
- store.getState() 初始化 state
- 当修改数据时需要 派发行动 dispatch action; store.dispatch({type: ‘ADD’, amount: 12}),dispatch 执行时传递的对象就是 action,action 对象会传递给 reducer 函数的第二个参数
- 如果需要数据修改后更新视图,需要订阅这个数据发生变化后的事件,如果想修改视图就更新 state,在组件挂载的钩子中订阅
- 订阅后会有取消订阅的需要,订阅函数会返回取消订阅的函数,在组件将要销毁的钩子中执行取消订阅的操作
2.3.1 导入
import store from '../store'
2.3.2 使用 store.getState() 初始化 state
....
constructor (props, context) {
super()
this.state = {
num: store.getState().num
}
}
....
2.3.3 store.dispatch(actionObj) 修改状态
<button onClick={() => store.dispatch({type: 'ADD', amount: 1})}>+</button>
2.3.4 store.subscribe(callback) store 订阅状态更新后执行的回调,以便更新视图
componentDidMount () {
// 订阅状态变化后做的事情
this.unsub = store.subscribe(() => {
this.setState({
num: store.getState().num
})
})
}
componentWillUnmount () {
// 组件销毁时取消订阅
this.unsub()
}
2.4 使用 redux 后的 Counter.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Computed from './Computed'
import store from '../store'
window.__store = store
class Counter extends Component {
constructor (props, context) {
super()
this.state = {
num: store.getState().num
}
}
componentDidMount () {
// 订阅状态变化后做的事情
this.unsub = store.subscribe(() => {
this.setState({
num: store.getState().num
})
})
}
componentWillUnmount () {
this.unsub()
}
render () {
return (<div className="container">
<button onClick={() => store.dispatch({type: 'ADD', amount: 1})}>+</button>
<span>{this.state.num}</span>
<button onClick={() => store.dispatch({type: 'MINUS', amount: 1})}>-</button>
<Computed num={this.state.num} />
</div>)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'))
2.5 使用 redux 后的 Computed.js
import React, { Component } from 'react'
import store from '../store'
export default class Computed extends Component {
constructor (props, context) {
super()
this.state = {
num: store.getState().num
}
}
componentDidMount () {
this.unsub = store.subscribe(() => {
this.setState({
num: store.getState().num
})
})
}
componentWillUnmount () {
this.unsub()
}
render () {
return (<div>
<h2>{this.state.num % 2 === 0 ? '偶数' : '奇数'}</h2>
</div>)
}
}
三、提取 action 的 type
把 action 的 type 提取出来,作为宏
let ADD = 'ADD'
let MINUS = 'MINUS'
对应修改 store 和 Counter.js
四、reducer 合并
- 现在有两个拥有独立功能的组件:Counter.js 和 Todo.js
- redux 是一个单一的状态树,整个应用内所有的数据都要保存到一个 store 中,所以如果多个组件的状态,那么就会有多个 reducer,而 createStore 只能接收一个 reducer,此时就需要使用 reducer 合并
- redux 中的 combineReducers 是用来整合状态的方法,它接收一个对象作为参数,会把多个 reducer 整合到一个中。
整合后返回一个新的 reducer ,我们在创建 store 的时候传入这个整合后的 reducer; - 同时状态也被整合,例如上面的 counter 和 todo 整合后的状态对象: { todo: {list: [], filter}, counter: {num: 0}}
4.1 整合后示例
import { createStore, combineReducers } from 'redux'
// 把 action 的 type 提取出来,作为宏
let ADD = 'ADD'
let MINUS = 'MINUS'
// 使用 redux 我们不能直接修改状态,我们需要定义修改状态的函数,这个函数称为 reducer
// 一个用来管理状态的函数
function counter(state = { num: 0 }, action) {
// state 就是当前 redux 托管的数据对象,在创建reducer时给 state 设置的默认值就是 state 的初始值
// action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: 'ADD', ...payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
switch (action.type) {
case ADD:
return {
num: state.num + action.amount
}
case MINUS:
return {
num: state.num - action.amount
}
}
// 使用 reducer 首先需要返回一个默认状态
return state
}
let todoState = {
list: [],
filter: 'all'
}
function todo(state = todoState, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
list: [ // 这个 list 会覆盖上面 ... 出来的 list
...state.list,
action.content
]
}
}
return state
}
// combineReducers 是用来整合状态的方法,它接收一个对象作为参数,会把多个 reducer 整合到一个中
// 整合后的 返回一个新的 reducer ,同时 状态也被整合,例如上面的 counter 和 todo 整合后的状态对象: { todo: {list: [], filter}, counter: {num: 0}}
let combinedReducer = combineReducers({
todo: todo,
counter: counter
})
let store = createStore(combinedReducer)
export default store
4.2 使用整合后的 store
整合后的 store 带有命名空间,在组件中使用的时候需要通过对应的命名空间获取状态;
4.2.1 Counter.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Computed from './Computed'
import Todo from './Todo'
import store from '../store'
// 把 action 的 type 提取出来,作为宏
let ADD = 'ADD'
let MINUS = 'MINUS'
window.__store = store
class Counter extends Component {
constructor (props, context) {
super()
this.state = {
num: store.getState().counter.num
}
}
componentDidMount () {
// 订阅状态变化后做的事情
this.unsub = store.subscribe(() => {
this.setState({
num: store.getState().counter.num
})
})
}
componentWillUnmount () {
this.unsub()
}
render () {
return (<div className="container">
<button onClick={() => store.dispatch({type: ADD, amount: 1})}>+</button>
<span>{this.state.num}</span>
<button onClick={() => store.dispatch({type: MINUS, amount: 1})}>-</button>
<Computed />
<Todo />
</div>)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'))
4.2.2 Computed.js
import React, { Component } from 'react'
import store from '../store'
export default class Computed extends Component {
constructor (props, context) {
super()
this.state = {
num: store.getState().counter.num
}
}
componentDidMount () {
this.unsub = store.subscribe(() => {
this.setState({
num: store.getState().counter.num
})
})
}
componentWillUnmount () {
this.unsub()
}
render () {
return (<div>
<h2>{this.state.num % 2 === 0 ? '偶数' : '奇数'}</h2>
</div>)
}
}
4.2.3 Todo.js
import React, { Component } from 'react'
import store from '../store'
export default class Todo extends Component {
constructor (props, context) {
super()
this.state = {
todos: store.getState().todo
}
}
componentDidMount () {
store.subscribe(() => {
this.setState({
todos: store.getState().todo
})
})
}
render () {
return (<div className="container">
<p><input type="text" onKeyDown={(e) => {
if (e.keyCode === 13) {
store.dispatch({
type: 'ADD_TODO',
content: e.target.value
})
e.target.value = ''
}
}
}/></p>
<ul>
{
this.state.todos.list.map((item, index) => <li key={index}>{item}</li>)
}
</ul>
</div>)
}
}
五、actionCreator 动作创建器
写一个函数专门生成 dispatch 需要的 action 对象,这个函数称为 action-creator
示例:
import * as Types from '../action-type'
function add(amount) {
return {
type: Types.ADD,
amount
}
}
function minus(amount) {
return {
type: Types.MINUS,
amount
}
}
- 有了 action-creator 以后,在 dispatch 时只需要调用 action-creator 并且传入 payload 即可;
- 示例:
// add 和 minus 就是 action-creator,而 1 是传递给 上面的 amount,即 payload
<button onClick={() => store.dispatch(add(1))}>+</button>
<span>{this.state.num}</span>
<button onClick={() => store.dispatch(minus(1))}>-</button>
六、文件拆分
React 的模块化特性很强,对应着 redux 的使用也需要拆分成模块化达到代码解耦的目的,上例中的 store 我们拆分文件后的结构如下;
6.1 文件结构
store
│ index.js 导出 store
│
├─action action-creator
│ counter.js
│ todo.js
│
├─action-type action 定义
│ index.js
│
└─reducer 存放 reducer
counter.js
todo.js
/store/index.js
import {createStore, combineReducers} from 'redux'
import { counter } from './reducer/counter'
import { todo } from './reducer/todo'
let combinedReducer = combineReducers({
todo: todo,
counter: counter
})
let store = createStore(combinedReducer)
export default store
store/action/counter.js
import * as Types from '../action-type'
function add(amount) {
return {
type: Types.ADD,
amount
}
}
function minus(amount) {
return {
type: Types.MINUS,
amount
}
}
export { add, minus }
store/action/todo.js
import * as Types from '../action-type'
function addTodo(content) {
return {
type: Types.ADD_TODO,
content
}
}
export { addTodo }
store/action-types/index
export const ADD = 'ADD'
export const MINUS = 'MINUS'
export const ADD_TODO = 'ADD_TODO'
store/reducer/counter
import * as Types from '../action-type'
export function counter(state = {num: 0}, action) {
switch (action.type) {
case Types.ADD:
return {
num: state.num + action.amount
}
case Types.MINUS:
return {
num: state.num - action.amount
}
}
// 使用 reducer 首先需要返回一个默认状态
return state
}
/store/reducer/todo.js
import * as Types from '../action-type'
let todoState = {
list: [],
filter: 'all'
}
export function todo(state = todoState, action) {
switch (action.type) {
case Types.ADD_TODO:
return {
...state,
list: [ // 这个 list 会覆盖上面 ... 出来的 list
...state.list,
action.content
]
}
}
return state
}
七、react-redux
上面虽然实现了数据的统一管理,但是代码组织很繁琐,为了解决这个问题我们使用一个另一个工具,react-redux;
7.1 安装 react-redux
yarn add react-redux --save
7.2 react-redux 的 Provider
react-redux 提供了一个 Provider 组件,通过 Provider 组件将 store 引入到组件树中,可以简化在组件里初始化状态,修改组件需要派发事件,然后还需要订阅更新;
- 使用 Provider 在主入口 index.js 引入 store,通过 prop 把 store 作为属性,传给 Provider 组件
7.3 react-redux 的 connect 方法
react-redux 提供了一个 connect 方法;通过 connect 改造组件,经过 connect 改造,组件中的数据以及派发数据的方法全部通过 props 来实现;
7.4 示例:
7.4.1 src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { Provider } from 'react-redux'
import store from './store'
import Counter from './components/Counter'
ReactDOM.render(<Provider store={store}>
<Counter />
</Provider>, document.getElementById('root'))
7.4.2 使用 connect 导出一个连接后的组件
- 7.3.2.1 使用 connect 首先从 react-redux 导出 connect;
import { connect } from 'react-redux'
- 7.3.2.2 使用 connect 导出的是连接后的组件,connect 可以执行两次:
connect 是一个高阶函数,形如let connect = > (m1, m2) => (component) => {}
- 第一次执行传入两个回调函数:
- mapStateToProps 把 store 的状态映射成为组件的 props
- mapDispatchToProps 把 dispatch 映射为 prop
- 第二次执行,传入组件名 Counter
使用 connect 后,对应组件内的使用数据和修改数据的方式也需要调整,方法和数据都要从 prop 上获取
- Counter.js 示例:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Computed from './Computed'
import Todo from './Todo'
import { add, minus} from '../store/action/counter'
import { connect } from 'react-redux'
// 使用 react-redux 需要导出一个连接后的组件
class Counter extends Component {
render () {
return (<div className="container">
<button onClick={() => this.props.add(1)}>+</button>
<span>{this.props.num}</span>
<button onClick={() => this.props.minus(1)}>-</button>
<Computed />
<Todo />
</div>)
}
}
// react-redux 的 connect 方法接收两个参数,let connect = > (m1, m2) => (component) => {}
// 第一次执行传入两个回调函数:
// 1. mapStateToProps 把 store 的状态映射称为组件的 props
// 2. mapDispatchToProps 把 dispatch 映射为 prop
// 第二次执行,传入组件名 Counter
let mapStateToProps = (state) => {
// mapStateToProps 的参数 state 就是合并后的状态对象
// 在 mapStateToProps 函数中要返回一个对象,这些对象中是数据都会成为对应组件的 props
return {
num: state.counter.num
}
}
let mapDispatchToProps = (dispatch) => {
// mapDispatchToProps 的参数是 dispatch 就是派发行为的 store.dispatch 方法
// mapDispatchToProps 需要返回一个对象,这个对象中
return {
add: (amount) => dispatch(add(amount)),
minus: (amount) => dispatch(minus(amount))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter)
7.5 mapStateToProps 和 mapDispatchToProps 有简便写法
- mapStateToProps 可以写成一个 箭头函数,在箭头函数中使用 … 运算符展开某个状态对象
- mapDispatchToProps 可以传入一个 actionCreator 对象,示例
export default connect(state => ({...state.counter}), {add, minus})(Counter)