一、从一个计数器开始

1.1 用 class 声明一个计数器

  1. import React, { Component } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import Computed from './Computed'
  4. class Counter extends Component {
  5. constructor (props, context) {
  6. super()
  7. this.state = {
  8. num: 0
  9. }
  10. }
  11. render () {
  12. return (<div className="container">
  13. <button onClick={() => this.setState({num: this.state.num + 1})}>+</button>
  14. <span>{this.state.num}</span>
  15. <button onClick={() => this.setState({num: this.state.num - 1})}>-</button>
  16. <Computed num={this.state.num} />
  17. </div>)
  18. }
  19. }
  20. ReactDOM.render(<Counter />, document.getElementById('root'))

1.2 声明一个计算当前计数器的数据是奇数还是偶数的 Computed 组件

  1. import React, { Component } from 'react'
  2. export default class Computed extends Component {
  3. render () {
  4. return (<div>
  5. <h2>{this.props.num % 2 === 0 ? '偶数' : '奇数'}</h2>
  6. </div>)
  7. }
  8. }

父子组件来回传,而且兄弟组件无能为力,所以我们需要使用 redux 全局托管数据

二、使用 redux

redux 是一种数据管理模式,它不仅可以用于 react 还可以配合其他库或者框架使用;我们把整个应用的状态交给 redux 托管,redux 会导出一个 store,其中包含获取状态的方法,以及变更状态的方法

2.1 安装 redux

  1. yarn add redux --save

2.2 创建 store

2.2.1. 创建 store 需要使用 redux 的 createStore 方法,使用前需要导入:

  1. 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 的初始值

  • 示例:
  1. function reducer(state = { num: 0 }, action) {
  2. // state 就是当前 redux 托管的数据对象,在创建 reducer 时给 state 设置的默认值就是 state 的初始值
  3. // action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: 'ADD', ...payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
  4. switch (action.type) {
  5. case 'ADD':
  6. return {
  7. num: state.num + action.amount
  8. }
  9. case 'MINUS':
  10. return {
  11. num: state.num - action.amount
  12. }
  13. }
  14. // 使用 reducer 首先需要返回一个默认状态
  15. return state
  16. }

2.2.3. 创建一个 store 的完整示例:

  1. import { createStore } from 'redux'
  2. // 使用 redux 我们不能直接修改状态,我们需要定义修改状态的函数,这个函数称为 reducer
  3. // 一个用来管理状态的函数
  4. function reducer(state = { num: 0 }, action) {
  5. // state 就是当前 redux 托管的数据对象,在创建 reducer 时给 state 设置的默认值就是 state 的初始值
  6. // action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: 'ADD', ...payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
  7. switch (action.type) {
  8. case 'ADD':
  9. return {
  10. num: state.num + action.amount
  11. }
  12. case 'MINUS':
  13. return {
  14. num: state.num - action.amount
  15. }
  16. }
  17. // 使用 reducer 首先需要返回一个默认状态
  18. return state
  19. }
  20. // 创建 store,只需要把 reducer 传递给 createStore
  21. let store = createStore(reducer)
  22. 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 导入

  1. import store from '../store'

2.3.2 使用 store.getState() 初始化 state

  1. ....
  2. constructor (props, context) {
  3. super()
  4. this.state = {
  5. num: store.getState().num
  6. }
  7. }
  8. ....

2.3.3 store.dispatch(actionObj) 修改状态

  1. <button onClick={() => store.dispatch({type: 'ADD', amount: 1})}>+</button>

2.3.4 store.subscribe(callback) store 订阅状态更新后执行的回调,以便更新视图

  1. componentDidMount () {
  2. // 订阅状态变化后做的事情
  3. this.unsub = store.subscribe(() => {
  4. this.setState({
  5. num: store.getState().num
  6. })
  7. })
  8. }
  9. componentWillUnmount () {
  10. // 组件销毁时取消订阅
  11. this.unsub()
  12. }

2.4 使用 redux 后的 Counter.js

  1. import React, { Component } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import Computed from './Computed'
  4. import store from '../store'
  5. window.__store = store
  6. class Counter extends Component {
  7. constructor (props, context) {
  8. super()
  9. this.state = {
  10. num: store.getState().num
  11. }
  12. }
  13. componentDidMount () {
  14. // 订阅状态变化后做的事情
  15. this.unsub = store.subscribe(() => {
  16. this.setState({
  17. num: store.getState().num
  18. })
  19. })
  20. }
  21. componentWillUnmount () {
  22. this.unsub()
  23. }
  24. render () {
  25. return (<div className="container">
  26. <button onClick={() => store.dispatch({type: 'ADD', amount: 1})}>+</button>
  27. <span>{this.state.num}</span>
  28. <button onClick={() => store.dispatch({type: 'MINUS', amount: 1})}>-</button>
  29. <Computed num={this.state.num} />
  30. </div>)
  31. }
  32. }
  33. ReactDOM.render(<Counter />, document.getElementById('root'))

2.5 使用 redux 后的 Computed.js

  1. import React, { Component } from 'react'
  2. import store from '../store'
  3. export default class Computed extends Component {
  4. constructor (props, context) {
  5. super()
  6. this.state = {
  7. num: store.getState().num
  8. }
  9. }
  10. componentDidMount () {
  11. this.unsub = store.subscribe(() => {
  12. this.setState({
  13. num: store.getState().num
  14. })
  15. })
  16. }
  17. componentWillUnmount () {
  18. this.unsub()
  19. }
  20. render () {
  21. return (<div>
  22. <h2>{this.state.num % 2 === 0 ? '偶数' : '奇数'}</h2>
  23. </div>)
  24. }
  25. }

三、提取 action 的 type

把 action 的 type 提取出来,作为宏

  1. let ADD = 'ADD'
  2. 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 整合后示例

  1. import { createStore, combineReducers } from 'redux'
  2. // 把 action 的 type 提取出来,作为宏
  3. let ADD = 'ADD'
  4. let MINUS = 'MINUS'
  5. // 使用 redux 我们不能直接修改状态,我们需要定义修改状态的函数,这个函数称为 reducer
  6. // 一个用来管理状态的函数
  7. function counter(state = { num: 0 }, action) {
  8. // state 就是当前 redux 托管的数据对象,在创建reducer时给 state 设置的默认值就是 state 的初始值
  9. // action 是修改状态具体的动作以及修改状态需要的参数,是一个带有 type 字段的对象 { type: 'ADD', ...payload } ,而 reducer 的作用就是根据不同的 action.type 返回一个新的状态对象
  10. switch (action.type) {
  11. case ADD:
  12. return {
  13. num: state.num + action.amount
  14. }
  15. case MINUS:
  16. return {
  17. num: state.num - action.amount
  18. }
  19. }
  20. // 使用 reducer 首先需要返回一个默认状态
  21. return state
  22. }
  23. let todoState = {
  24. list: [],
  25. filter: 'all'
  26. }
  27. function todo(state = todoState, action) {
  28. switch (action.type) {
  29. case 'ADD_TODO':
  30. return {
  31. ...state,
  32. list: [ // 这个 list 会覆盖上面 ... 出来的 list
  33. ...state.list,
  34. action.content
  35. ]
  36. }
  37. }
  38. return state
  39. }
  40. // combineReducers 是用来整合状态的方法,它接收一个对象作为参数,会把多个 reducer 整合到一个中
  41. // 整合后的 返回一个新的 reducer ,同时 状态也被整合,例如上面的 counter 和 todo 整合后的状态对象: { todo: {list: [], filter}, counter: {num: 0}}
  42. let combinedReducer = combineReducers({
  43. todo: todo,
  44. counter: counter
  45. })
  46. let store = createStore(combinedReducer)
  47. export default store

4.2 使用整合后的 store

整合后的 store 带有命名空间,在组件中使用的时候需要通过对应的命名空间获取状态;

4.2.1 Counter.js

  1. import React, { Component } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import Computed from './Computed'
  4. import Todo from './Todo'
  5. import store from '../store'
  6. // 把 action 的 type 提取出来,作为宏
  7. let ADD = 'ADD'
  8. let MINUS = 'MINUS'
  9. window.__store = store
  10. class Counter extends Component {
  11. constructor (props, context) {
  12. super()
  13. this.state = {
  14. num: store.getState().counter.num
  15. }
  16. }
  17. componentDidMount () {
  18. // 订阅状态变化后做的事情
  19. this.unsub = store.subscribe(() => {
  20. this.setState({
  21. num: store.getState().counter.num
  22. })
  23. })
  24. }
  25. componentWillUnmount () {
  26. this.unsub()
  27. }
  28. render () {
  29. return (<div className="container">
  30. <button onClick={() => store.dispatch({type: ADD, amount: 1})}>+</button>
  31. <span>{this.state.num}</span>
  32. <button onClick={() => store.dispatch({type: MINUS, amount: 1})}>-</button>
  33. <Computed />
  34. <Todo />
  35. </div>)
  36. }
  37. }
  38. ReactDOM.render(<Counter />, document.getElementById('root'))

4.2.2 Computed.js

  1. import React, { Component } from 'react'
  2. import store from '../store'
  3. export default class Computed extends Component {
  4. constructor (props, context) {
  5. super()
  6. this.state = {
  7. num: store.getState().counter.num
  8. }
  9. }
  10. componentDidMount () {
  11. this.unsub = store.subscribe(() => {
  12. this.setState({
  13. num: store.getState().counter.num
  14. })
  15. })
  16. }
  17. componentWillUnmount () {
  18. this.unsub()
  19. }
  20. render () {
  21. return (<div>
  22. <h2>{this.state.num % 2 === 0 ? '偶数' : '奇数'}</h2>
  23. </div>)
  24. }
  25. }

4.2.3 Todo.js

  1. import React, { Component } from 'react'
  2. import store from '../store'
  3. export default class Todo extends Component {
  4. constructor (props, context) {
  5. super()
  6. this.state = {
  7. todos: store.getState().todo
  8. }
  9. }
  10. componentDidMount () {
  11. store.subscribe(() => {
  12. this.setState({
  13. todos: store.getState().todo
  14. })
  15. })
  16. }
  17. render () {
  18. return (<div className="container">
  19. <p><input type="text" onKeyDown={(e) => {
  20. if (e.keyCode === 13) {
  21. store.dispatch({
  22. type: 'ADD_TODO',
  23. content: e.target.value
  24. })
  25. e.target.value = ''
  26. }
  27. }
  28. }/></p>
  29. <ul>
  30. {
  31. this.state.todos.list.map((item, index) => <li key={index}>{item}</li>)
  32. }
  33. </ul>
  34. </div>)
  35. }
  36. }

五、actionCreator 动作创建器

写一个函数专门生成 dispatch 需要的 action 对象,这个函数称为 action-creator

示例:

  1. import * as Types from '../action-type'
  2. function add(amount) {
  3. return {
  4. type: Types.ADD,
  5. amount
  6. }
  7. }
  8. function minus(amount) {
  9. return {
  10. type: Types.MINUS,
  11. amount
  12. }
  13. }
  • 有了 action-creator 以后,在 dispatch 时只需要调用 action-creator 并且传入 payload 即可;
  • 示例:
  1. // add 和 minus 就是 action-creator,而 1 是传递给 上面的 amount,即 payload
  2. <button onClick={() => store.dispatch(add(1))}>+</button>
  3. <span>{this.state.num}</span>
  4. <button onClick={() => store.dispatch(minus(1))}>-</button>

六、文件拆分

React 的模块化特性很强,对应着 redux 的使用也需要拆分成模块化达到代码解耦的目的,上例中的 store 我们拆分文件后的结构如下;

6.1 文件结构

  1. store
  2. index.js 导出 store
  3. ├─action action-creator
  4. counter.js
  5. todo.js
  6. ├─action-type action 定义
  7. index.js
  8. └─reducer 存放 reducer
  9. counter.js
  10. todo.js

/store/index.js

  1. import {createStore, combineReducers} from 'redux'
  2. import { counter } from './reducer/counter'
  3. import { todo } from './reducer/todo'
  4. let combinedReducer = combineReducers({
  5. todo: todo,
  6. counter: counter
  7. })
  8. let store = createStore(combinedReducer)
  9. export default store

store/action/counter.js

  1. import * as Types from '../action-type'
  2. function add(amount) {
  3. return {
  4. type: Types.ADD,
  5. amount
  6. }
  7. }
  8. function minus(amount) {
  9. return {
  10. type: Types.MINUS,
  11. amount
  12. }
  13. }
  14. export { add, minus }

store/action/todo.js

  1. import * as Types from '../action-type'
  2. function addTodo(content) {
  3. return {
  4. type: Types.ADD_TODO,
  5. content
  6. }
  7. }
  8. export { addTodo }

store/action-types/index

  1. export const ADD = 'ADD'
  2. export const MINUS = 'MINUS'
  3. export const ADD_TODO = 'ADD_TODO'

store/reducer/counter

  1. import * as Types from '../action-type'
  2. export function counter(state = {num: 0}, action) {
  3. switch (action.type) {
  4. case Types.ADD:
  5. return {
  6. num: state.num + action.amount
  7. }
  8. case Types.MINUS:
  9. return {
  10. num: state.num - action.amount
  11. }
  12. }
  13. // 使用 reducer 首先需要返回一个默认状态
  14. return state
  15. }

/store/reducer/todo.js

  1. import * as Types from '../action-type'
  2. let todoState = {
  3. list: [],
  4. filter: 'all'
  5. }
  6. export function todo(state = todoState, action) {
  7. switch (action.type) {
  8. case Types.ADD_TODO:
  9. return {
  10. ...state,
  11. list: [ // 这个 list 会覆盖上面 ... 出来的 list
  12. ...state.list,
  13. action.content
  14. ]
  15. }
  16. }
  17. return state
  18. }

七、react-redux

上面虽然实现了数据的统一管理,但是代码组织很繁琐,为了解决这个问题我们使用一个另一个工具,react-redux;

7.1 安装 react-redux

  1. 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

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import './index.css';
  4. import { Provider } from 'react-redux'
  5. import store from './store'
  6. import Counter from './components/Counter'
  7. ReactDOM.render(<Provider store={store}>
  8. <Counter />
  9. </Provider>, document.getElementById('root'))

7.4.2 使用 connect 导出一个连接后的组件

  • 7.3.2.1 使用 connect 首先从 react-redux 导出 connect;
  1. 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 示例:
  1. import React, { Component } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import Computed from './Computed'
  4. import Todo from './Todo'
  5. import { add, minus} from '../store/action/counter'
  6. import { connect } from 'react-redux'
  7. // 使用 react-redux 需要导出一个连接后的组件
  8. class Counter extends Component {
  9. render () {
  10. return (<div className="container">
  11. <button onClick={() => this.props.add(1)}>+</button>
  12. <span>{this.props.num}</span>
  13. <button onClick={() => this.props.minus(1)}>-</button>
  14. <Computed />
  15. <Todo />
  16. </div>)
  17. }
  18. }
  19. // react-redux 的 connect 方法接收两个参数,let connect = > (m1, m2) => (component) => {}
  20. // 第一次执行传入两个回调函数:
  21. // 1. mapStateToProps 把 store 的状态映射称为组件的 props
  22. // 2. mapDispatchToProps 把 dispatch 映射为 prop
  23. // 第二次执行,传入组件名 Counter
  24. let mapStateToProps = (state) => {
  25. // mapStateToProps 的参数 state 就是合并后的状态对象
  26. // 在 mapStateToProps 函数中要返回一个对象,这些对象中是数据都会成为对应组件的 props
  27. return {
  28. num: state.counter.num
  29. }
  30. }
  31. let mapDispatchToProps = (dispatch) => {
  32. // mapDispatchToProps 的参数是 dispatch 就是派发行为的 store.dispatch 方法
  33. // mapDispatchToProps 需要返回一个对象,这个对象中
  34. return {
  35. add: (amount) => dispatch(add(amount)),
  36. minus: (amount) => dispatch(minus(amount))
  37. }
  38. }
  39. export default connect(mapStateToProps, mapDispatchToProps)(Counter)

7.5 mapStateToProps 和 mapDispatchToProps 有简便写法

  • mapStateToProps 可以写成一个 箭头函数,在箭头函数中使用 … 运算符展开某个状态对象
  • mapDispatchToProps 可以传入一个 actionCreator 对象,示例
  1. export default connect(state => ({...state.counter}), {add, minus})(Counter)