前言
状态管理:
- redux:操作都是同步的,异步action 需要使用插件
- rematch:基于 redux 的状态管理库
- useReducer:使用 React Hook 的简单状态管理
- 其他:unstated-next:使用 react API 的状态管理库;
代码:react-test
前面几篇笔记:
异步 action
-
为什么需要 异步action?
最简单的做法是:在组件里面异步请求然后再
dispatch
更新,但是一旦多个组件使用,且需要更新这个 state 那么就要在多处写逻辑了。。。 -
什么时候用 异步action?
- 可以单独把异步请求然后dispatch的函数封装起来,需要用到的组件,只要引入这个封装的函数(action),加上插件如
redux-thunk
使得 dispatch 可以接收函数参数,然后dispatch那个函数就行了~ - 原来我也不清楚具体应用场景,谢谢大佬的 这篇文章 ,看了的评论才明白为什么要在 action 里面做异步操作。。。
- 可以单独把异步请求然后dispatch的函数封装起来,需要用到的组件,只要引入这个封装的函数(action),加上插件如
数据持久化 (localStorage/sessionStorage)
- 不使用插件 最简单的写法:
实际上,如果可能的话,最好只选择某些 必要的字段 缓存~
// 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/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-
reducer (Function): 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。
-
[preloadedState] (any): 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。
-
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.type
,dispatch
执行后都会被调用,然后反映到各自的 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
接收一个参数state
,return
一个对象,这个对象会被merge
到props
上,一般是引用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
- 接收一个参数
dispatch
,return
一个对象,这个对象会被merge
到props
上; - 一般是
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 并没有太大的区别,但是代码量少了一些,也比较清晰,用起来舒服一点
有几点:
- 不需要单独设定
action.type
, type 由模块名 + '/' + reducers 的 key
自动生成,
如{ type: 'countRematch/increment' }
effects:{}
属性: 接收一些方法,如异步 action,搭配 async/await,就不需要 redux-thunk 等库redux:{}
属性: 兼容 redux- init 初始化 的 store 可以使用 redux
createStore
生成的 store 一些方法,如store.subscribe
可以监听每次 state 的变化,然后可以做数据持久化sessionStorage/localStorage- 其他的看文档吧
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 索引
参考
- Redux 中文文档
- 异步action之 redux-thunk
- 基于redux封装的状态管理库 rematch
- useReducer: 使用 React Hook 的简单状态管理