Redux进阶

68 阅读6分钟

1. 项目中使用redux

1.1 只使用不修改

// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// App.jsx
import React, { PureComponent } from 'react'
import Home from './pages/home'
import Profile from './pages/profile'
import "./style.css"
import store from "./store"

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: store.getState().counter
    }
  }

  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState()
      this.setState({ counter: state.counter })
    })
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>App Counter: {counter}</h2>

        <div className='pages'>
          <Home/>
          <Profile/>
        </div>
      </div>
    )
  }
}

export default App

// home.jsx
import React, { PureComponent } from 'react'
import store from "../store"
import { addNumberAction } from '../store/actionCreators'

export class Home extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: store.getState().counter
    }
  }

  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState()
      this.setState({ counter: state.counter })
    })
  }

  addNumber(num) {
    store.dispatch(addNumberAction(num))
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>Home Counter: {counter}</h2>
        <div>
          <button onClick={e => this.addNumber(1)}>+1</button>
          <button onClick={e => this.addNumber(5)}>+5</button>
          <button onClick={e => this.addNumber(8)}>+8</button>
        </div>
      </div>
    )
  }
}

export default Home

// profile.jsx
import React, { PureComponent } from 'react'
import store from "../store"
import { subNumberAction } from '../store/actionCreators'

export class Profile extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: store.getState().counter
    }
  }

  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState()
      this.setState({ counter: state.counter })
    })
  }

  subNumber(num) {
    store.dispatch(subNumberAction(num))
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>Profile Counter: {counter}</h2>
        <div>
          <button onClick={e => this.subNumber(1)}>-1</button>
          <button onClick={e => this.subNumber(5)}>-5</button>
          <button onClick={e => this.subNumber(8)}>-8</button>
          <button onClick={e => this.subNumber(20)}>-20</button>
          <button onClick={e => this.subNumber(100)}>-100</button>
        </div>
      </div>
    )
  }
}

export default Profile

// store/index.js
import { createStore } from "redux"
import reducer from "./reducer"

const store = createStore(reducer)

export default store


// store/reducer.js
import * as actionTypes from "./constants"

const initialState = {
  counter: 100
}

function reducer(state = initialState, action) {
  switch (action.type) {
    case actionTypes.ADD_NUMBER:
      return { ...state, counter: state.counter + action.num }
    case actionTypes.SUB_NUMBER:
      return { ...state, counter: state.counter - action.num }
    default:
      return state
  }
}

export default reducer

// store/constants.js
export const ADD_NUMBER = "add_number"
export const SUB_NUMBER = "sub_number"

// actionCreators.js
import * as actionTypes from "./constants"

export const addNumberAction = (num) => ({
  type: actionTypes.ADD_NUMBER,
  num
})

export const subNumberAction = (num) => ({
  type: actionTypes.SUB_NUMBER,
  num
})

2. 使用 react-redux

npm install react-redux

// 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'))
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)

// about.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'

export class about extends PureComponent {
  render() {
    const { counter } = this.props
    return <div>About Page: {counter}</div>
  }
}

const mapStateToProps = state => ({
  counter: state.counter
})

export default connect(mapStateToProps)(about)

1.2 在组件中修改值

import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { addNumberAction, subNumberAction } from '../store/actionCreators'

export class about extends PureComponent {
  calcNumber(num, isAdd) {
    if (isAdd) {
      this.props.addNumber(num)
    } else {
      this.props.subNumber(num)
    }
  }

  render() {
    const { counter } = this.props
    return (
      <div>
        <h2>About Page: {counter}</h2>
        <button onClick={e => this.calcNumber(6, true)}>+6</button>
        <button onClick={e => this.calcNumber(88, true)}>+88</button>
        <button onClick={e => this.calcNumber(6, false)}>-6</button>
        <button onClick={e => this.calcNumber(88, false)}>-88</button>
      </div>
    )
  }
}

const mapStateToProps = state => ({
  counter: state.counter
})

// 需定义一个 dispatch 函数
const mapDispatchToProps = dispatch => ({
  addNumber: num => dispatch(addNumberAction(num)),
  subNumber: num => dispatch(subNumberAction(num))
})

export default connect(mapStateToProps, mapDispatchToProps)(about)

1.3 请求服务器中的数据并且修改state中的值

// Category.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import axios from 'axios'
import { changeBannersAction, changeRecommendsAction } from '../store/actionCreators'

export class Category extends PureComponent {
  componentDidMount() {
    axios.get('XXXX').then(res => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      this.props.changeBanners(banners)
      this.props.changeRecommends(recommends)
    })
  }
  render() {
    return (
      <div>
        <h2>Category Page</h2>
      </div>
    )
  }
}

const mapDispatchToProps = dispatch => ({
  changeBanners: banners => dispatch(changeBannersAction(banners)),
  changeRecommends: recommends => dispatch(changeRecommendsAction(recommends))
})

export default connect(null, mapDispatchToProps)(Category)

// store/constants
export const CHANGE_BANNERS = 'change_banners'
export const CHANGE_RECOMMENDS = 'change_recommends'

// store/actionCreators
import store from "."
import * as actionTypes from "./constants"

export const addNumberAction = num => ({ type: actionTypes.ADD_NUMBER, num })
export const subNumberAction = num => ({ type: actionTypes.SUB_NUMBER, num })

export const changeBannersAction = banners =>({
    type: actionTypes.CHANGE_BANNERS,
    banners
  })

export const changeRecommendsAction = recommends =>({
    type: actionTypes.CHANGE_RECOMMENDS,
    recommends
  })

// store/reducers
import * as actionTypes from './constants'

const initialState = {
  counter: 100,

  banners: [],
  recommends: []
}

function reducer(state = initialState, action) {
  switch (action.type) {
    case actionTypes.ADD_NUMBER:
      return { ...state, counter: state.counter + action.num }
    case actionTypes.SUB_NUMBER:
      return { ...state, counter: state.counter - action.num }
    case actionTypes.CHANGE_BANNERS:
      return { ...state, banners: state.banners }
    case actionTypes.CHANGE_RECOMMENDS:
      return { ...state, recommends: state.recommends }
    default:
      return state
  }
}

export default reducer

// about.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { addNumberAction, subNumberAction } from '../store/actionCreators'

export class about extends PureComponent {
  calcNumber(num, isAdd) {
    if (isAdd) {
      this.props.addNumber(num)
    } else {
      this.props.subNumber(num)
    }
  }

  render() {
    const { counter, banners, recommends } = this.props
    return (
      <div>
        <h2>About Page: {counter}</h2>
        <button onClick={e => this.calcNumber(6, true)}>+6</button>
        <button onClick={e => this.calcNumber(88, true)}>+88</button>
        <button onClick={e => this.calcNumber(6, false)}>-6</button>
        <button onClick={e => this.calcNumber(88, false)}>-88</button>
        <div>
          <ul>
            {banners.map((item, index) => (
              <li key={index}>{item.title}</li>
            ))}
          </ul>
          <h2>推荐列表:</h2>
          <ul>
            {recommends.map((item, index) => (
              <li key={index}>{item.title}</li>
            ))}
          </ul>
        </div>
      </div>
    )
  }
}

const mapStateToProps = state => ({
  counter: state.counter,
  banners: state.banners,
  recommends: state.recommends
})

const mapDispatchToProps = dispatch => ({
  addNumber: num => dispatch(addNumberAction(num)),
  subNumber: num => dispatch(subNumberAction(num))
})

export default connect(mapStateToProps, mapDispatchToProps)(about)

1.4 优化

  • 上述代码缺陷:
    • 必须将网络请求的异步代码放到组件的生命周期中来完成;
    • 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其交给 redux 来管理
// store/index.js
import { createStore, applyMiddleware } from "redux"
import thunk from "redux-thunk"
import reducer from "./reducer"

// 使用中间件
const store = createStore(reducer, applyMiddleware(thunk))

export default store

// store/actionCreators.js
...
export const fetchHomeMultidataAction = () => {
  // 如果是一个普通的action,那么我们这里需要返回的是一个action对象
  // 问题: 对象中是不能直接拿到从服务器请求的异步数据
  // return {}
  function foo(dispatch, getState) {
    // 异步操作: 网络请求
    console.log("foo function execution------", getState().counter)
    axios.get("xxx").then(res => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      dispatch(changeBannersAction(banners))
      dispatch(changeRecommendsAction(recommends))
    })
  }

  // 如果返回的是一个函数,那么 redux 是不支持的
  return foo
}

// Category.jsx
import React, { PureComponent } from "react"
import { connect } from "react-redux"
import { fetchHomeMultidataAction } from "../store/actionCreators"

export class Category extends PureComponent {
  componentDidMount() {
    this.props.fetchHomeMultidata()
  }
  render() {
    return (
      <div>
        <h2>Category Page: {this.props.counter}</h2>
      </div>
    )
  }
}

const mapStateToProps = state => ({
  counter: state.counter
})

const mapDispatchToProps = dispatch => ({
  fetchHomeMultidata: () => dispatch(fetchHomeMultidataAction())
})

export default connect(mapStateToProps, mapDispatchToProps)(Category)

1.5 分离 store

  • 当组件过多时,如果将所有的store都放到一个文件中,此时会造成文件臃肿,这种情况下要将这些文件拆分成单独的文件,方便管理与修改。
// home/actionCreators.js
import * as actionTypes from "./constants"
import axios from "axios"

export const changeBannersAction = banners => ({
  type: actionTypes.CHANGE_BANNERS,
  banners
})
export const changeRecommendsAction = recommends => ({ type: actionTypes.CHANGE_RECOMMENDS, recommends })

export const fetchHomeMultidataAction = () => (dispatch, getState) => {
  axios.get("xxx").then(res => {
    const banners = res.data.data.banner.list
    const recommends = res.data.data.recommend.list
    dispatch(changeBannersAction(banners))
    dispatch(changeRecommendsAction(recommends))
  })
}

// home/constants.js
export const CHANGE_BANNERS = "change_banners"
export const CHANGE_RECOMMENDS = "change_recommends"

// home/reducers.js
import * as actionTypes from "./constants"

const initialState = {
  banners: [],
  recommends: []
}

function reducer(state = initialState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_BANNERS:
      return { ...state, banners: action.banners }
    case actionTypes.CHANGE_RECOMMENDS:
      return { ...state, recommends: action.recommends }
    default:
      return state
  }
}

export default reducer

// home/index.js
import reducer from "./reducer"

export default reducer
export * from "./actionCreators"

......
// 其他文件同理

// store/index.js
import { createStore, applyMiddleware, compose, combineReducers } from "redux"
import thunk from "redux-thunk"
import counterReducer from "./counter"
import homeReducer from "./home"
import userReducer from "./user"

// 合并多个reducer
const reducer = combineReducers({
  counter: counterReducer,
  home: homeReducer,
  user: userReducer
})

// combineReducers 实现原理
// function reducer(state = {}, action) {

//   // 返回一个对象, store 的 state
//   return {
//     counter: counterReducer(state.counter, action),
//     home: homeReducer(state.home, action),
//     user: userReducer(state.user, action)
//   }
// }

// redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))

export default store


2. 使用 redux-toolkit

2.1 store 中的子文件

// store/features/counter.js
import { createSlice } from "@reduxjs/toolkit"

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    counter: 888
  },
  reducers: {
    addNumberAction: (state, { payload }) => {
      state.counter += payload
    },
    subNumberAction: (state, { payload }) => {
      state.counter -= payload
    }
  }
})

export const { addNumberAction, subNumberAction } = counterSlice.actions

export default counterSlice.reducer

// store/features/home.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import axios from "axios"

// 异步操作需要使用 createAsyncThunk
// 定义的异步函数也可以传参
export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata", async (extraInfo, { dispatch, getState }) => {
  // console.log(extraInfo, dispatch, getState)
  // 1. 发送网络请求, 获取数据
  const res = await axios.get("xxx")

  // 2. 取出数据, 并且在此处直接dispatch操作(可以不做)
  const banners = res.data.data.banner.list
  const recommends = res.data.data.recommend.list
  dispatch(changeBannersAction(banners))
  dispatch(changeRecommendsAction(recommends))

  // 3. 返回结果, 那么action状态会变成fulfilled状态
  return res.data
})

const homeSlice = createSlice({
  name: "home",
  initialState: {
    banners: [],
    recommends: []
  },
  reducers: {
    changeBannersAction(state, { payload }) {
      state.banners = payload
    },
    changeRecommendsAction(state, { payload }) {
      state.recommends = payload
    }
  },
  // 不推荐使用
  // extraReducers: {
  //   [fetchHomeMultidataAction.pending](state, action) {
  //     console.log("fetchHomeMultidataAction pending")
  //   },
  //   [fetchHomeMultidataAction.fulfilled](state, { payload }) {
  //     console.log("fetchHomeMultidataAction fulfilled", payload)
  //     state.banners = payload.data.banner.list
  //     state.recommends = payload.data.recommend.list
  //   },
  //   [fetchHomeMultidataAction.rejected](state, action) {
  //     console.log("fetchHomeMultidataAction rejected")
  //   }
  // }
  extraReducers: builder => {
    builder
      .addCase(fetchHomeMultidataAction.pending, (state, action) => {
        console.log("fetchHomeMultidataAction pending")
      })
      .addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
        state.banners = payload.data.banner.list
        state.recommends = payload.data.recommend.list
      })
  }
})
export const { changeBannersAction, changeRecommendsAction } = homeSlice.actions

export default homeSlice.reducer

// store/index.js
import { configureStore } from "@reduxjs/toolkit"
import counterReducer from "./features/counter"
import homeReducer from "./features/home"

const store = configureStore({
  reducer: {
    counter: counterReducer,
    home: homeReducer
  }
})

export default store

// src/index.js
// 使用 Provider
import React from "react"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
import App from "./App"
import store from "./store"

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  // <React.StrictMode>
  <Provider store={store}>
    <App />
  </Provider>
  // </React.StrictMode>
)

// pages/Home.jsx
import React, { PureComponent } from "react"
import { connect } from "react-redux"
import { addNumberAction } from "../store/features/counter"
import { fetchHomeMultidataAction } from "../store/features/home"

// import axios from "axios"
// import store from "../store"
// import { changeBannersAction, changeRecommendsAction } from "../store/features/home"

export class Home extends PureComponent {
  componentDidMount() {
    // axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
    //   const banners = res.data.data.banner.list
    //   const recommends = res.data.data.recommend.list
    //   store.dispatch(changeBannersAction(banners))
    //   store.dispatch(changeRecommendsAction(recommends))
    // })
    this.props.fetchHomeMultidata()
  }

  addNumber(num) {
    this.props.addNumber(num)
  }
  render() {
    const { counter } = this.props
    return (
      <div>
        <h2>Home Counter: {counter}</h2>
        <button onClick={e => this.addNumber(5)}>+5</button>
        <button onClick={e => this.addNumber(8)}>+8</button>
      </div>
    )
  }
}

const mapStateToProps = state => ({
  counter: state.counter.counter
})

const mapDispatchToProps = dispatch => ({
  addNumber: num => dispatch(addNumberAction(num)),
  fetchHomeMultidata: () => dispatch(fetchHomeMultidataAction({ name: "kobe", age: 18 }))
})

export default connect(mapStateToProps, mapDispatchToProps)(Home)

// pages/Profile.jsx
import React, { PureComponent } from "react"
import { connect } from "react-redux"
import { subNumberAction } from "../store/features/counter"

export class Profile extends PureComponent {
  subNumber(num) {
    this.props.subNumber(num)
  }
  render() {
    const { counter, banners, recommends } = this.props
    return (
      <div>
        <h2>Profile: {counter}</h2>
        <button onClick={e => this.subNumber(5)}>-5</button>
        <button onClick={e => this.subNumber(8)}>-8</button>

        <div className="banners">
          <h2>轮播图列表:</h2>
          <ul>
            {banners.map((item, index) => (
              <li key={index}>{item.title}</li>
            ))}
          </ul>
        </div>
        <div className="recommends">
          <h2>展示的列表:</h2>
          <ul>
            {recommends.map((item, index) => (
              <li key={index}>{item.title}</li>
            ))}
          </ul>
        </div>
      </div>
    )
  }
}

const mapStateToProps = state => ({
  counter: state.counter.counter,
  banners: state.home.banners,
  recommends: state.home.recommends
})

const mapDispatchToProps = dispatch => ({
  subNumber: num => dispatch(subNumberAction(num))
})

export default connect(mapStateToProps, mapDispatchToProps)(Profile)

3. redux 封装

3.1 封装日志信息

// store/middleware/log.js
// 对每次派发的action进行拦截, 进行日志打印
function log(store) {
  const next = store.dispatch

  function logAndDispatch(action) {
    console.log("当前派发的action: ", action)
    // 真正派发的代码: 使用之前的dispatch进行派发
    next(action)
    console.log("派发之后的结果: ", store.getState())
  }

  // mokey patching: 候补定 => 篡改现有的代码, 对整体的执行逻辑进行修改
  store.dispatch = logAndDispatch
}

export default log

3.2 封装thunk

// store/middleware/thunk.js
function thunk(store) {
  const next = store.dispatch
  function dispatchThunk(action) {
    if (typeof action === "function") {
      action(store.dispatch, store.getState)
    } else {
      next(action)
    }
  }
  store.dispatch = dispatchThunk
}

export default thunk

3.3 合并中间件

// store/middleware/applyMiddleware.js
function applyMiddleware(store, ...fns) {
  fns.forEach(fn => fn(store))
}

export default applyMiddleware

// store/middleware/index.js
import log from "./log"
import thunk from "./thunk"
import applyMiddleware from "./applyMiddleware"

export { log, thunk, applyMiddleware }

3.4 使用

// store/index.js

import { applyMiddleware } from "./middleware"
...
applyMiddleware(store, log, thunk)