React状态管理:redux,rematch,useReducer

2,796 阅读10分钟

前言

状态管理:

  • redux:操作都是同步的,异步action 需要使用插件
  • rematch:基于 redux 的状态管理库
  • useReducer:使用 React Hook 的简单状态管理
  • 其他:unstated-next:使用 react API 的状态管理库;

代码:react-test

前面几篇笔记:

  1. react学习笔记一:路由及懒加载
  2. react学习笔记二:css module
  3. React使用之:Hooks和数据展示处理

异步 action

  1. 为什么需要 异步action?

    最简单的做法是:在组件里面异步请求然后再 dispatch 更新,但是一旦多个组件使用,且需要更新这个 state 那么就要在多处写逻辑了。。。

  2. 什么时候用 异步action?

    • 可以单独把异步请求然后dispatch的函数封装起来,需要用到的组件,只要引入这个封装的函数(action),加上插件如 redux-thunk 使得 dispatch 可以接收函数参数,然后dispatch那个函数就行了~
    • 原来我也不清楚具体应用场景,谢谢大佬的 这篇文章 ,看了的评论才明白为什么要在 action 里面做异步操作。。。

数据持久化 (localStorage/sessionStorage)

  1. 不使用插件 最简单的写法:

实际上,如果可能的话,最好只选择某些 必要的字段 缓存~

// src/store/index.js
// 省略其他代码

// 需要缓存的列表
const cacheList = ['numReducer', 'countReducer'];
let stateCache = sessionStorage.getItem('store');
// 初始化的 state
const initState = (stateCache && JSON.parse(stateCache)) || {};

// stroe: { subscribe, dispatch, getState, replaceReducer }
const store = createStore(reducers, initState, applyMiddleware(ReduxThunk));

// 监听每次 state 的变化
store.subscribe(() => {
  const state = store.getState();
  let stateData = {};

  Object.keys(state).forEach(item => {
    if (cacheList.includes(item)) stateData[item] = state[item];
  });

  sessionStorage.setItem('store', JSON.stringify(stateData));
});
  1. 插件
    redux-persist

1、redux/react-redux

1.1 安装

npm i -S redux
npm i -S react-redux

1.2 redux

  • 1.2.1 combineReducers

合并多个 reducer

// src/store/index.js
import { combineReducers, createStore } from 'redux';
import countReducer from './count-reducer.js';
import numReducer from './num-reducer.js';

// reducers 集中处理
const reducers = combineReducers({
  countReducer,
  numReducer
});

// stroe: { subscribe, dispatch, getState }
const store = createStore(reducers);
// ...

export default store;
  • 1.2.2 createStore

    格式: const store = createStore(reducer, [preloadedState], enhancer);
    参数: 摘抄自 Store

    1. reducer (Function): 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。

    2. [preloadedState] (any): 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。

    3. enhancer (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。applyMiddleware 就是其中一个实现

    store 有四个方法:{ subscribe, dispatch, getState, replaceReducer }

    • store.subscribe

    订阅:在这里可以做持久性存储(localStorage/sessionStorage),vuex就是在 store 的 这个地方做的

    // 可以手动订阅更新
    let unsubscribe = store.subscribe(() => {
      const state = store.getState();
      console.log(state);
    });
    
    // 解除监听
    unsubscribe();
    
    • store.dispatch

    发送/派发:改变内部 state 惟一方法是 dispatch 一个 action。
    可以直接在这里调用,修改 state

    store.dispatch({ type: 'INCREMENT' });
    
    • store.getState

    获取初始的 state

    // 获取初始 state
    const state = store.getState();
    

项目入口:

// src/index.js
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import store from '@/store';

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

1.3 react-redux

  • 1.3.1 Provider

// src/index.js
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import store from '@/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  • 1.3.2 connect

    关联React组件和Redux;

    格式:connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

    • mapStateToProps: 注入state状态
    • mapDispatchToProps: 注入dispatch方法
    • mergeProps: 合并属性,比较复杂,场景简单没用到。。。
    • options: 定制 connector 的行为,场景简单没用到。。。
// src/components/redux-1.jsx
import { connect } from 'react-redux';
import React from 'react';
import { connect } from 'react-redux';
// 省略其他代码

class ReduxTest2 extends React.Component {...}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

export default connect(mapStateToProps)(ReduxTest2);
// src/components/redux-2.jsx
// 省略其他代码
class ReduxTest1 extends React.Component {...}

function mapDispatchToProps(dispatch) {
  return {
    dispatch,
    Add: () => {
      return dispatch({ type: 'INCREMENT' });
    },
    Todo: todo => dispatch({ type: 'TODO_LIST', todoList: todo })
  };
}

// 只注入 dispatch,不监听 store
export default connect(
  null, // 如果只有 dispatch,而不需要 state,这里必须要一个占位
  mapDispatchToProps
)(ReduxTest1);

1.4 使用前的准备

1.4.1 reducer

形式:(state, action) => state
指定了应用状态的变化如何响应 actions 并发送到 store 的,actions 只是对触发事件的描述,reducer 才是执行者。
返回一个新的 state

  • 1.4.1.1 reducer 集中

// src/store/index.js
import { combineReducers, createStore } from 'redux';
import countReducer from './count-reducer.js';
import numReducer from './num-reducer.js';

// reducers 集中处理
const reducers = combineReducers({
  countReducer,
  numReducer
});

// stroe: { subscribe, dispatch, getState }
const store = createStore(reducers);
// ...

export default store;
  • 1.4.1.2 reducer 模块

有多个 reducer 使用 combineReducers 后,操作的结果反映到各自模块的 state,如 state.countReducer
如果多个 reducer 有相同的 action.typedispatch 执行后都会被调用,然后反映到各自的 state

点击 count++ 两次:

点击todo:

// src/components/redux-test/redux-2.jsx
...
  render() {
    return (
      <React.Fragment>
        <div>todo: {this.props.todoList}</div>
        <div>countReducer: {this.props.count}</div>
        <div>numReducer: {this.props.count1}</div>
      </React.Fragment>
    );
  }
...
function mapStateToProps(state) {
  return {
    todoList: state.countReducer.todoList,
    count: state.countReducer.count,
    count1: state.numReducer.count,
    json: state.countReducer.json
  };
}
...
// src/components/redux-test/redux-1.jsx
...
  render() {
    return (
      <React.Fragment>
        <div>
          <button
            onClick={() => this.props.Todo('干嘛' + new Date().getTime())}
          >
            todo
          </button>
        </div>
        <div>
          <button onClick={this.props.Add}>count++</button>
        </div>
      </React.Fragment>
    );
  }
...

function mapDispatchToProps(dispatch) {
  return {
    dispatch,
    Add: () => dispatch({ type: 'INCREMENT' }),
    Todo: todo => dispatch({ type: 'TODO_LIST', todoList: todo })
  };
}
...
// src/store/reducers/count-reducer.js
import { INCREMENT, TODO_LIST, JSON_DATA } from '../types';

// 这里的参数默认值,比createState 的初始 initState 优先级低
function countReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state,
        count: state.count + 1
      };
    case TODO_LIST:
      return {
        ...state,
        todoList: action.todoList
      };
    case JSON_DATA:
      return {
        ...state,
        json: action.data
      };
    default:
      return state;
  }
}

export default countReducer;
// src/store/reducers/num-reducer.js
import { INCREMENT } from '../types';

// 这里的参数默认值,比createState 的初始 initState 优先级低
function numReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state,
        count: state.count + 2
      };
    default:
      return state;
  }
}

export default numReducer;

1.5 dispatch

mapDispatch2Props 的一个参数,用来调用某个 reducer(由 action 决定);
改变内部 state 惟一方法是 dispatch 一个 action

1.6 action

action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作,一般作为 dispatch 的参数

1.6.1 异步 action 之 redux-thunk 插件

使dispatch 支持函数参数;如果两个组件都用到同一个 state,且都需要异步操作之后更新 state,这个时候使用 redux-thunk 然后 dispatch 可以传入 异步action 函数,就可以避免两个地方写一样的逻辑了

下面代码写的不是很规范,一般异步action,要有三个 action.type 对应 request, success, failed 的,偷懒了用同一个 action.type~

注意:异步action 的 dispatch 传入的是 自定义的异步action函数,然后在action函数内部进行dispatch!!! 本质上与先异步请求再手动dispatch一样,但是这种写法可以避免多个地方重复代码,可读性较好

  • action
// src/store/actions/index.js
// 异步请求数据
export function asyncAction({ url = './manifest.json', type }) {
  return dispatch => {
    dispatch({ type: type, data: 'loading' });
    return fetch(url)
      .then(res => res.json())
      .then(json => {
        return dispatch({ type: type, data: json });
      })
      .catch(err => {
        return dispatch({ type: type, data: err });
      });
  };
}
  • reducer
// src/store/reducers/count-reducer.js
// 省略其他代码
// 这里的参数默认值,比createState 的初始 initState 优先级低
function countReducer(state = { count: 0 }, action) {
  switch (action.type) {
    ...
    case 'JSON_DATA':
      return {
        ...state,
        json: action.data
      };
    default:
      return state;
  }
}

export default countReducer;
  • dispatch
// src/store/index.js
// 省略其他代码
import { asyncAction } from '@/store/actions';

store.dispatch(
  asyncAction({
    url: './manifest.json',
    type: 'JSON_DATA'
  })
);

1.7 开始使用

store 目录的结构:

 D:\code\react-t1\src\store
    ├─ actions
      └─ index.js           // 异步action封装
    ├─ index.js             // store 入口
    ├─ reducers             // reducer 描述
      ├─ count-reducer.js
      └─ num-reducer.js
    └─ types                // action.type 定义
      └─ index.js

1.7.1 项目入口

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './reducers';
import App from './App';
import * as serviceWorker from './serviceWorker';
import './index.css';

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

1.7.2 mapState2Props

接收一个参数statereturn一个对象,这个对象会被mergeprops上,一般是引用store.state数值

// src/components/redux-test/redux-2.jsx
import React from 'react';
import { connect } from 'react-redux';

class ReduxTest2 extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <React.Fragment>
        <div>todo: {this.props.todoList}</div>
        <div>countReducer: {this.props.count}</div>
        <div>numReducer: {this.props.count1}</div>
      </React.Fragment>
    );
  }
}

function mapStateToProps(state) {
  return {
    todoList: state.countReducer.todoList,
    count: state.countReducer.count,
    count1: state.numReducer.count,
    json: state.countReducer.json
  };
}

export default connect(mapStateToProps)(ReduxTest2);

1.7.3 mapDispatch2Props

  • 接收一个参数 dispatchreturn 一个对象,这个对象会被 mergeprops 上;
  • 一般是 store 的事件函数
  • 对象的 key 的值一般是 dispatch 一个 action 然后 reducer 内部处理 返回一个新的 state ,看《1.4.1 reducer
// src/components/redux-test/redux-1.jsx
import React from 'react';
import { connect } from 'react-redux';
import { asyncAction } from '@/actions';

// redux练习
class ReduxTest1 extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount = () => {
    // 异步 action
    this.props.dispatch(
      asyncAction({ url: './manifest.json', type: 'JSON_DATA' })
    );
  };

  render() {
    return (
      <React.Fragment>
        <div>
          <button
            onClick={() => this.props.Todo('干嘛' + new Date().getTime())}
          >
            todo
          </button>
        </div>
        <div>
          <button onClick={this.props.Add}>count++</button>
        </div>
      </React.Fragment>
    );
  }
}

function mapDispatchToProps(dispatch) {
  return {
    dispatch,
    Add: () => {
      return dispatch({ type: 'INCREMENT' });
    },
    Todo: todo => dispatch({ type: 'TODO_LIST', todoList: todo })
  };
}

// 只注入 dispatch,不监听 store
export default connect(
  null, // 如果只有 dispatch,而不需要 state,这里必须要一个占位
  mapDispatchToProps
)(ReduxTest1);

2、rematch 的使用

Rematch 是没有 boilerplate 的 Redux 最佳实践。
没有多余的 action types,action creators,switch 语句或者thunks。
使用与 redux 并没有太大的区别,但是代码量少了一些,也比较清晰,用起来舒服一点

有几点:

  1. 不需要单独设定 action.type, type 由 模块名 + '/' + reducers 的 key 自动生成,
    { type: 'countRematch/increment' }
  2. effects:{}属性: 接收一些方法,如异步 action,搭配 async/await,就不需要 redux-thunk 等库
  3. redux:{}属性: 兼容 redux
  4. init 初始化 的 store 可以使用 redux createStore生成的 store 一些方法,如 store.subscribe 可以监听每次 state 的变化,然后可以做数据持久化sessionStorage/localStorage
  5. 其他的看文档吧

rematch 目录结构:

D:\code\react-t1\src\store-rematch
    ├─ index.js
    └─ models
      ├─ countRematch.js
      └─ index.js

例:

npm install @rematch/core
import { init, dispatch, getState } from '@rematch/core'

2.1 models

// src/store-rematch/models/index.js
import countRematch from './countRematch.js';
export { countRematch };
// src/store-rematch/models/countRematch.js
let models = {
  state: {
    count: 0,
    JSON_DATA: ''
  },
  reducers: {
    increment(state) {
      return {
        ...state,
        count: state.count + 1
      };
    },
    setJSON_DATA(state, data) {
      return {
        ...state,
        JSON_DATA: data
      };
    }
  },
  effects: {
    async getJsonData() {
      await fetch('./manifest.json')
        .then(res => res.json())
        .then(json => {
          this.setJSON_DATA(json);
        })
        .catch(err => {
          this.setJSON_DATA(err);
        });
    }
  }
};

export default models;

2.2 store

// src/store-rematch/index.js
import { init } from '@rematch/core';
import * as models from './models';

// 需要缓存的列表
const cacheList = ['countRematch'];
const stateCache = sessionStorage.getItem('store-rematch');
// 初始化的 state
const initialState = (stateCache && JSON.parse(stateCache)) || {};

const store = init({
  models: {
    ...models
  },
  redux: {
    initialState: initialState
  }
});

// 监听每次 state 的变化
store.subscribe(() => {
  const state = store.getState();
  let stateData = {};

  Object.keys(state).forEach(item => {
    if (cacheList.includes(item)) stateData[item] = state[item];
  });

  sessionStorage.setItem('store-rematch', JSON.stringify(stateData));
});

export default store;

2.3 组件

// src/components/rematch-test/rematch-1.jsx
import React, { useEffect } from 'react';
import { connect } from 'react-redux';

function Rematch(props) {
  useEffect(() => {
    props.getJsonData();
  }, []);

  return (
    <div>
      <button onClick={props.Add}>count++</button>
      <div>countRematch: {props.count}</div>
    </div>
  );
}

function mapStateToProps(state) {
  return {
    count: state.countRematch.count,
    JSON_DATA: state.countRematch.JSON_DATA
  };
}

function mapDispatchToProps(dispatch) {
  return {
    Add: () => dispatch({ type: 'countRematch/increment' }),
    getJsonData: () => dispatch.countRematch.getJsonData()
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Rematch);

3、useReducer/useContext 的使用

  • useReducer: useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
  • useContext: 接收一个 context 对象(React.createContext() 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。
    当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

使用场景:

  • 做组件内的状态管理还是不错的
  • 其他。。。

3.1 简单使用:

// src/components/useReducer-test/useReducer1.jsx
import React, { useReducer, useContext } from 'react';

export function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {
        ...state,
        count: state.count + 1
      };
    case 'decrement':
      return {
        ...state,
        count: state.count - 1
      };
    default:
      return state;
  }
}

export const UseReducer1Dispatch = React.createContext(null);

// 子组件 通过父组件传递的 dispatch
export function Child1(props) {
  // 这里 UseReducer1Dispatch 是 父组件 cerateContext() 的返回值
  const dispatch = useContext(UseReducer1Dispatch);

  function handleClick() {
    dispatch({ type: 'increment' });
  }

  return (
    <div>
      <button onClick={handleClick}>Child1 count+</button>
    </div>
  );
}

export default function UseReducer1({ initialState = { count: 1 } }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <UseReducer1Dispatch.Provider value={dispatch}>
      {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>count+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>count-</button>
      <hr />
      <Child1 dispatch={UseReducer1Dispatch} />
    </UseReducer1Dispatch.Provider>
  );
}

3.2 与 useContext 配合实现 dispatch 传递

  • 父组件通过 export const UseReducer1Dispatch = React.createContext(null);
    然后 <UseReducer1Dispatch.Provider value={dispatch}>...<UseReducer1Dispatch.Provider />包裹自身,向子组件传递 dispatch,而不是回调函数
  • 子组件通过父组件生成的 createContext 返回值 UseReducer1Dispatch,
    使用 const dispatch = useContext(UseReducer1Dispatch); 获取 dispatch
// ... 代码与 3.1 相同
export const UseReducer1Dispatch = React.createContext(null);

// 子组件 通过父组件传递的 dispatch
export function Child1(props) {
  // 这里 UseReducer1Dispatch 是 父组件 cerateContext() 的返回值
  const dispatch = useContext(UseReducer1Dispatch);

  function handleClick() {
    dispatch({ type: 'increment' });
  }

  return (
    <div>
      <button onClick={handleClick}>Child1 count+</button>
    </div>
  );
}

export default function UseReducer1({ initialState = { count: 1 } }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <UseReducer1Dispatch.Provider value={dispatch}>
      {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>count+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>count-</button>
      <hr />
      <Child1 dispatch={UseReducer1Dispatch} />
    </UseReducer1Dispatch.Provider>
  );
}

更多Hook:Hook API 索引

参考