React状态管理(类似于Vuex)

296 阅读13分钟

状态管理

React中,没有专门的状态管理库,都是js通用的状态管理库来进行全局的数据存储和管理。

所以在React中要实现状态管理就比较麻烦,首先需要创建一个全局的数据存储和数据管理的工具,然后通过其他工具让全局数据的修改能触发React页面更新。

image.png

Redux是最老的全局数据管理库,@reduxjs/toolkit是Redux的进化版本。

Redux、@reduxjs/toolkit和MobX这三个库所创建的全局数据管理对象就是单纯的js对象,和React无关并不能够触发React的更新,所以需要将它们所创建的全局数据管理对象通过另外一个库来和React的组件进行连接。

Redux介绍

Redux是一个流行的JavaScript框架,通常与 React 框架一起使用,但它可以与任何 JavaScript 框架或库一起工作。Redux 用于在应用中处理全局状态管理,确保状态的变化是可预测的。

redux.png

Redux基本原则:

  • 单一数据源
  • state是只读的
  • 使用纯函数来执行修改

Redux核心API:

  • legacy_createStore(reducer):用于创建store数据仓库,需要引入import {legacy_createStore} from "redux";
  • store.getState():用于获取store里面的数据
  • store.dispatch(action):用于派发action,触发reducer修改store里面的数据,它接受一个对象。
  • store.subscribe(methods):订阅store的修改,它接受一个函数。只要store发生改变,传入的回调函数就会被执行

redux-store.png

Redux使用

安装模块:

npm i redux
npm i react-redux

创建仓库

在项目中src目录下新建store目录,在store目录下新建index.js文件,在该文件中编写reducer方法。

Redux创建store数据仓库就是编写reducer方法,然后在方法中给state默认值作为初始数据,接着在方法中判断type来进行修改数据,最后在方法中调用legacy_createStore创建store仓库

reducer是一个纯函数,接收两个参数:当前的状态(state)也就是修改前上一次的状态和动作对象(action),然后返回新的状态。

  1. 当前的状态state,也就是仓库store的数据
  2. 动作对象action表示发生了什么,是一个普通的 JavaScript 对象。必须有一个 type 属性来表示这个 action 的类型,还可以包含额外的信息作为 payload属性,这个payload也就是需要修改的数据。type 属性、payload属性这两个属性名是任意命名的,也就是调用store.dispatch(action)这个方法时需要传入一个对象,至于具体传入什么样的对象取决于开发者。但通常都是传入type 属性表示修改store仓库的哪个数据,payload属性表示修改的数据的值。

Redux创建store数据仓库代码:

// src/store/index.js
import { legacy_createStore as createStore } from 'redux'
const defaultState = {
    singer: "G.E.M.",
    album: ['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录']
}
const reducer = (state = defaultState, action) => {
    /** reducer方法接受两个参数state,action
     * state是当前的状态也就是仓库的数据,并给state仓库数据赋值一个默认值defaultState作为初始数据
     * action是动作对象
     **/
    // 具体修改数据的行为
    switch (action.type) {
        case "CHANGESINGER":
            state.singer = action.payload;
            break;
        case "RESETSINGER":
            state.singer = "G.E.M.";
            break;
        default:
            break;
    }
    // 返回的新的状态state需要深拷贝,这样的目的是解除对原来的state状态的引用
    state = JSON.parse(JSON.stringify(state))
    // 最后reducer方法必须返回处理后的新的状态state
    return state;
}
// 调用redux的legacy_createStore来创建store数据仓库
let store = createStore(reducer);
export default store; // 将创建的仓库暴露出去

state进行深拷贝的原因是,当修改后通过react-redux触发React进行重新渲染时会将之前的store的状态和现在获取到的修改后的store的状态进行全等判断,如果store中只是修改了引用数据的某个属性,而引用地址没有改变,React是不会重新渲染页面的,这也保证了状态的唯一性。

App.jsx中引入仓库,并在控制台输出查看创建的store数据仓库

// App.jsx
import React from 'react';
// 引入仓库
import store from './store/index.js';
// 控制台输出仓库
console.log(store,'store');
function App() {
  return (
    <div>
      <h1>My App</h1>
    </div>
  );
}

export default App;

image.png

其实store数据仓库就是一个对象,里面有一些方法。最关键的两个方法就是dispatch方法,用于触发对某个数据的修改和getState方法,用于获取state状态的数据。

获取仓库数据

store.getState():用于获取store里面的数据

// App.jsx
import React from 'react';
// 引入仓库
import store from './store/index.js';
// 获取仓库的数据
let state = store.getState();
console.log(state, 'state');
function App() {
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{state.singer}</p>
      <p>专辑:</p>
      {
        state.album.map((item,index) => {
          return <p key={index}>{item}</p>
        })
      }
    </div>
  );
}

export default App;

image.png

修改仓库数据

store.dispatch(action):用于修改store里面的数据

// App.jsx
import React from 'react';
// 引入仓库
import store from './store/index.js';
// 获取仓库的数据
let state = store.getState();
function App() {
  const changeStore = () => {
    // 此处传入的对象就是reducer方法中接受的action参数
    store.dispatch({type:'CHANGESINGER',payload:"G.E.M. 邓紫棋"});
  }
  const looStore = () => {
    console.log(store.getState())
  }
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{state.singer}</p>
      <p>专辑:</p>
      {
        state.album.map((item,index) => {
          return <p key={index}>{item}</p>
        })
      }
      <button onClick={changeStore}>修改store</button>
      <button onClick={looStore}>查看store</button>
    </div>
  );
}

export default App;

2.gif

页面上的数据并没有被修改,但是store里面的数据确实被修改了。因为Redux是一个单纯的JavaScript框架,它更新了store里面的数据并不会触发React更新页面的数据。

Redux更新了store里面的数据虽然不会触发React更新页面的数据,但是它会触发Redux自己的subscribe监听,store.subscribe(methods)就是通用于订阅store的修改。

使用react-redux将redux和React连接起来

首先引入react-redux的Provider组件,通过这个组件将redux创建的store仓库关联到项目中,然后利用react-redux的connect方法连接到某个要使用store仓库中数据的组件,最后编写一些映射关系。

注册react-redux插件,将react-redux和React进行关联来触发React更新页面数据

在React中注册插件就是引入一个组件,然后把要使用该插件的部分用引入的组件包裹起来。所以React中全局注册react-redux插件就是,将引入的react-redux的Provider组件把App组件包裹起来。

需要把redux创建的store仓库注入到react-redux的Provider组件,这样才能将 redux的 store 注入到 React 组件树中,使所有组件都能访问到 store

// main.jsx
import { createRoot } from 'react-dom/client' // 用于渲染页面的
import React from 'react' // 用于创建组件的
import { Provider } from 'react-redux'; // 引入react-redux的Provider组件,然后用这个组件包裹App组件完成react-redux的全局注册
import App from './App.jsx'

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

react-redux的connect方法连接要使用store仓库数据的组件

调用connect方法会返回一个高阶组件,然后把使用store仓库数据的组件传给返回的这个高阶组件。这样就使用了connect方法来连接 React 组件和 redux的 store。不过在最新的 react-redux 版本中,推荐使用 useSelectoruseDispatch Hook,还有一个Hook是useStore

  • useSelector: 用于从 redux的store 中选择数据,并订阅数据变化,当所选的数据发生变化时,React的组件会自动重新渲染。
  • useDispatch: 用于获取 redux的store 的 dispatch 函数,以便在组件中修改数据。
  • useStore 没有参数,它直接返回当前的redux的store实例,可以通过这个实例调用 redux的getStatedispatchsubscribe 等方法

connect方法可以在类组件和函数组件中使用,useSelectoruseDispatch等Hook只能在函数组件中使用。

使用connect方法

因为将组件传入了connect方法返回的高阶组件,所以就可以通过组件的props属性来获取react-redux注入到React 组件树中的redux的 store

//App.jsx
import React from 'react';
// 引入connect组件
import { connect } from 'react-redux'
function App(props) {
  console.log(props,'props');
  return (
    <div>
      <h1>My App</h1>
    </div>
  );
}

export default connect()(App);

image.png

props属性中添加了dispatch方法

connect方法参数

connect 方法接受四个参数,分别是 mapStateToPropsmapDispatchToPropsmergePropsoptions,前面三个参数都是一个函数,第四个参数是一个配置对象。下面是每个参数作用和用法的详细介绍:

  1. mapStateToProps(state, [ownProps]),作用是将 redux 的store 中的状态映射到传入connect方法返回的高阶组件中的 React 组件的 props。换而言之,它的作用是要给React组件的props加入redux 的store 中的哪些数据。

参数:

  • state:redux 的store的当前状态,也就是修改前的上一次的状态。
  • ownProps(可选):传入connect方法返回的高阶组件中的React 组件的 props

mapStateToProps(state, [ownProps])需要提供一个返回值:一个对象,其键值对会被合并到传入connect方法返回的高阶组件中的 React 组件的 props中。如果想映射redux 的store的所有状态可以直接返回redux 的整个store的状态,如果只想映射redux 的store的某些状态就把数据的属性名和属性值作为返回对象的键值对。

示例:

const mapStateToProps = (state, ownProps) => {
    return {
        todos: state.todos, 
        userId: ownProps.userId 
    }
};

通过这个参数和高阶组件注入到props属性中的dispatch方法就可以完成修改redux 的store的数据并触发React更新页面。

// App.jsx
import React from 'react';
// 引入connect组件
import { connect } from 'react-redux'
function App(props) {
  console.log(props, 'props');
  const changeStore = () => {
    // 调用注入到props属性的dispatch来修改数据,此处传入的对象同样是reducer方法中接受的action参数
    props.dispatch({ type: 'CHANGESINGER', payload: "G.E.M. 邓紫棋" });
  }
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{props.singer}</p>
      <p>专辑:</p>
      {
        props.album.map((item, index) => {
          return <p key={index}>{item}</p>
        })
      }
      <button onClick={changeStore}>修改store</button>
    </div>
  );
}
let ReduxApp = connect((state) => state)(App);
export default ReduxApp;

2.gif

react-redux是将redux 的store状态注入到了React组件的props属性,所以当修改数据时就相当于修改了React组件的props属性,这也就触发了React更新页面的操作,重新运行了函数组件的函数体。所以react-redux和React进行关联的工作原理就是将redux 的store的数据映射到React组件的props属性。

rudecer方法需要返回新的状态,将state进行深拷贝的原因也是如果store状态注入到React组件的props属性中的引用数据只是修改了值,引用地址不变,修改前后的props属性是进行浅比较,会认为props属性没有改变,就不会React的更新重新渲染页面。

  1. mapDispatchToProps(dispatch, [ownProps]),作用是将 redux 的dispatch 函数映射到传入connect方法返回的高阶组件中的 React 组件的 props。换而言之,它的作用是要给React组件的props加入哪些方法。

参数:

  • dispatch:redux 的storedispatch 函数。
  • ownProps(可选):React 组件的 props。

mapDispatchToProps(dispatch, [ownProps])需要提供一个返回值:一个对象,其键值对会被合并到传入connect方法返回的高阶组件中的 React 组件的 props中。

示例:

const mapDispatchToProps = (dispatch, ownProps) => ({ 
    addTodo: (text) => dispatch(addTodo(text)), 
    clearTodos: () => dispatch(clearTodos()) 
});

通过这个参数可以将封装的dispatch方法映射到props属性中,如果写了这个参数,在props属性中就不会在注入dispatch方法,而是将dispatch方法作为mapDispatchToProps(dispatch, [ownProps])这个参数的第一个参数,然后自己封装一些修改方法。

// App.jsx
import React from "react"
import { connect } from 'react-redux'
function App(props) {
  console.log(props, 'props');
  const changeStore = () => {
    // 调用注入到props属性的方法来修改数据,是封装过的dispatch方法
    props.changSiger("G.E.M. 邓紫棋");
  }
  const resetStore = () => {
    // 调用注入到props属性的方法来修改数据,是封装过的dispatch方法
    props.resetSinger();
  }
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{props.singer}</p>
      <p>专辑:</p>
      {
        props.album.map((item, index) => {
          return <p key={index}>{item}</p>
        })
      }
      <button onClick={changeStore}>修改歌手名</button>
      <button onClick={resetStore}>重置歌手名</button>
    </div>
  );
}
let ReduxApp = connect((state) => state, (dispatch) => {
  return {
    changSiger: (val) => {
      // 调用传入的dispatch方法修改store仓库的数据,此处传入的对象同样是reducer方法中接受的action
      dispatch({ type: 'CHANGESINGER', payload: val })
    },
    resetSinger: () => {
      // 调用传入的dispatch方法修改store仓库的数据,此处传入的对象同样是reducer方法中接受的action
      dispatch({ type: 'RESETSINGER' })
    }
  }
})(App);
export default ReduxApp;

2.gif

image.png

React组件的props属性中就不会在注入dispatch方法

  1. mergeProps(stateProps, dispatchProps, ownProps),作用是将 mapStateToPropsmapDispatchToProps 返回的对象以及组件自身的 props 合并成一个最终的 props 对象。

参数:

  • statePropsmapStateToProps 返回的对象。
  • dispatchPropsmapDispatchToProps 返回的对象。
  • ownProps:React 组件的 props

mergeProps(stateProps, dispatchProps, ownProps)需要提供一个返回值返回值:一个对象,最终会被传递给组件。

示例:

const mergeProps = (stateProps, dispatchProps, ownProps) => ({
    ...ownProps, 
    ...stateProps, 
    ...dispatchProps, 
    customProp: 'custom value' 
});

通过这个参数可以将所有要传入React组件的数据合并成一个对象映射到props属性

// main.jsx
import { createRoot } from 'react-dom/client' // 用于渲染页面的
import React from 'react' // 用于创建组件的
import { Provider } from 'react-redux'
import store from './store/index.js'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
    <Provider store={store}>
        <App msg='我是一条消息' />
    </Provider>
)
// App.jsx
import React from "react"
import { connect } from 'react-redux'
function App(props) {
  console.log(props, 'props');
  const changeStore = () => {
    // 调用注入到props属性的方法来修改数据,是封装过的dispatch方法
    props.changSiger("G.E.M. 邓紫棋");
  }
  const resetStore = () => {
    // 调用注入到props属性的方法来修改数据,是封装过的dispatch方法
    props.resetSinger();
  }
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{props.singer}</p>
      <p>专辑:</p>
      {
        props.album.map((item, index) => {
          return <p key={index}>{item}</p>
        })
      }
      <button onClick={changeStore}>修改歌手名</button>
      <button onClick={resetStore}>重置歌手名</button>
    </div>
  );
}
let ReduxApp = connect((state) => state, (dispatch) => {
  return {
    changSiger: (val) => {
      // 调用传入的dispatch方法修改store仓库的数据,此处传入的对象同样是reducer方法中接受的action
      dispatch({ type: 'CHANGESINGER', payload: val })
    },
    resetSinger: () => {
      // 调用传入的dispatch方法修改store仓库的数据,此处传入的对象同样是reducer方法中接受的action
      dispatch({ type: 'RESETSINGER' })
    }
  }
}, (stateProps, dispatchProps, ownProps) => ({
  ...ownProps,
  ...stateProps,
  ...dispatchProps,
  customProp: 'custom value'
}))(App);
export default ReduxApp;

image.png

  1. options(可选),作用是提供一些配置选项,用于优化性能或自定义行为。

常用选项:

  • pure:布尔值,表示是否启用纯组件优化,默认为 true
  • areStatesEqual:一个函数,用于比较两个状态对象(修改前的store状态和修改后的store状态)是否相等。
  • areOwnPropsEqual:一个函数,用于比较组件自身的两个 props(修改前的props属性和修改后的props属性) 对象是否相等。
  • areStatePropsEqual:一个函数,用于比较 mapStateToProps 返回的两个 props 对象是否相等。
  • areMergedPropsEqual:一个函数,用于比较 mergeProps 返回的两个 props 对象是否相等。

使用useStore获取 store 实例

// App.jsx
import React from "react"
import { useSelector, useDispatch, useStore } from 'react-redux'
function App(props) {
  console.log(props,'props')
  // 获取redux的store示例
  const store = useStore();
  console.log(store,'store');
  // 获取当前的state
  const state = store.getState();
  console.log(state,'state')
  // 用dispatch修改数据
  const changeStore = () => {
    // 调用store实例的dispatch方法
    store.dispatch({
      type:"CHANGESINGER",
      payload:"G.E.M. 邓紫棋"
    })
  }
  const lookStore = () => {
    console.log(store.getState(),'look look');
  }
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{state.singer}</p>
      <p>专辑:</p>
      {
        state.album.map((item, index) => {
          return <p key={index}>{item}</p>
        })
      }
      <button onClick={changeStore}>修改歌手名</button>
      <button onClick={lookStore}>查看数据</button>
    </div>
  );
}
export default App;

2.gif

image.png

从示例中能够看出通过store实例获取的store数据在修改后不会更新

注意事项:

  1. 性能优化:直接使用 store.getState() 获取状态不会触发组件的重新渲染。如果需要在状态变化时重新渲染组件,建议使用 useSelector 而不是 useStore
  2. 高级用法:useStore 主要用于那些需要直接操作 store 的场景,例如在自定义 Hook 中。

使用 useSelector获取store中的数据

useSelector Hook 用于从 store 中选择数据,并将其订阅到组件中。当所选的数据发生变化时,组件会自动重新渲染。它接收 redux的 store 的当前状态作为参数,返回需要的数据。

// App.jsx
import React from "react"
import { useSelector, useDispatch } from 'react-redux'
function App(props) {
  console.log(props, 'props')
  // 获取当前的state
  let singer = useSelector((state) => state.singer);
  console.log(singer,'singer');
  let album = useSelector((state) => state.album);
  console.log(album,'album');
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{singer}</p>
      <p>专辑:</p>
      {
        album.map((item, index) => {
          return <p key={index}>{item}</p>
        })
      }
    </div>
  );
}
export default App;

image.png

使用 useDispatch获取store中的dispatch 函数

useDispatch Hook 用于获取 redux的 storedispatch 函数,以便在组件中修改仓库数据。

// App.jsx
import React from "react"
import { useSelector, useDispatch } from 'react-redux'
function App(props) {
  console.log(props, 'props')
  // 获取当前的state
  let singer = useSelector((state) => state.singer);
  console.log(singer, 'singer');
  let album = useSelector((state) => state.album);
  console.log(album, 'album');
  const dispatch = useDispatch();
  // 用dispatch修改数据
  const changeStore = () => {
    // 调用s获取 redux的 store的dispatch函数修改数据
    dispatch({
      type: "CHANGESINGER",
      payload: "G.E.M. 邓紫棋"
    })
  }
  const resetStore = () => {
    dispatch({type:"RESETSINGER"})
  }
  return (
    <div>
      <h1>My App</h1>
      <p>歌手:{singer}</p>
      <p>专辑:</p>
      {
        album.map((item, index) => {
          return <p key={index}>{item}</p>
        })
      }
      <button onClick={changeStore}>修改歌手名</button>
      <button onClick={resetStore}>重置歌手名</button>
    </div>
  );
}
export default App;

2.gif