React中如何使用Redux(一)

122 阅读6分钟

在react中使用redux

这里我们创建两个组件,展示同一个count,分别在两个组件中对count进行操作。

1679816141812.png

项目目录结构:

1679779924304.png

安装redux:

npm install redux

代码:

// ./store/index.js
import {createStore } from "redux";
import reducer from "./reducer";
​
const store = createStore(reducer); // 创建store
export default store;
// ./store/reducer.js
import { ADD_COUNT, SUB_COUNT } from "./constants";
​
const initialState = {
    count: 100  // 初始状态
}
​
function reducer(state= initialState,action){
    switch(action.type){
        case ADD_COUNT: 
            return { ...state,count: state.count +  action.num };
        case SUN_COUNT:
            return { ...state,count: state.count - action.num };
        default:
            return state;
    }
}
export default reducer;
// ./store/actionCreators.js
import * as actionTypes from "./constants";
​
export const addCountAction = (num) => ({
  type: actionTypes.ADD_COUNT,
  num,
});
​
export const subCountAction = (num) => ({
  type: actionTypes.SUB_COUNT,
  num,
});
​
export const changeBannerAction = (banner) => ({
  type: actionTypes.CHANGE_BANNER,
  banner,
});
./store/constants.js
export const ADD_COUNT = "add_count";
export const SUB_COUNT = "sub_count";
// App.jsx
import React,{ PureComponent } from "react";
import "./App.css";
import Detail from "./pages/detail";
import Home from "./pages/home";
import store from "./store";
​
export class App extends PureComponent {
    constructor(){
        super();
        this.state = {
            // 初始化count的时候,读取state中的count
            count: store.getState().count 
        }
    }
    componentDidMount(){
        // 组件挂载的时候订阅store,监听state的变化
        // 可以在组件卸载的时候,取消订阅
        store.subscribe(()=>{
            const state = store.getState();
            this.setState({count:state.count});
        })
    }
    render() {
    const { count } = this.state;
    return (
      <div>
        <h1>App Count: {count}</h1>
        <div className="pages">
          <Home />
          <Detail />
        </div>
      </div>
    );
  }
}
​
export default App
// ./pages/home.jsx
import React, { PureComponent } from "react";
import store from "../store";
import { addCountAction } from "../store/actionCreators";
​
export class Home extends PureComponent {
  constructor() {
    super();
    this.state = {
      count: store.getState().count,
    };
  }
  componentDidMount() {
    // 订阅state的变化
    store.subscribe(() => {
      const state = store.getState();
      this.setState({ count: state.count });
    });
  }
  addcount(num){
    // 派发action,修改state
    store.dispatch(addCountAction(num))
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        <h2>Home Count: {count}</h2>
        <button onClick={e=>this.addcount(1)}>+1</button>
        <button onClick={e=>this.addcount(5)}>+5</button>
      </div>
    );
  }
}
​
export default Home;
​
// ./pages/detail.jsx
import React, { PureComponent } from "react";
import store from "../store";
import { subCountAction } from "../store/actionCreators";
​
export class Detail extends PureComponent {
  constructor() {
    super();
    this.state = {
      count: store.getState().count,
    };
  }
  componentDidMount() {
    // 初始化的时候没有展示store中的数据
    store.subscribe(() => {
      const state = store.getState();
      this.setState({ count: state.count });
    });
  }
  subCount(num){
    store.dispatch(subCountAction(num))
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        <h2>Detail Count: {count}</h2>
        <button onClick={()=>this.subCount(1)}>-1</button>
        <button onClick={()=>this.subCount(8)}>-8</button>
      </div>
    );
  }
}
​
export default Detail;
​

分析上面的代码,我们发现三个组件中有很多重复的逻辑部分。

但是核心的主要是两个:

  1. 在componentDidMount中订阅数据的变化,当数据发生变化的时候重置count;
  2. 在发生点击事件的时候,调用store.dispatch来派发对应的action;

这里我想是不是能自定义一个高阶组件,把相同代码逻辑抽取到高阶组件中。

// ./hoc/getStore.js
import React, { PureComponent } from "react";
import store from "../store";
import { addCountAction, subCountAction } from "../store/actionCreators";
​
function getStore (WrapperComponent) {
    class NewComponent extends PureComponent {
        constructor(){
            super();
            this.state = {
                count: store.getState().count, // 获取初始化state
            };
            this.unSubScribe = null;
        }
        componentDidMount() {
            // 在组件挂载的时候订阅state
            this.unSubscribe = store.subscribe(() => {
            const state = store.getState();
            // 当count发生变化,更新组件内部的count
            this.setState({ count: state.count });
          });
        }
        componentWillUnmount() {
          // 在组建卸载的时候,取消订阅
          this.unSubscribe();
        }
        // 点击事件处理函数
        handleClick(num, isAdd) {
          if (isAdd) {
            // 派发action
            store.dispatch(addCountAction(num))
          } else{
            store.dispatch(subCountAction(num))
          }
        }
        render() {
          return (
            <WrapperComponent
              {...this.props}
              {...this.state}
              changeCountClick={(num,isAdd) => this.handleClick(num,isAdd)}
            />
          );
        }
    }
    return NewComponent;
}
// 导出高阶组件
export default getStore;

然后组件中可以直接调用高阶组件:

// ./pages/test.jsximport React, { PureComponent } from "react";
import getStore from "../hoc/getStore";
​
export class Test extends PureComponent {
    // 点击事件的处理方法
    changeCount(num,isAdd){
        this.props.changeCountClick(num,isAdd)
    }
  render() {
    const { count } = this.props;
    return (
      <div>
        <h2>Test Count:{count}</h2>
        <button onClick={()=>this.changeCount(3,true)}>+3</button>
        <button onClick={()=>this.changeCount(5,false)}>-5</button>
      </div>
    );
  }
}
​
export default getStore(Test);

但其实我们可以不用自定义,有现成的库帮助解决,下面介绍一个很好用的库。

react-redux的使用

redux为了和react结合的更好,redux官方提供了react-redux的库,可以在直接在项目中使用。

安装react-redux:

npm install react-redux

由于应用中的任何react组件都可以连接到store,因此在顶层用一个<Provider>,将整个应用的组件树包裹起来。

// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
​
const root = ReactDOM.createRoot(document.getElementById("root"));
// 不用每一个组件都引入一次store
root.render(
    <Provider store={store}>
      <App />
    </Provider>
);

connect()函数将react组件连接到store,它像连接的组件提供其需要从store中获取的数据片段,以及可以用来向 store dispatch actions的功能。

connent()接受四种不同的、皆可选的参数;

  • mapStateToProps?:Function
  • mapDispatchToProps?:Function | Object
  • mergeProps?:Function
  • options?:Object

mapStateToProps 和 mapDispatchToProps 分别处理 Redux store 的 state 和 dispatch。state 和 dispatch 将作为第一个参数提供给 mapStateToProps 或 mapDispatchToProps 函数。

// ./pages/about.jsx
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addCountAction, subCountAction } from "../store/counter";
​
export class About extends PureComponent {
  calcCount(num, isAdd) {
    if (isAdd) {
       // 调用映射进props的函数
      this.props.addCount(num);
    } else {
      this.props.subCount(num);
    }
  }
  render() {
    const { count } = this.props;
    return (
      <div>
        <h2>About Count:{count}</h2>
        <button onClick={() => this.calcCount(6, true)}>+6</button>
        <button onClick={() => this.calcCount(6, false)}>-4</button>
      </div>
    );
  }
}
​
const mapStateToProps = (state) => ({
  count: state.count,
});
const mapDispatchToProps = (dispatch) => ({
  addCount: (num) => {
    // addCount是一个函数
    dispatch(addCountAction(num));
  },
  subCount: (num) => {
     // 派发action
    dispatch(subCountAction(num));
  },
});
// connect本身是一个高阶函数,接受两个函数作为参数,映射到props中
// connect返回值是一个高阶组件;
export default connect(mapStateToProps, mapDispatchToProps)(About);
​
​

异步操作

组件中的异步操作

前面的案例中,redux中保存的count是一个本地定义的数据,我们直接通过同步的操作来dispatch action,state就会被立即更新。

但是在真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求操作。现在我们来看看,在组件中发起请求获取数据的操作。

// ./pages/category.jsx
import React, { PureComponent } from 'react'
import axios from "axios";
import { connect } from "react-redux";
import { changeBannerAction } from '../store/actionCreators';
​
export class Category extends PureComponent {
  // 组件加载
  componentDidMount(){
      // 在组件挂载的时候向服务器发起请求
    axios.get("http://XXXX").then(res=>{
        const banner  = res.data.data;
        // 调用函数,派发action
        this.props.changeBanner(banner);
    })
  }
  render() {
    return (
      <div>Category</div>
    )
  }
}
const mapDispatchToProps =(dispatch)=>({
    changeBanner:(data)=>{
        dispatch(changeBannerAction(data))
    }
})
export default connect(null,mapDispatchToProps)(Category)
// ./store/actionCreators.js
// ..........
export const changeBannerAction = (banner) => ({
  type: actionTypes.CHANGE_BANNER,
  banner,
});
​
​
// ./store/reducer.js
const initialState = {
  // .....
  banner: []
};
function reducer(state = initialState, action) {
  switch (action.type) {
    // .....
    case CHANGE_BANNER:
      return { ...state, banner: action.banner };
    default:
      return state;
  }
}

流程如下:

1679823884917.png

上面的代码有一个缺陷:我们必须将网络请求的异步代码放到组件的生命周期中来完成;

事实上,网络请求到的数据也属于我们状态管理的一部分,更好的方式应该是将它交给redux管理;

redux中的异步操作

我们之前创建的action都是返回一个对象,试图通过返回函数来发起网络请求。但是redux不允许这样操作。

1679776819301.png

那在redux中如何可以进行异步的操作呢?

答案是使用中间件。

官方推荐使用redux-thunk;

使用redux-thunk

安装:

npm install redux-thunk

在创建store时传入应用了中间件的enhance函数

  • 通过applyMiddleware来结合多个中间件,返回一个enhancer;
  • 将enhancer作为第二个参数传入到createStore()中

1679776923316.png

定义一个返回函数的action:

export const fetchHomeMultidataAction = () => {
  // 如果是一个普通的action,那我们要返回一个对象
  // 问题:对象不能直接从服务器拿数据啊
  return function(dispatch, getState) { //函数被调用的时候,会传给这个函数一个dispatch和getState函数
    axios.get("XXXX").then((res) => {
      const banner = res.data.data;
      dispatch(changeBannerAction(banner))
    });
  }
};

应用redux-thunk中间件:

import {createStore,applyMiddleware } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk";
const store = createStore(reducer,applyMiddleware(thunk));
// 用thunk对store进行增强,可以派发一个函数export default store;

总结:rudex-thunk是怎么做到允许发送异步请求的呢?**

  • 默认情况下的dispatch(action),action是一个js的对象;

  • redux-thunk可以让dispatch(action函数);

  • 该函数会被调用,并且传给这个函数一个dispatch和getState函数;

    • dispatch函数用于我们之后再次派发action
    • getState函数用于获取之前一些状态

流程图:

image.png

redux文件拆分

随着应用程序的模块越来越多,各个业务模块的reducer和action在一个文件中,不利于后续的维护。所以实际开发中,我们可以把各个模块的reducer单独封装在一起。

1679825514978.png

redux给我们提供了一个combineReducer函数,来对多个reducer进行合并:

// index.js
import {createStore,applyMiddleware,combineReducers } from "redux";
import thunk from "redux-thunk";
​
import homeReducer from "./home";
import countReducer from "./counter";
​
const reducer = combineReducers({  // 合并多个reducer
    counter:countReducer,
    home: homeReducer
});
const store = createStore(reducer,applyMiddleware(thunk));
​
export default store;

以上全部,谢谢!