2619 字
13 分钟
06.Redux
2025-03-28

Redux in class#

Redux 是一个帮助我们管理 State 的容器:Redux 是 JS 的状态容器,提供了可预测的状态管理。

yarn add redux

Store#

Store 就是用来维持应用所有 state 树的一个对象。改变 store 内 state 的唯一途径是对它 dispatch 一个 action。

const initialState = {
    friends: [
        { name: 'Sytus', age: 17 },
        { name: 'Ram', age: 18 },
        { name: 'You', age: 20 },
    ]
}

action#

Redux 要求我们通过 action 来更新数据:

  • 所有数据的变化,必须通过派发(dispatch)action 来更新;

  • action 是一个普通的 JS 对象,用来描述这次更新的 type 和 content。

  • 强制使用 action 的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可追踪、可预测的;

  • 下面案例中的 action 是固定的对象,真实应用中,我们会通过函数来定义,返回一个 action。

const action1 = { type: 'ADD_FRIEND', info: { name: 'lucy', age: 20 } }
const action2 = { type: 'INC_AGE', index: 0 }
const action3 = { type: 'CHANGE_NAME', playload: { index: 0, newName: 'Sytus' } }

reducer#

Reducer 将 state 和 action 联系在一起。

  • Reducer 是一个纯函数;

  • Reducer 做的事情就是将传入的 state 和 action 结合起来生成一个新的 state。

const redux = require('redux') // 引入redux

const initialState = {
    counter: 0,
}

// reducer

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

// store(创建的时候需要传入一个reducer)

const store = redux.createStore(reducer)

// 订阅store修改
store.subscribe(() => {
    console.log('counter:', store.getState().counter);
})

// action
const action1 = { type: 'INCREMENT' }
const action2 = { type: 'DECREMENT' }
const action3 = { type: 'ADD_NUMBER', num: 5 }
const action4 = { type: 'SUB_NUMBER', num: 12 }

// dispatch

store.dispatch(action1) // 执行reducer函数
store.dispatch(action2)
store.dispatch(action3)
store.dispatch(action4)

// 最终打印:
// counter: 1
// counter: 0
// counter: 5
// counter: -7

Redux的三大原则#

  1. 单一数据源

    1. 整个应用程序的 state 都储存在一颗 object tree 中,并且这个 object tree 只储存在一个 store 中;

    2. Redux 并没有强制让我们不能创建多个 Store,但是那样并不利于数据的维护;

    3. 单一的数据源可以让整个应用程序变得方便维护、追踪、修改。

  2. State 是只读的

    1. 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何方式来修改 State;

    2. 确保 View 或网络请求都不能直接修改 state,它们只能通过 action 来描述自己想要如何修改 state;

    3. 保证所有修改被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竟态)的问题。

  3. 使用纯函数来执行修改

    1. 通过 reducer 将旧 state 和 action 联系在一起,并且返回一个新的 state;

    2. 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同的 state tree 的一部分;

    3. 但所有的 reducer 都应该是纯函数,不能产生任何的副作用。

纯函数:

  1. 函数的返回结果只依赖于它的参数;

  2. 函数执行过程里面没有副作用。

let foo=(a, b)=>a+b
foo(1,2) //=3

副作用:

  • 除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 AJAX 请求,还有调用 window.reload 刷新浏览器,甚至是 console.log 往控制台打印数据也是副作用。

开发事项#

文件处理#

  • 在正常的开发中,我们会把 redux 的相关代码放进一个命名为“store”的文件夹中;

  • store文件夹下我们会创建以下的文件:

    • index.js:用于创建 store
    import redux from 'redux'
    import reducer from './reducer.js'
    
    const store = redux.createStore(reducer)
    
    export default store
    • reducer.js:用于创建 reducer
    import {
        ADD_NUMBER,
        SUB_NUMBER
    } from './constants.js'
    
    const defaultState = {
        counter: 0
    }
    
    function reducer(state = defaultState, action) {
        switch (action.type) {
            case ADD_NUMBER:
                return { ...state, counter: state.counter + action.num }
            case SUB_NUMBER:
                return { ...state, counter: state.counter - action.num }
            default:
                return state
        }
    }
    
    export default reducer
    • actionCreators.js:用于创建 action
    import {
        ADD_NUMBER,
        SUB_NUMBER
    } from './constants.js'
    
    export const addAction = num => ({
        type: ADD_NUMBER,
        num
    })
    
    export const subAction = num => ({
        type: SUB_NUMBER,
        num
    })
    
    • constants.js:用于创建一些我们容易写错的值
    export const ADD_NUMBER = 'ADD_NUMBER'
    export const SUB_NUMBER = 'SUB_NUMBER'

使用高阶组件简化代码#

connect.js#

import React, {PureComponent} from 'react'
import store from '../store'

export function connect(mapStateToProps, mapDispatchToProps) {
    return function enhanceHOC(WrappedComponent) {
        return class extends PureComponent {
            constructor(props) {
                super(props)
                this.state = {
                    storeState: mapStateToProps(store.getState())
                }
            }
            componentDidMount() {
                this.unsubscribe = store.subscribe(() => {
                    this.setState({
                        storeState: mapStateToProps(store.getState())
                    })
                })
            }
            componentWillUnmount() {
                this.unsubscribe()
            }
            render() {
                return <WrappedComponent {...this.props}
                                         {...mapStateToProps(store.getState())}
                                         {...mapDispatchToProps(store.dispatch)} />
            }
        }
    }
}

目标组件#

import React from 'react'
import {
    addAction
} from '../store/actionCreators.js'

import { connect } from '../utils/connect'

function Home(props) {
    return (
        <div>
            <h1>Home</h1>
            <h2>当前计数:{props.counter}</h2>
            <button onClick={e => { props.increment() }}>+1</button>&nbsp;
            <button onClick={e => { props.addNumber(5) }}>+5</button>
        </div>
    )
}

const mapStateToProps = state => {
    return {
        counter: state.counter
    }
}
const mapDispatchToProps = dispatch => {
    return {
        increment: function() {
            dispatch(addAction(1))
        },
        addNumber: function(num) {
            dispatch(addAction(num))
        }
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Home)

使用redux库#

yarn add react-redux
  • 项目入口 index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'
import store from './store';
import { Provider } from 'react-redux';

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

);
  • 在目标文件中更改 connect 导入即可:
// import { connect } from '../utils/connect'
import {connect} from 'react-redux'

redux中异步操作#

在 redux 中,我们可以使用中间件Middleware)进行异步操作。

它可以让我们在 dispatch actions 和 reducer 处理之间进行异步操作(例“网络请求”)。

redux-thunk#

  1. 安装
yarn add redux-thunk
  • 在创建 store 时传入应用了 middleware 的 enhance 函数

    1. 通过 applyMiddleware 来结合多个 Middleware,返回一个 enhancer;

    2. 将 enhancer 作为第二个参数传入到 createStore 中;

import {createStore, applyMiddleware} from 'redux'
import thunkMiddleware from 'redux-thunk'

const enhancer = applyMiddleware(thunkMiddleware)
const store = createStore(reducer, enhancer)
  • 定义返回一个函数的 action

    1. 注意:这里不是返回一个对象了,而是一个函数;

    2. 该函数在 dispatch 之后会被执行;

const getHomeMultidataAction = (dispatch, getState) => {
    axios.get("https://...").then(res => {
        const data = res.data.data
            dispatch(changeBannersAction(data.banner.list))
    })
}

Redux in function#

在 React 中使用 redux,官方要求安装两个其他插件 - Redux Toolkit 和 react-redux

  1. Redux Toolkit(RTK):官方推荐编写 Redux 逻辑的方式,是一套工具的集合集,简化书写方式;

    1. 简化 store 的配置方式;

    2. 内置 immer 支持可变式状态修改;

    3. 内置 thunk 更好的异步创建。

  2. React-redux - 用来链接 Redux 和 React 组件的中间件

yarn add @reduxjs/toolkit react-redux

React-redux 负责把 Redux 和 React 链接起来,内置 Provider 组件通过 store 参数把创建好的 store 实例注入到应用中,链接正式建立。

参考文档:redux最佳实践redux-toolkit使用指南

Store的目录设计#

  1. 在 src 文件夹下创建 store 文件夹;

  2. 应用通常会有多个 store 模块,所以创建一个 modules 文件夹,在其内部编写业务分类的子 store;

  3. Store 中的入口文件 index.js 的作用是组合 modules 中所有的子模块,并导出 store。

文件编写#

  1. counterStore
import { createSlice } from "@reduxjs/toolkit"

const counterStore = createSlice({
    name: 'counter',
    // 初始化state
    initialState: {
        count: 0
    },
    // 修改状态的方法 同步方法 支持直接修改
    reducers: {
        // state 就是 initialState
        increment(state) {
            state.count++
        },
        decrement(state) {
            state.count--
        }
    }
})

// 结构actionCreater函数
const {increment, decrement} = counterStore.actions
// 获取reducer
const reducer = counterStore.reducer
// 以按需导出的方式导出actionCreater
export {increment, decrement}
// 以默认导出的方式导出reducer
export default reducer
  • index.js
import { configureStore } from "@reduxjs/toolkit";
// 导入子模块reducer
import counterReducer from './modules/counterStore'

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

export default store

React组件使用store中的数据#

在 React 组件中使用 store 中的数据。需要用到一个钩子函数 useSelector,它的作用是把 store 中的数据映射到组件中。

import React from 'react'
import { useSelector } from 'react-redux';

function App() {
  const {count} = useSelector(state => state.counter)
  return (
    <div>
      App
      {count}
    </div>
  );
}

export default App;

React组件修改store中的数据#

React 组件中修改 store 中的数据需要借助另外一个 hook 函数 useDispatch,它的作用是生成提交 action 对象的 dispatch 函数。

import React from 'react'
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './store/modules/counterStore';

function App() {
  const { count } = useSelector(state => state.counter)
  const dispatch = useDispatch()
  return (
    <div>
      {count}
      <hr />
      <button onClick={() => dispatch(increment())}>+</button>&nbsp;
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

export default App;

提交action传参#

在 reducers 的同步修改方法中添加 action 对象参数,在调用 actionCreater 的时候传递参数,参数会被传递到 action 对象 payload 属性上。

Payload 只能收到一个参数,且不能是对象,但我们可以通过数组传递多个参数。

// counterStore
import { createSlice } from "@reduxjs/toolkit"
import { act } from "react-dom/test-utils"
// const {createSlice} = require('@reduxjs/toolkit')

const counterStore = createSlice({
    name: 'counter',
    // 初始化state
    initialState: {
        count: 0
    },
    // 修改状态的方法 同步方法 支持直接修改
    reducers: {
        increment(state) {
            state.count++
        },
        decrement(state) {
            state.count--
        },
        addToNum(state, action) {
            state.count = (state.count
                + action.payload[0]
                + action.payload[1]
                + action.payload[2].a
                + action.payload[2].b)
        }
    }
})

// 结构actionCreater函数
const { increment, decrement, addToNum } = counterStore.actions
// 获取reducer
const reducer = counterStore.reducer
// 以按需导出的方式导出actionCreater
export { increment, decrement, addToNum }
// 以默认导出的方式导出reducer
export default reducer
// app.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, addToNum } from './store/modules/counterStore';

function App() {
  const { count } = useSelector(state => state.counter)
  const dispatch = useDispatch()
  return (
    <div>
      {count}
      <hr />
      <button onClick={() => dispatch(increment())}>+</button>&nbsp;
      <button onClick={() => dispatch(decrement())}>-</button>&nbsp;
      <button onClick={
      () => dispatch(addToNum([5, 10, {a: 2, b: 3}]))
      }>+20</button>
    </div>
  );
}

export default App;

异步状态操作#

  • 单独封装一个函数,在函数内部 return 一个新函数,在新函数中:

    • 封装异步请求获取数据

    • 调用同步 actionCreater 传入异步数据生成一个 action 对象,并使用 dispatch 提交

    // 结构actionCreater
    const { setChannels } = channelStore.actions
    
    // 异步请求
    const url = 'http://geek.itheima.net/v1_0/channels'
    const fetchChannel = () => {
        return async (dispatch) => {
            const res = await axios.get(url)
            dispatch(setChannels(res.data.data.channels))
        }
    }

各文件代码#

channelStore

// channelStore
import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"

const channelStore = createSlice({
    name: 'channel',
    initialState: {
        channelList: [],
    },
    reducers: {
        setChannels(state, action) {
            state.channelList = action.payload
        },
    }
})

// 结构actionCreater
const { setChannels } = channelStore.actions

// 异步请求
const url = 'http://geek.itheima.net/v1_0/channels'
const fetchChannel = () => {
    return async (dispatch) => {
        const res = await axios.get(url)
        dispatch(setChannels(res.data.data.channels))
    }
}

export { fetchChannel }
const reducer = channelStore.reducer
export default reducer

入口文件

import { configureStore } from "@reduxjs/toolkit";
// 导入子模块reducer
import channelReducer from './modules/channelStore'

const store = configureStore({
    reducer: {
        channel: channelReducer
    }
})

export default store

App

import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux';
import { fetchChannel } from './store/modules/channelStore';

function App() {
  const { channelList } = useSelector(state => state.channel)
  // state 来源于store入口文件的reducer
  // 结构出来的参数来源于 Stroe 里的 initialState 中的数据
  const dispatch = useDispatch()
  // 使用useEffect触发异步请求执行
  useEffect(() => {
    dispatch(fetchChannel())
    console.log(dispatch);
  }, [dispatch])
  return (
    <div>
      <ul>
        {channelList.map(item => (<li key={item.id}>{item.name}</li>))}
      </ul>
    </div>
  );
}

export default App;
06.Redux
https://sytusramlethal.github.io/posts/06redux/
作者
Sytus Ramlethal
发布于
2025-03-28
许可协议
CC BY-NC-SA 4.0