⑧ React Redux 基本用法

末蓝、 2022-10-29 15:21 259阅读 0赞

查看专栏其它文章:

① 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 是什么?

  1. redux 是一个独立专门用于做状态管理的 JS 库(不是 react 插件库)
  2. 它可以用在 react, angular, vue 等项目中, 但基本与 react 配合使用
  3. 作用: 集中式管理 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?

  1. 总体原则:能不用就不用,如果不用比较吃力才考虑使用
  2. 某个组件的状态需要共享
  3. 某个状态需要在任何地方都可以拿到
  4. 一个组件需要改变全局状态
  5. 一个组件需要改变另一个组件的状态

基础用法

在本例中将实现下图功能:次数的值需要通过加减按钮进行变化,加减数量由左侧选择框决定。且当次数为奇数时,点击 increment if odd 按钮才会变化;点击 increment async 将会通过 定时器 setTimeout 异步增加次数。
在这里插入图片描述
为了比对 React 和 Redux 的区别,将会编写这两种代码。


React 版

内容比较简单,项目结构如下:

(如果对基本用法感到困惑,可参考我的 ① ~ ④ 测试语法文章)
在这里插入图片描述
index.js:

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. import App from './components/app'
  4. ReactDOM.render(<App/>, document.getElementById('root'))

app.jsx:

  1. import React, { Component} from 'react'
  2. export default class App extends Component {
  3. state = {
  4. count: 0
  5. }
  6. increment = () => {
  7. const num = this.refs.numSelect.value*1
  8. const count = this.state.count + num
  9. this.setState({ count})
  10. }
  11. decrement = () => {
  12. const num = this.refs.numSelect.value*1
  13. const count = this.state.count - num
  14. this.setState({ count})
  15. }
  16. incrementIfOdd = () => {
  17. let count = this.state.count
  18. if(count%2==1) {
  19. const num = this.refs.numSelect.value*1
  20. count += num
  21. this.setState({ count})
  22. }
  23. }
  24. incrementAsync = () => {
  25. setTimeout(() => {
  26. const num = this.refs.numSelect.value*1
  27. const count = this.state.count + num
  28. this.setState({ count})
  29. }, 1000)
  30. }
  31. render () {
  32. const { count} = this.state
  33. return (
  34. <div>
  35. <p>
  36. click { count} times
  37. </p>
  38. <select ref="numSelect">
  39. <option value="1">1</option>
  40. <option value="2">2</option>
  41. <option value="3">3</option>
  42. </select>{ ' '}
  43. <button onClick={ this.increment}>+</button>{ ' '}
  44. <button onClick={ this.decrement}>-</button>{ ' '}
  45. <button onClick={ this.incrementIfOdd}>increment if odd</button>{ ' '}
  46. <button onClick={ this.incrementAsync}>increment async</button>
  47. </div>
  48. )
  49. }
  50. }

Redux 版(相关用法)

在具体看代码前,先了解一下 Redux 的相关概念和用法。

之后如果突然对某步骤感到困惑,可参照该图记忆:
在这里插入图片描述
为了方便说明,在这里展示本例的项目结构:
在这里插入图片描述
redux 的三个核心概念:

(1)Action:

定义:Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch(action) 将 action 传到 store。( 但是需要注意,Action并没有直接修改 state,想要修改需要 Reducers)

而 dispatch 中的 action 应该需要包含两个方面的属性。

  1. type:用来表示将要执行的动作,其值为字符串,且是必要的唯一属性。多数情况下,type 会被定义成字符串常量。
  2. xxx:数据属性,值类型任意,是可选属性。

例:

  1. store.dispatch({ type: INCREMENT, data: number})

当应用规模越来越大时,建议使用单独的模块或文件来存放 action。因此在项目结构中创建 actions.js 和 action-types.js。综上所述,在 actions.js 中就是这样的内容:(这就是 Action Creators 模块:创建 Action 的工厂函数)

  1. /*action creator模块*/
  2. import { INCREMENT, DECREMENT} from './action-types'
  3. export const increment = number => ({ type: INCREMENT, number})
  4. export const decrement = number => ({ type: DECREMENT, number})

action-types.js 中是这样的内容:

  1. /*Action对象的type常量名称模块*/
  2. export const INCREMENT = 'increment'
  3. export const DECREMENT = 'decrement'

在这里的 action-types.js 是样板文件。像这样使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。

改进后,store.dispatch 这么用:

  1. import * as actions from '../redux/actions'
  2. store.dispatch(actions.increment(number)

也就是:我像 store 传递了一个 increment 标识 和 number 参数,之后 Reducers 就可以根据这个标识和参数,对原本的状态进行更新。

(2)Reducers:

定义:Reducers 指定了应用状态的变化如何响应 Action 并发送到 Store 的,即:根据原本的 State 和 Action,去产生新的 State。

reducers.js 中的内容:

  1. /*根据原本的state和指定action, 处理返回一个新的state*/
  2. import { INCREMENT, DECREMENT} from './action-types'
  3. export function counter(state = 0, action) {
  4. console.log('counter', state, action)
  5. switch (action.type) {
  6. case INCREMENT:
  7. return state + action.number
  8. case DECREMENT:
  9. return state - action.number
  10. default:
  11. return state
  12. }
  13. }

在这里,Reducers 也就是 counter,共有 2 个参数。其中 state 就是存储的状态,action 就是从 Actions 传递过来的内容。传递过来的内容如下:
在这里插入图片描述
我们可以看到,Reducers 部分 接收到了 type标识 和传递过来的参数。现在我们需要根据标识来对状态进行更新,但是在这里需要注意,虽然我们可以调用 state,但是不要修改原来的这个状态,需要通过 return 的方式返回一个新的状态,就像上面的代码所示。为了方便判断标识,才选择使用 switch case。

(3)Stores:

在上面的 Reducers 之所以能够接收到这些信息,是因为有 Store 的帮助。Store 是 redux 库最核心的管理对象。在它的内部维护着 状态 state 和 reducer。( 不要忘记之前在工作流程中提到,Store 不能直接修改状态,需要通过 Reducers )

如何得到此对象:

  1. import { createStore} from 'redux'
  2. import reducer from './reducers'
  3. 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:

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. import { createStore} from 'redux'
  4. import App from './components/app'
  5. import { counter} from './redux/reducers'
  6. // 根据counter函数创建store对象
  7. const store = createStore(counter)
  8. // 定义渲染根组件标签的函数
  9. const render = () => {
  10. ReactDOM.render(
  11. <App store={ store}/>,
  12. document.getElementById('root')
  13. )
  14. }
  15. // 初始化渲染
  16. render()
  17. // 注册(订阅)监听, 一旦状态发生改变, 自动重新渲染
  18. store.subscribe(render)

action-types.js:

  1. /* Action对象的type常量名称模块 */
  2. export const INCREMENT = 'increment'
  3. export const DECREMENT = 'decrement'

actions.js:

  1. /* action creator模块 */
  2. import { INCREMENT, DECREMENT} from './action-types'
  3. export const increment = number => ({ type: INCREMENT, number})
  4. export const decrement = number => ({ type: DECREMENT, number})

reducers.js:

  1. /* 根据老的state和指定action, 处理返回一个新的state */
  2. import { INCREMENT, DECREMENT} from './action-types'
  3. export function counter(state = 0, action) {
  4. console.log('counter', state, action)
  5. switch (action.type) {
  6. case INCREMENT:
  7. return state + action.number
  8. case DECREMENT:
  9. return state - action.number
  10. default:
  11. return state
  12. }
  13. }

app.jsx:

  1. /* 应用组件 */
  2. import React, { Component} from 'react'
  3. import PropTypes from 'prop-types'
  4. import * as actions from '../redux/actions'
  5. export default class App extends Component {
  6. static propTypes = {
  7. store: PropTypes.object.isRequired,
  8. }
  9. increment = () => {
  10. const number = this.refs.numSelect.value * 1
  11. this.props.store.dispatch(actions.increment(number))
  12. }
  13. decrement = () => {
  14. const number = this.refs.numSelect.value * 1
  15. this.props.store.dispatch(actions.decrement(number))
  16. }
  17. incrementIfOdd = () => {
  18. const number = this.refs.numSelect.value * 1
  19. let count = this.props.store.getState()
  20. if (count % 2 === 1) {
  21. this.props.store.dispatch(actions.increment(number))
  22. }
  23. }
  24. incrementAsync = () => {
  25. const number = this.refs.numSelect.value * 1
  26. setTimeout(() => {
  27. this.props.store.dispatch(actions.increment(number))
  28. }, 1000)
  29. }
  30. render() {
  31. return (
  32. <div>
  33. <p>
  34. click { this.props.store.getState()} times { ' '}
  35. </p>
  36. <select ref="numSelect">
  37. <option value="1">1</option>
  38. <option value="2">2</option>
  39. <option value="3">3</option>
  40. </select>{ ' '}
  41. <button onClick={ this.increment}>+</button>
  42. { ' '}
  43. <button onClick={ this.decrement}>-</button>
  44. { ' '}
  45. <button onClick={ this.incrementIfOdd}>increment if odd</button>
  46. { ' '}
  47. <button onClick={ this.incrementAsync}>increment async</button>
  48. </div>
  49. )
  50. }
  51. }

其实 Redux 存在一些问题:

  1. Redux 与 React 组件的代码耦合度太高
  2. 编码不够简洁

为了解决这个问题,出现了 React-Redux。


React-Redux 版(相关用法)

React-Redux 是 React 的插件库。它专门用来简化 React 应用中使用 Redux。

使用 react-redux 下载依赖包:npm install --save react-redux

React-Redux 将所有组件分成两大类:

  1. UI 组件
    a. 只负责 UI 的呈现,不带有任何业务逻辑
    b. 通过 props 接收数据( 一般数据和函数 )
    c. 不使用任何 Redux 的 API
    d. 一般保存在 components 文件夹下
  2. 容器组件
    a. 负责管理数据和业务逻辑,不负责 UI 的呈现
    b. 使用 Redux 的 API
    c. 一般保存在 containers 文件夹下

综上所述,我们先创建项目结构:
在这里插入图片描述
action-types.js 、actions.js、reducers.js 和之前没有区别。

  1. export const INCREMENT = 'increment'
  2. export const DECREMENT = 'decrement'
  3. /* action creator模块 */
  4. import { INCREMENT, DECREMENT} from './action-types'
  5. export const increment = number => ({ type: INCREMENT, number})
  6. export const decrement = number => ({ type: DECREMENT, number})
  7. import { INCREMENT, DECREMENT} from './action-types'
  8. export function counter(state = 0, action) {
  9. console.log('counter', state, action)
  10. switch (action.type) {
  11. case INCREMENT:
  12. return state + action.number
  13. case DECREMENT:
  14. return state - action.number
  15. default:
  16. return state
  17. }
  18. }

接下来看 React-Redux 到底做了什么简化。

index.js

相比于 Redux 版本,现在不需要注册监听 store.subscribe(render),但是需要使用 Provider:

  1. <Provider store={store}>
  2. <App />
  3. </Provider>

这将能够让所有组件都可以得到 state 数据

完整代码:

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. import { createStore} from 'redux'
  4. import { Provider} from 'react-redux'
  5. import App from './containers/app'
  6. import { counter} from './redux/reducers'
  7. // 根据counter函数创建store对象
  8. const store = createStore(counter)
  9. // 定义渲染根组件标签的函数
  10. ReactDOM.render(
  11. (
  12. <Provider store={ store}>
  13. <App/>
  14. </Provider>
  15. ),
  16. document.getElementById('root')
  17. )

app.jsx

对于 app.jsx,在这里它是容器组件,负责管理数据和业务逻辑,不负责 UI 的呈现。在这里会用到 connect。它专门用于包装 UI 组件生成容器组件。用法如下:

  1. import { connect } from 'react-redux'
  2. connect(
  3. mapStateToprops,
  4. mapDispatchToProps
  5. )(Counter)

在这里,mapStateToprops 处就可以将外部的数据(即 state 对象)转换为 UI 组件的标签属性;mapDispatchToProps 处就可以将分发 action 的函数转换为 UI 组件的标签属性。然后将这些信息传递给 Counter。

完整代码:

  1. /* 包含Counter组件的容器组件 */
  2. import React from 'react'
  3. // 引入连接函数
  4. import { connect} from 'react-redux'
  5. // 引入action函数
  6. import { increment, decrement} from '../redux/actions'
  7. import Counter from '../components/counter'
  8. // 向外暴露连接App组件的包装组件
  9. export default connect(
  10. state => ({ count: state}),
  11. { increment, decrement}
  12. )(Counter)

counter.jsx

随后,在 UI 组件 counter.jsx 中 就可以只负责 UI 的呈现,不带有任何业务逻辑,直接通过 props 接收数据。

完整代码:

  1. /* UI组件: 不包含任何redux API */
  2. import React from 'react'
  3. import PropTypes from 'prop-types'
  4. export default class Counter extends React.Component {
  5. static propTypes = {
  6. count: PropTypes.number.isRequired,
  7. increment: PropTypes.func.isRequired,
  8. decrement: PropTypes.func.isRequired
  9. }
  10. increment = () => {
  11. const number = this.refs.numSelect.value * 1
  12. this.props.increment(number)
  13. }
  14. decrement = () => {
  15. const number = this.refs.numSelect.value * 1
  16. this.props.decrement(number)
  17. }
  18. incrementIfOdd = () => {
  19. const number = this.refs.numSelect.value * 1
  20. let count = this.props.count
  21. if (count % 2 === 1) {
  22. this.props.increment(number)
  23. }
  24. }
  25. incrementAsync = () => {
  26. const number = this.refs.numSelect.value * 1
  27. setTimeout(() => {
  28. this.props.increment(number)
  29. }, 1000)
  30. }
  31. render() {
  32. return (
  33. <div>
  34. <p>
  35. click { this.props.count} times { ' '}
  36. </p>
  37. <select ref="numSelect">
  38. <option value="1">1</option>
  39. <option value="2">2</option>
  40. <option value="3">3</option>
  41. </select>{ ' '}
  42. <button onClick={ this.increment}>+</button>
  43. { ' '}
  44. <button onClick={ this.decrement}>-</button>
  45. { ' '}
  46. <button onClick={ this.incrementIfOdd}>increment if odd</button>
  47. { ' '}
  48. <button onClick={ this.incrementAsync}>increment async</button>
  49. </div>
  50. )
  51. }
  52. }

现在只剩下一个问题,Redux 默认是不能进行异步处理的,但是在我们的例子中又需要在 Redux 中执行异步任务(ajax, 定时器),所以 Redux 的异步编程就出现了。


Redux 异步编程

使用前需要下载 Redux 插件(异步中间件)npm install --save redux-thunk

index.js 中,首先需要应用上异步中间件。这里需要使用 applyMiddleware() ,它的作用就是应用上基于 redux 的中间件 ( 插件库 ),在这里就是应用 thunk。

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. import { Provider} from 'react-redux'
  4. import App from './containers/app'
  5. import { createStore, applyMiddleware} from "redux";
  6. import thunk from 'redux-thunk'
  7. import reducers from "./redux/reducers";
  8. // 根据counter函数创建store对象
  9. const store = createStore(
  10. reducers,
  11. applyMiddleware(thunk) // 应用上异步中间件
  12. )
  13. // 定义渲染根组件标签的函数
  14. ReactDOM.render(
  15. (
  16. <Provider store={ store}>
  17. <App/>
  18. </Provider>
  19. ),
  20. document.getElementById('root')
  21. )

随后将异步代码放入 action 中。

在这里需要注意,同步的action只需要返回一个对象,而对于异步的action需要返回一个函数。

  1. /*action creator模块*/
  2. import { INCREMENT, DECREMENT} from './action-types'
  3. export const increment = number => ({ type: INCREMENT, number})
  4. export const decrement = number => ({ type: DECREMENT, number})
  5. // 异步action creator(返回一个函数)
  6. export const incrementAsync = number => {
  7. return dispatch => {
  8. setTimeout(() => {
  9. dispatch(increment(number))
  10. }, 1000)
  11. }
  12. }

然后将 action 传递给 UI组件。

  1. /* 包含Counter组件的容器组件 */
  2. import React from 'react'
  3. // 引入连接函数
  4. import { connect} from 'react-redux'
  5. // 引入action函数
  6. import { increment, decrement, incrementAsync} from '../redux/actions'
  7. import Counter from '../components/counter'
  8. // 向外暴露连接App组件的包装组件
  9. export default connect(
  10. state => ({ count: state.counter}),
  11. { increment, decrement, incrementAsync}
  12. )(Counter)

之后去 UI 组件中调用。

  1. incrementAsync = () => {
  2. const number = this.refs.numSelect.value*1
  3. this.props.incrementAsync(number)
  4. }

其它用法

Reducers 中会不止一个状态,此时为了方便创建 Store 对象,我们可以使用 combineReducers(),它用来合并多个 reducer 函数。

  1. import { combineReducers} from 'redux'
  2. // ....
  3. export default combineReducers({
  4. user, chatUser, chat
  5. })
  6. import { createStore} from "redux";
  7. import reducers from "./redux/reducers";
  8. // 创建store对象
  9. const store = createStore(reducers)

在 chrome 浏览器上 使用 redux 调试工具,首先需要为浏览器下载 redux-devtools_2_15_1.crx 。随后在项目中还要npm install --save-dev redux-devtools-extension。代码:

  1. import { composeWithDevTools } from 'redux-devtools-extension'
  2. const store = createStore(
  3. counter,
  4. composeWithDevTools(applyMiddleware(thunk))
  5. )

发表评论

表情:
评论列表 (有 0 条评论,259人围观)

还没有评论,来说两句吧...

相关阅读

    相关 react redux

    在react中使用redux入门。 基本的例子使用redux管理react的状态。 <!DOCTYPE html> <html> <head>