Redux手把手教学,有这篇就够了

Redux 是 React 框架下的一款状态管理工具,可以实现多个组件之间的数据共享和传递。学习和掌握 Redux 以及周边生态可以使我们更好的进行 React 项目开发。下面我们就详细的讲述 Redux 在实际项目开发中的使用。

Redux 基本使用

简单使用

  • 1、创建storereducer
// store/reducer.js
// 创建reducer
const defaultState = { a: "" };
export default (state = defaultState, action) => {
  return state;
};
// store/index.js
// 创建store
import { createStore } from "redux";
import reducer from "./reducer.js";
const store = createStore(reducer);
export default store;
  • 2、 在组件中获取store中的数据
// todoList.js
this.state = store.getState();
  • 3、 派发action请求修改store中的数据
// todoList.js
const action = {
  type : "CHANGE_INPUT_VALUE",
  a: 123;
}
store.dispatch( action )
  • 4、 store收到修改请求后在reducer中执行更新
// reducer.js
const defaultState = { a : "" };
export default ( state = defaultState , action ) => {
  if ( action.type === "CHANGE_INPUT_VALUE" ) {
    const newState = JSON.parse(JSON.stringify( state );
    newState.a = action.a;
    return newState;
  }
  return state ;
}
  • 5、在组件中监听到store数据发生变化后触发回调方法重新获取store数据来更新组件数据。
// todoList.js
// 监听store中数据发生变化
store.subscribe( this.handleStoreChange)
// 执行回调更新组件数据
handleStoreChange() {
this.setState(store.getState());
}

项目使用示例

|- store
  |- index.js
    import { createStore } from "redux";
    import reducer from "./reducer.js";
    const store = createStore(reducer);
    export default store;
  |- reducer.js
    import { CHANGE_INPUT_VALUE } from "actionTypes";
    const defaultState = { inputValue: "" };
    export default ( state = defaultState , action ) => {
      if ( action.type === "CHANGE_INPUT_VALUE" ) {
        const newState = JSON.parse(JSON.stringify( state );
        newState. inputValue = action.inputValue;
          return newState;
        }
        return state ;
      }
  |- actionTypes.js
    export const CHANGE_INPUT_VALUE = "change_input_value";
  |- actionCreators.js
    import { CHANGE_INPUT_VALUE } from "actionTypes";
    export const getInputChangeAction = (value) => {
      type: CHANGE_INPUT_VALUE,
      value
    }
|- todoList.js
    import React , { Component } from "react";
    import store from "./store";
    import { getInputChangeAction } from "./store/actionCreators";
    class TodoList extends Component {
      constructor( props ) {
        super( props );
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        store.subscribe(this.handleStoreChange);
      }
      render() {
        return (
          <input
            value={this.state.inputValue}
            onChange={this.handleInputChange}/>
        )
      }
      handleInputChange(e) {
        const action = getInputChangeAction(e.target.value);
        store.dispatch(action);
      }
      handleStoreChange() {
        this.setState(store.getState());
      }
    }

Redux 的中间件

为了近一步丰富Redux生态,围绕着Redux周边有很多第三方中间件。其中为了使Redux更好的提供异步操作的支持,有两个比较常用的中间件Redux-thunkRedux-saga

写在使用之前

一点疑惑 ?

在使用Redux-thunkRedux-saga之前,一直有一个困扰我的地方,那就是:

我们为什么要在React项目中使用类似于Redux-thunk这样的中间件?
它能给我们带来什么好处?

Redux-thunk官方仓库这样介绍:

With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. Middleware extend the store’s abilities, and let you write async logic that interacts with the store.Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests.

意思是说:

基础的Redux store只能通过派发action来同步的更新数据,中间件扩展了store的能力,你可以书写异步逻辑来影响store。在需要在store中处理复杂的同步逻辑和异步逻辑的时候,Thunks是被推荐的中间件。

这点解释并不能打消我的疑虑…

  • 1、在需要异步更新store的数据的时候,我可以在组件中执行异步请求,然后再派发action更新reducer的数据即可,类似下面的操作:
// component.js
import { getData } from './actionCreators';
import store from '../store/index.js'
// ......
componentDidMount() {
  fetch('http://192.168.xx/getList').then(res => {
    const { data }  = res
    const action = getData(data)
    store.dispatch(action)
  })
},
// actionCreators.js
export const getData = (value) => ({
  type: 'GET_DATA',
  value
});
  • 2、或者是在actionCreators中操作异步逻辑,然后通过参数的方式传递dispacth
// component.js
import store from '../store/index.js'
import { getData } from './store/actionCreatores.js'
......
componentDidMount() {
  store.getData(store.dispatch)
}
// actionCreators.js
export const getData = (dispatch) => {
  return fetch('http://192.168.xx/getList').then(res => {
      const { data }  = res
      const action = {
        type: 'GET_DATA',
        data
      }
      dispatch(action)
    })
};

以上两种方式都可以实现更新store中的状态state

既然如此,我为什么还需要使用Redux-thunk?

直到我看到这个

英文版:https://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594

中文你可以看这篇:http://www.xiaojichao.com/post/why-do-we-need-middleware-for-async-flow-in-redux.html

才明白问题所在…

  • 1、方式一如果在大型应用中,需要在大量的组件中执行相同的操作,之所以使用redux就是因为数据共享的问题,如果需要使用数据的每个组件都执行一遍请求操作,一方面不利于调试,也明显违背了使用redux的初衷。因此需要讲 UI 组件和action creator进行剥离。
  • 2、方式二通过动态传递参数的方式,虽然看起来已经将 UI 组件和action creator进行了分离,但是 UI 组件首先知道要调用的对象是同步还是异步的,而且还需要根据不同的方式编写不同风格的代码(例如,传递同步参数等)。

正确的打开方式应该是这样

UI 组件并不关心 action creator 是否是异步的。UI 组件就像正常的去调用一个普通的操作,当然也可以使用 mapDispatchToProps 来简化代码。UI 组件也不知道 action creator 是怎么实现的,因此你可以切换各种异步实现方式(Redux Thunk, Redux Promise, Redux Saga),而且还不需要改组件的代码。

来个更加形象一点的说法!

UI 组件就好比是我们,而redux就好比是银行,通常我们将钱存进银行,需要的时候就去银行取,这就是同步更新数据操作。

但是,当有一天我们需要使用在境外银行存的钱,我们就没必要自己直接去境外银行取,因为可能涉及到比较多的手续会很麻烦。那么我们就可以将专业的事情交给专业的人去做,让银行之间进行划转,我们直接去使用的银行取就可以了。对于我们来说,并没有改变取钱的方式,而中间的工作都交给了银行取操作,也就是这里的action creator的异步操作。我们根本不用关心银行使用的是什么方法,对应的就是 UI 组件不关心,在action creator中是使用的是Redux-thunk还是 Redux-saga,它不需要改组件的代码。

说到这里,大家应该都明白了吧!所以Redux-thunk Redux-saga就该登场了!!

Redux-thunk

  • 安装
npm install redux-thunk -S
  • 使用配置

注意:在配置redux-thunk的时候如果安装官方文档配置,直接把thunk放到createStore里的第二个参数,如果配置了Redux Dev Tools会发生冲突,如果想要同时使用,就需要引入增强函数compose

// store/index.js
// 1引入applyMiddleware和增强函数compose
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
// 2引入redux-thunk
import thunk from "redux-thunk";
// 3使用compose创建增强函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
// 4传入thunk
const enhancer = composeEnhancers(applyMiddleware(thunk));
// 5把enhancer作为createStore的第二个参数传入
const store = createStore(reducer, enhancer);
export default store;
  • 开始使用
// actionCreatores.js
export const getData = () =>{
  // dispatch 会作为参数自动传递进来
    return (dispatch)=>{
        axios.get('xxx/getData').then((res)=>{
            const data = res.data
            const action = {
              type: 'GET_DATA',
              data
            }
            dispatch(action)
        })
    }
}
// component.js
import store from '../store/index.js'
import { getData } from './store/actionCreatores.js'
....
componentDidMount() {
  const action = getData()
  store.dispatch(action)
}

Redux-saga

Redux-thunk中间件可以使我们以一种更加优雅的方式异步更新store中的数据,在中小型项目中,非常好用,但是也不得不承认其存在的缺点,Redux-thunk将同步异步更新数据的逻辑都放在creator中操作,很容易形成面条式的代码,不方便管理。那么,我们可以使用另一种更加适合于大型项目异步更新数据的中间件 - Redux-saga

基本使用

  • 安装
npm install redux-saga -S
  • store文件夹下创建saga.js文件单独管理异步操作.
touch saga.js
  • 使用配置
// store/index.js
// 1引入applyMiddleware和增强函数compose
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
// 2引入创建saga中间件的方法.并创建saga中间件
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
// 3引入saga.js文件
import mySagas from "./saga.js";
// 4使用compose创建增强函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
// 5传入saga中间件
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
// 6把enhancer作为createStore的第二个参数传入
const store = createStore(reducer, enhancer);
// 7执行saga.js文件
sagaMiddleware.run(mySagas);

export default store;
  • 开始使用
  1. UI 组件派发获取数据的actionactionCreator
// component.js
import store from '../store/index.js'
import { getData } from '../store/actionCreator.js'
componentDidMount() {
  const action = getData()
  store.dispatch(action)
}
  1. actionCreator转发actionsaga
// actionTypes.js
// 获取数据
export const GET_DATA = "get_data";
// 更新数据
export const UPDATE_DATA = "update_data";

// actionCreator.js
import { GET_DATA, UPDATE_DATA } from "./actionTypes.js";
// 通知saga执行异步操作的action
export const getData = () => ({
  type: GET_DATA,
});
// saga执行异步操作完毕,更新reducer数据的action
export const updateData = () => ({
  type: UPDATE_DATA,
  data,
});
  1. saga监听到GET_DATAaction后,获取异步数据,获取到数据后派发actionreducer更新数据
// saga.js
// 引入takeEvery(监听action),put(派发action)
import { takeEvery, put } from "redux-saga/effects";
import { GET_DATA } from "./actionTypes.js";
import { reqData } from "../api/index.js";
import { updateData } from "../store/actionCreator.js";
// 异步获取数据方法
function* getDataList() {
  // 执行异步操作
  const res = yield reqData();
  if (res.code === 200) {
    const { data } = res;
    // 获取到数据后,派发action到reducer更新数据
    const action = updateData(data);
    yield put(action);
  }
}

function* mySaga() {
  // 监听到GET_DATA的action后执行getDataList方法获取数据
  yield takeEvery(GET_DATA, getDataList);
}
export default mySaga;
  1. reducer收到更新数据的action后更新reducer数据
// reduce.js
import { UPDATE_DATA } from "./actionTypes.js";
const defaultState = {
  list: [],
};
export default (state = defaultState, action) => {
  if (action.type === UPDATE_DATA) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.list = action.data;
    return newState;
  }
};

saga分模块管理

在实际项目中,可能有很多需要异步派发的action,如果所有的saga都写在同一个文件中很容易造成混乱,因此根据业务分模块管理就变得十分有必要,redux-saga本身也提供了分模块管理的方法。

// store/sagas/index.js
// saga相关模块化引入
import { fork, all } from "redux-saga/effects";
// 异步逻辑模块文件引入
import { loginSagas } from "./modules/login.js";
import { userSagas } from "./modules/user.js";
import { businessSagas } from "./modules/business.js";
// 合并saga,单一进入点,一次启动所有Saga
export default function* rootSaga() {
  yield all([
    fork(loginSagas).default,
    fork(userSagas).default,
    fork(businessSagas).default,
  ]);
}

// store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
// 引入合并后的saga
import rootSaga from "./sagas/index";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);
// 执行saga
sagaMiddleware.run(rootSaga);
export default store;

React-redux

Redux作为React的状态共享管理工具,其本身也存在使用缺陷,为了更好的使用Redux,社区基于Redux推出了更加方便易用的第三方库React-redux

基本使用

  • 1、 创建storereducer
// store/index.js
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;
// store/reducer.js
const defaultState = {
  inputValue: "",
};
export default (state = defaultState, action) => {
  return state;
};
  • 2、 使用Provider组件连接store

Provider组件包裹的组件都可以使用store中的数据.

// 入口index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import TodoList from './TodoList'
cosnt App = (
  <Provider store = {store}>
    <TodoList />
  </Provider>
)
ReactDOM.render(App,document.getElementById('root'))
  • 3、 使用connect方法将store中的数据和映射到组件的props
  • 4、 使用connect方法将storedispatch方法映射到组件的props
// TodoList.js
import React, { Component } from 'react'
import store from '../store'
import connect from 'react-redux'
class TodoList extends Component {
  return (
    // store的state隐射到组件props之后,使用的时候直接是this.props.xxx
    // store的dispacth映射到props后,也可以直接this.props.fn绑定方法
    <input value = {this.props.value}
            onChange={this.props.changeInputValue}/>
  )
}

// 将store中的数据映射到组件,变成组件的props,参数state是store中的state
const mapStateToProps = (state) => {
  return {
    inputValue:state.inputValue
  }
}
// 把store的dispatch映射到组件,变成组件的props,可以直接this.props.xx的方式调用
const mapDisptachToProps = (dispatch) => {
  return {
    changeInputValue(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    }
  }
}
// 把store的state和dispatch和TodoList组件关联
export defalut connect(mapStateToProps,mapDisptachToProps)(TodoList)
  • 5、 store接收到dispatchaction后,在reducer中更新store中的数据
// store/reducer.js
const defaultState = {
  inputValue: ''
}
export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify( state );
    newState.inputValue = action.inputValue;
    return newState;
  }
  return state
}

Reducer分模块管理

实际项目开发中,reducer中可能要放很多的数据,如果都存放在一个reducer中,很容易造成数据很多而难以维护,因此我们可以根据业务模板拆分成多个reducer进行分开管理。

  • 1、分模块拆分reducer
// header/reducer.js
const defaultState = {
  a: "",
};
export default (state = defaultState, action) => {
  // ...更新数据,需要带模块名
  return state;
};
// footer/reducer.js
const defaultState = {
  b: "",
};
export default (state = defaultState, action) => {
  // ...更新数据,需要带模块名
  return state;
};
  • 2、合并整合reducer
// store/reducer.js
import { combineReducers } from 'redux'
import reducer as headerReducer  from './header/reducer.js'
import reducer as footerReducer from './footer/reducer.js'
const reducer = combineReducers({
  header: headerReducer,
  footer: footerReducer
})
export default reducer;
  • 3、组件取值

分模块管理后,取值时需要带上模块名。

// TodoList.js
// ...
const mapStateToProps = (state) => {
  return {
    // 需要带上模块名才能正确取值
    a: state.header.a,
  };
};
// ...

reducer 分模块管理后,主要变更就是以上三点,其他步骤不变。

Immutable 加持

Redux中,我们修改store中的数据的时候,不可以直接修改,而是需要先拷贝一个副本来修改,然后再返回副本。该方式虽然可以实现,但拷贝副本对性能有一定的损耗。此时,immutable.js就该登场了。将reducer中的普通数据对象转换为immutable对象就可以直接修改数据而无需拷贝副本。

immutable.js

  • 1、 将reducer中普通数据对象转换为immutable对象
// reducer.js
import { fromJS } from "immutable";
// 转换
const defaultState = fromJS({
  a: "",
});
  • 2、 获取和更新数据
// 获取数据
// TodoList.js
// ...
const mapStateToProps = (state) => {
  return {
    // state是根基级reducer的state,header模块已经是immutable对象,因此需要get()方法获取值
    a: state.header.get("a"),
  };
};
// ...
// 更新数据
// reducer.js
// ...
export default (state = defaultState, action) => {
  if (action.type === "xxxx") {
    // 这里的state是header模块的state,已经转换为immutable对象,可以使用set来更新值
    return state.set("a", 123);
    /*  同时改变多个值时可以使用merge
    return state.merge({
      a: xxx,
      b: yyy
    })
    */
  }
  return state;
};

Redux-immutable

使用immutable.js虽然进一步完善了redux,但是在获取store中的数据的时候,因为是不同类型的对象取值方式不统一。如:state.header.get(),state是普通对象,通过点的方式取对象属性,headerimmutable对象,通过get方法取值。为了进一步统一和完善。redux-immutable就诞生了。

  • 1、 在合并reducer时将根级reducerstate转换为immutable对象
// store/reducer.js
import { combineReducers } from "redux-immutable";
import { reducer as headerReducer } from "../header/store/reducer.js";
const reducer = combineReducers({
  header: headerReducer,
});
export default reducer;
  • 2、 取值
// TodoList.js
// ...
const mapStateToProps = (state) => {
  return {
    // state和header都被转化为了immutable对象,都可以使用get方法取值
    a: state.get("header").get("a"),
    // 或者使用getIn方法
    // a: state.getIn(['header','a'])
  };
};
// ...

完整代码演示

下面会分别演示在类组件和函数式组件中的用法:

|- index.js // 入口js
  import React from 'react'
  import ReactDOM from 'react-dom'
  import { Provider } from 'react-redux'
  import store from './store'
  import User from './User'
  cosnt App = (
    <Provider store = {store}>
      <User />
    </Provider>
  )
  ReactDOM.render(App,document.getElementById('root'))

// 类组件中使用
|- User.js
  import React, { Component } from 'react';
  import { connect } from 'react-redux';
  import { actionCreators } from '../store/user';
  class User extends Component {
    componentDidMount(){
      this.props.getUserInfoData()
    }
    render() {
      return (
        <div>{this.props.userInfo.userName}</div>
      )
    }
  }
  const mapStateToProp = (state) => {
    return {
      // 006-监听到store中数据更新了,传递到UI组件,组件就可以拿到数据了
      userInfo: state.getIn(['user','userInfo'])
    }
  }
  const mapDispathToProps = (dispatch) => ({
    getUserInfoData() {
      // 001-UI组件派发获取数据的action
      dispatch(actionCreators.getUserInfo())
    }
  })
  export default connect(mapStateToProps, mapDispathToProps)(User);

// 函数式组件中使用
|- User.js
  import React,{ useEffect } from 'react';
  import { useSelector } from 'react-redux';
  import { actionCreators } from '../store/user';
  function User(){
    // 006-监听到store中数据更新了,传递到UI组件,组件就可以拿到数据了
    const userInfo = useSelector((state) => {
      return state.getIn(['user', 'userInfo']);
    })
    useEffect(() => {
      // 001-UI组件派发获取数据的action
      store.dispatch(actionCreators.getUserInfo());
    },[])
    return (
      <div>{userInfo.userName}</div>
    )
  }
  export default User;

|- store
  |- modules
    |- user
      |- actionTypes.js
        export const GET_USER_INFO = 'user/GET_USER_INFO';
        export const SET_USER_INFO = 'user/SET_USER_INFO';

      |- actionCreators.js
        import * as actionTyps from './actionTyps';
        // 002-接收UI组件获取用户信息的action,转发到saga
        export const getUserInfo = () => ({
          type: actionTyps.GET_USER_INFO,
        });
        // 接收saga更新用户信息的action,转发到reducer
        export const setUserInfo = (payload) => ({
          type: actionTyps.SET_USER_INFO,
          data: payload,
        });

      |- saga.js
        import { takeEvery, put } from 'redux-saga/effects';
        import { queryUserInfo } from '@/api/api-user';
        import { setUserInfo } from './actionCreators';
        import * as actionTypes from './actionTypes';
        // 异步获取用户信息
        function* getUserInfos() {
            const res = yield queryUserInfo();
            if (res.code === '0') {
                const { data } = res;
                // 004- 获取到异步数据后派发action到reducer,更新数据
                const action = setUserInfo(data);
                yield put(action);
            }
        }
        function* userSagas() {
            // 003- 监听到转发过来的GET_USER_INFO的action,执行异步方法getUserInfos
            yield takeEvery(actionTypes.GET_USER_INFO, getUserInfos);
        }
        export default businessSagas;

      |- reducer.js
        import * as actionTyps from "./actionTypes";
        const defaultState = {
          userInfo: {}
        };
        export default ( state = defaultState , action ) => {
          switch(action.type) {
            // 005-接收到saga更新数据的action后,执行更新数据操作
            case actionTyps.SET_USER_INFO:
              return state.set('userInfo',action.data)
            default:
              break;
          }
          return state;
        }

      |- index.js
        import reducer from './reducer';
        import * as actionCreators from './actionCreators';
        import * as actionTypes from './actionTypes';
        import * as userSagas from './sagas';
        export { reducer, actionCreators, actionTypes, userSagas };

  |- reducers.js
    import { combineReducers } from 'redux-immutable';
    import { reducer as userReducer }  from './modules/user'
    const reducer = combineReducers({
      user: userReducer
    })
    export default reducer;

  |- sagas.js
    import { all, fork } from 'redux-saga/effects';
    import { userSagas } from './modules/user';
    // 合并saga,单一进入点,一次启动所有Saga
    export default function* rootSaga() {
        yield all([fork(userSagas.default)]);
    }

  |- index.js
    import { createStore, applyMiddleware, compose } from 'redux';
    import createSagaMiddleware from 'redux-saga';
    import reducer from './reducers';
    // 引入合并后的saga
    import rootSaga from './sagas';
    const sagaMiddleware = createSagaMiddleware();
    const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
        ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
        : compose;
    const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
    const store = createStore(reducer, enhancer);
    // 执行saga
    sagaMiddleware.run(rootSaga);
    export default store;

至此,使用Redux作为React的状态管理工具的分享就结束了,本文可谓是手把手的Redux及周边生态教学,希望看到的你能有所裨益!

你可能感兴趣的:(React,javascript,前端,react.js)