React-Redux 快速入门

1,326 阅读11分钟

1. 提出案例

一个累加器和一个用户信息列表:

累加器:具有自增、自减和异步加的功能;

用户信息列表:具有修改和删除用户信息的功能。

2. 涉及相关库

redux 、 react-redux、 redux-thunk

3. Redux 数据流

首先了解一下Redux数据流,redux属于“单向数据流”,它描述了更新应用程序的以下步骤序列:

  • State 描述了应用程序在特定时间点的状况
  • 基于 state 来渲染视图
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新
  • 基于新的 state 重新渲染视图

具体来说,对于 Redux,我们可以将这些步骤分解为更详细的内容:

  • 初始启动:

    • 使用最顶层的 root reducer 函数创建 Redux store
    • store 调用一次 root reducer,并将返回值保存为它的初始 state
    • 当视图 首次渲染时,视图组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
  • 更新环节:

    • 应用程序中发生了某些事情,例如用户单击按钮
    • dispatch 一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'})
    • store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state
    • store 通知所有订阅过的视图,通知它们 store 发生更新
    • 每个订阅过 store 数据的视图 组件都会检查它们需要的 state 部分是否被更新。
    • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

动画的方式来表达数据流更新:

ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

4. 涉及相关API

4.1. createStore

createStore 是Redux 核心库的一个API,它可以创建一个 react store。Redux store 汇集了构成应用程序的 state、actions 和 reducers。store 有以下几个职责:

重要的是要注意 Redux 应用程序中只有一个 store。当你想要拆分数据处理逻辑时,你将使用 reducer composition 并创建多个可以组合在一起reducer,而不是创建单独的 store。

4.2. Reducer切片、RootReducer 和 combineReducers

4.2.1. Reducer切片

大多数时候,一个应用了 redux 的项目其维护的 state 数据量都比较大,而维护的数据有一些数据的耦合度不高,这个时候我们就可以通过reducer切片的方式,按照业务的不同,创建reducer切片,使得不同的业务板块可以独立的维护自己的 state 。例如,下面5.2的 ⑤ store/reducer/counterSlice.js ⑥ store/reducer/personSlice.js 的切片。

4.2.2. RootReducer

在4.2.1中我们获得了两个独立的 slice 文件后,每个文件都有自己的 slice reducer 函数。但是,Redux 存储在创建时需要一个 根reducer 函数。那么,如何才能在不将所有代码放在一个大函数中的情况下重新使用根 redux 呢?

由于 reducers 是一个普通的 JS 函数,我们可以将 slice reducer 重新导入 reducer.js,并编写一个新的根 reducer,它唯一的工作就是调用其他两个函数。

import counterReducer from "./counterSlice";
import personReducer from "./personSlice";

export default function rootReducer(state = {}, action) {
  // 返回一个新的根 state 对象
  return {
    // `state.counter` 的值是 counter reducer 返回的值
    // 对于这两个reducer,我们只传入它们的状态 slice
    counter: counterReducer(state.counter, action),
    person: personReducer(state.person, action)
  }
}

这些 reducer 中的每一个都在管理自己的全局状态部分。每个 reducer 的 state 参数都是不同的,并且对应于它管理的 state 部分。

4.2.3. combineReducers 合并 Reducer

新的根 reducer 对每个 slice 都做同样的事情:调用 slice reducer,传入属于该 reducer 的 state slice,并将结果分配回根 state 对象。如果要添加更多 slice ,则重复该模式。

Redux 核心库包含一个名为 combineReducers 的实用程序,它为我们执行相同的样板步骤。我们可以用 combineReducers 生成的较短的 rootReducer 替换手写的 rootReducer

import { combineReducers } from "redux";
import counterReducer from "./counterSlice";
import personReducer from "./personSlice";

const rootReducer = combineReducers({
  // 定义一个名为`counter`的顶级状态字段,由`counterReducer`处理
  counter: counterReducer,
  persons: personReducer
})

export default rootReducer

4.3. applyMiddleware 和 composeWithDevTools

  • applyMiddleware: 顾名思义,应用一个中间件,主要用于应用一些额外的中间件,增强redux的灵活性,这里主要是想为程序应用redux-thunk中间件
  • composeWithDevTools: 是 Redux 专门设计用于更容易理解你的 state 何时、何地、为何以及如何随时间变化的工具,,它向你显示 dispatch 了哪些 action 的历史记录,这些操作包含什么,以及在每个 dispatch action 之后 state 如何变化。配合浏览器 Redux DevTools 拓展使用。

image.png

4.4. react-thunk (Redux异步操作)

Redux store 本身无法处理异步逻辑。它只会同步地 dispatch action,并通过调用根 reducer 函数来更新 state,然后通知视图更新。任何异步都必须在 store 之外发生。

Redux 官网描述说 Redux reducer 绝对不能包含“副作用”。 “副作用”是指除函数返回值之外的任何变更,包括 state 的更改或者其他行为。一些常见的副作用是:

  • 在控制台打印日志
  • 保存文件
  • 设置异步定时器
  • 发送 AJAX HTTP 请求
  • 修改存在于函数之外的某些 state,或改变函数的参数
  • 生成随机数或唯一随机 ID(例如 Math.random()Date.now()

Redux middleware 就是用来放这些副作用逻辑代码的地方

当 Redux middleware 执行 dispatch action 时,它可以做 任何事情:记录某些内容、修改 action、延迟 action,进行异步调用等。此外,由于 middleware 围绕真正的 store.dispatch 函数形成了一个管道,这也意味着我们实际上可以将一些 不是 普通 action 对象的东西传递给 dispatch,只要 middleware 截获该值并且不让它到达 reducer。

Middleware 也可以访问 dispatchgetState。这意味着你可以在 middleware 中编写一些异步逻辑,并且仍然能够通过 dispatch action 与 Redux store 进行交互。

middleware 和异步逻辑如何影响 Redux 应用程序的整体数据流的呢?

就像普通 action 一样,首先需要在应用程序中处理用户事件,比如点击按钮。然后,调用 dispatch(),并传入 一些内容,无论是普通的 action 对象、函数,还是 middleware 可以找到的其他值。

一旦 dispatch 的值到达 middleware,它就可以进行异步调用,然后在异步调用完成时 dispatch 一个正真的 action 对象。

前面,我们看了Redux 同步数据流程图。当我们向 Redux 应用程序添加异步逻辑时,额外添加了 middleware 步骤,可以在其中运行 AJAX 请求等逻辑,然后 dispatch action。这使得异步数据流看起来像这样:

ReduxAsyncDataFlowDiagram-d97ff38a0f4da0f327163170ccc13e80.gif

Redux Thunk Middleware

在没有redux-thunk middleware之前,我们每一次在redux中写异步任务都需要自己手写一个处理异步任务的middleware(官网解释)。而实际上,Redux 已经有了异步函数 middleware 的正式版本,称为 Redux “Thunk” middleware。thunk middleware 允许我们编写以 dispatchgetState 作为参数的函数。thunk 函数可以包含我们想要的任何异步逻辑,并且该逻辑可以根据需要 dispatch action 以及读取 store state。

4.5. 整体配置 redux-thunk

4.5.1. 安装依赖包

    npm install redux react-redux
    npm install redux-thunk 
    npm install redux-devtools-extension 

4.5.2. 配置

 import { legacy_createStore as createStore, applyMiddleware } from "redux";
    import counterReducer from "./counterReducer";
    import thunk from "redux-thunk";
    import { composeWithDevTools } from 'redux-devtools-extension'//添加增强剂(thunk中间件)
    const middleEnhancer = composeWithDevTools(applyMiddleware(thunk)) 
    ​
    export default createStore(counterReducer, middleEnhancer)
    //相当于
    //const store = createStore(counterReducer, middleEnhancer)
    //export default store

4.6. connet

connet详解请移步至官网:Connect | React Redux 中文文档 (react-redux.js.org)

在react组件中,使用 redux 中的数据和方法是不会触发 render 函数的,所以需要在组件中手动订阅 redux 和 手动触发 state 更新,继而触发 render 函数。

   componentDidMount(){
       //检测redux中状态的变化,只要变化,就调用render
       store.subscribe(()=>{
           this.setState({})
       })
   }

为了解决这种困境,redux 设计出了另一种模式:通过给 react 组件(UI 组件)包裹一层容器wrapper(不同于UI组件,此容器主要用于数据交互),实现 react 组件能有与 redux 实时通讯的效果。而 connet api 主要起到的作用是把 UI 组件和容器连接起来。

image.png

4.6.1. 基本用法

connet(mapStateToProps, mapDispatchToProps)(需要连接的React UI组件)
  • connet的前两个参数都是一个函数
  • mapStateToProps:mapStateToProps函数返回的是一个对象;返回的对象中的 key 就作为传递给UI组件 props 的 key ,value 就作为传递给UI组件 props 的 value;mapStateToProps 用于传递状态(即把 redux 中 reducer 整合到 props 中往 React UI组件传递)
  • mapDispatchToProps:mapDispatchToProps函数返回的是一个对象;返回的对象中的key 就作为传递给 UI 组件 props 的 key ,value 就作为传递给 UI 组件 props 的 value;
  • mapDispatchToProps用于传递操作状态的方法(即把 action 整合到 props 中往 React UI组件传递)
//引入写好的actions
    import {
        increment,
        decrement,
        incrementAsync
    } from '../../redux/count_action'/// CountUI组件代码...//默认第一个参数是 state
    function mapStateToProps( state ){
        //do something...
        return {counter:state}
    }
    ​
    //默认第一个参数是 dispatch
    function mapDispatchToProps( dispatch ){
        //do something...
        return {
            increment:number => dispatch(increment(number)),
            decrement:number => dispatch(decrement(number)),
            incrementAsync:(number,time) => dispatch(incrementAsync(number,time)),
        }
    }
    ​
    //使用connect()()创建并暴露一个Count的容器组件
    export default connect(mapStateToProps,mapDispatchToProps)(incrementAsync)

效果: 可以在 UI 组件中直接通过 this.props 调用到对应的方法

image.png

4.6.2. 简化写法

一般写法:

//使用connect()()创建并暴露一个Count的容器组件
   export default connect(
       state => ({count:state}),
       dispatch => ({
           increment:number => dispatch(increment(number)),
           decrement:number => dispatch(decrement(number)),
           incrementAsync:(number,time) => dispatch(incrementAsync(number,time)),
       }) 
   )(Count UI)

简写:

 export default connect(
       state => ({count:state}),
       //mapDispatchToProps的简写(注意:这里会自动包裹dispath)
       {
           increment:increment,
           decrement:decrement,
           incrementAsync:incrementAsync,
       }
   )(Count UI)
   ​
   //           更加优雅一点 👇↓↓👇export default connect(
       state => ({count:state}),
       //mapDispatchToProps的简写(注意:这里会自动包裹dispath)
       {
           increment,
           decrement,
           incrementAsync
       }
   )(Count UI)

4.7. provide

<Provider> 组件使 Redux store 可用于任何需要访问 Redux store 的嵌套组件。

由于 React Redux 应用中的任何 React 组件都可以连接到 store,因此大多数应用会在顶层渲染一个 <Provider>,将整个应用的组件树包裹其中。

import React from 'react'
   import ReactDOM from 'react-dom'
   import { Provider } from 'react-redux'
   import { App } from './App'
   import store from './store'const root = ReactDOM.createRoot(document.getElementById('root'))
   root.render(
     <Provider store={store}>
       <App />
     </Provider>
   )

5. 案例实现

5.1 目录结构

image.png

src
        │  App.css
        │  App.jsx
        │  index.css
        │  main.jsx
        │
        ├─assets
        │      react.svg
        │
        ├─component         //组件
        │      Count.jsx
        │      Person.jsx
        │
        └─store             //redux仓库
            │  store.js
            │
            ├─action        //action分类管理
            │   counter.js
            │   person.js
            │ 
            ├─constant      //action常量名分类管理
            │      countConstant.js
            │      personConstant.js
            │
            └─reducer       //reducer分片以及组合rootReducer
                    counterSlice.js
                    index.js
                    personSlice.js

5.2 创建store

① store/action/counter.js

     /* 
        该文件专门为Count组件生成action对象
    */import * as types from '../constant/countConstant'//同步action,就是指action的值为Object类型的 [一般对象]
    export const increment = payload => ({ type: types.INCREMENT,  payload})
    export const decrement = payload => ({ type: types.DECREMENT,  payload})
    ​
    ​
    //异步action,就是指action的值为 [函数] ,
    //异步action中一般都会调用 [同步action],异步action不是必须要用的。
    export const incrementAsync = (delay) => {
      return ( dispatch ) => {
        setTimeout(()=> {
          dispatch(increment())
        }, delay )
      }
    }

② store/action/person.js

   import * as types from '../constant/personConstant'//同步action,就是指action的值为Object类型的 [一般对象]
   export const updataInfo = payload => ({ type: types.UPDATAINFO,  payload})
   export const delPerson = payload => ({ type: types.DEL_PERSON,  payload})

③ store/constant/countConstant.js

   export const INCREMENT = 'increment'
   export const DECREMENT = 'decrement'

④ store/constant/personConstant.js

   export const UPDATAINFO = 'updataInfo'
   export const DEL_PERSON = 'delPerson'

⑤ store/reducer/counterSlice.js

   import * as types from "../constant/countConstant";
   ​
   const initState = {
     count: 1
   }
   ​
   export default function counterReducer(state = initState, action) {
     const { type, payload } = action
     switch (type) {
       case types.INCREMENT:
         return { count: state.count + 1 }
       case types.DECREMENT:
         return { count: state.count - 1 }
       default:
         return state;
     }
   } 

⑥ store/reducer/personSlice.js

   import * as types from "../constant/personConstant";
   ​
   const initState = [
       {
         id: 1,
         name: '张三',
         age: 18,
         gender: '男'
       },
       {
         id: 2,
         name: '李四',
         age: 20,
         gender: '男'
       },
     ]
   ​
   export default function personReducer(state = initState, action) {
     const { type, payload } = action
     switch (type) {
       case types.UPDATAINFO :
         return state.map(p => {
           if (p.id === payload.id) {
             return { ...p, ...payload }
           }
           return p
         })
       case types.DEL_PERSON:
         return state.filter(p => p.id !== payload)
       default:
         return state;
     }
   } 

⑦ store/reducer/index.js(联合reducerSlice)

import { combineReducers } from "redux";
import counterReducer from "./counterSlice";
import personReducer from "./personSlice";

const rootReducer = combineReducers({
  counter: counterReducer,
  persons: personReducer
})

export default rootReducer

⑧ store/store.js

   import { legacy_createStore as createStore, applyMiddleware } from "redux";
   import thunk from "redux-thunk";
   import { composeWithDevTools } from 'redux-devtools-extension'
   import rootReducer from '../store/reducer'const middleEnhancer = composeWithDevTools(applyMiddleware(thunk)) 
   ​
   export default createStore(rootReducer, middleEnhancer)

⑨ main.jsx

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import App from './App.jsx'
    import './index.css'
    import { Provider } from 'react-redux'
    import store from './store/store.js'

    ReactDOM.createRoot(document.getElementById('root')).render(
      <Provider store={store}>
        <App />
      </Provider>
    )

5.3 Count组件

类组件,具体的dispath action 映射到props中

image.png

    import React, { Component } from "react";
    import { increment, decrement, incrementAsync } from "../store/action/counter";
    import { connect } from "react-redux";
    class Count extends Component {
      
      increment = () => {
        //在props上调用dispath action
        this.props.increment()
      }
    ​
      decrement = () =>{
        this.props.decrement()
      }
     
      incrementAsync = () => {
        this.props.incrementAsync(2000)
      }
      
      render() {
        return (
          <div>
            <h1>当前求和为: {this.props.counter.count}</h1>
            <button onClick={this.increment}>+</button>&nbsp;
            <button onClick={this.decrement}>-</button>&nbsp;
            <button onClick={this.incrementAsync}>异步加</button>&nbsp;
          </div>
        )
      }
    }
    ​
    //使用connect()()创建并暴露一个Count的容器组件
    export default connect(
      state => ({
        counter: state.counter,
        persons: state.persons
      }),
      {
        increment, 
        decrement, 
        incrementAsync
      }
    )(Count)

5.4 Person组件

函数式组件,不把具体的dispath action 映射到 props 中,手动调用 dispath。这里只是换了一种使用方式前面的方式也是可以在函数式组件上用的。(提示:connet不传 mapDispatchToProps 参数时, 会把 dispatch 方法放到props上)

    import React, { useEffect } from "react";
    import { connect } from "react-redux";
    import { updataInfo, delPerson } from "../store/action/person";
    ​
    const Person = (props) => {
    ​
      const { dispatch,  persons} = props
      
      const onUpdata = () => {
        const newPerson = { id: 1, name: 'kellen'}
        dispatch(updataInfo(newPerson))
      }
      const onDel = () => {
        dispatch(delPerson(2))
      }
      return (
        <>
          <ul>
            { persons?.map(item => <li key={item.id}>{item.id}-{item.name}-{item.gender}</li>) }
          </ul>
          <button onClick={onUpdata}>更新第一项</button>&nbsp;
          <button onClick={onDel}>删除第二项</button>
        </>
      )
    }
    ​
    export default connect(
      state => ({ persons: state.persons })
    )(Person)

6.参考: