Skip to content

Instantly share code, notes, and snippets.

@otakustay
Created July 14, 2017 09:47
Show Gist options
  • Select an option

  • Save otakustay/e916dbef301c28ea0d10335d96cc0166 to your computer and use it in GitHub Desktop.

Select an option

Save otakustay/e916dbef301c28ea0d10335d96cc0166 to your computer and use it in GitHub Desktop.
Store normalization with backend API connected
/**
* @file 管理Store Nomalization的相关逻辑
* @author zhanglili
*/
import {reduce} from 'lodash';
const UPDATE_ENTITY_TABLE = 'UPDATE_ENTITY_TABLE';
/**
* 创建一个用于在请求结束后更新Normalized Store中的实体数据的高阶函数
*
* 当传递一个`store`后,会返回一个`withTableUpdate`函数,这个函数的用法如下:
*
* ```javascript
* const apiWithTableUpdate = withTableUpdate(entitySetName, selectEntities)(api);
* const response = await apiWithTableUpdate(args);
* ```
*
* 当一个用于获取后端数据的API函数被这个高阶函数包装后,会在响应返回时额外做以下行为:
*
* 1. 调用`selectEntities`函数,将`response`传入并得到一个对象,该对象是一系列需要更新的实体,以实体的索引属性(比如id)为键
* 2. 派发一个类型为`UPDATE_ENTITY_TABLE`的Action,并通过`payload`提供`entitySetName`和`entity`属性
*
* 当reducer包含了对这个Action的处理时,会有以下逻辑:
*
* 1. 根据`entitySetName`从`state`中获取到对应的表
* 2. 将当前从响应中获取的实体一一合并到表中
*
* 通过这种方式,可以将后端的接口与Normalized Store中的实体信息建立关联,保持所有实体更新在实体表中,其它地方通过id的方式引用保持信息同步
*
* 如果一个响应同时返回多个实体,也同样可以调用多次`withTableUpdate`来进行包装:
*
* ```javascript
* import {property, keyBy} from 'lodash';
*
* const withUsersTableUpdate = withTableUpdate('usersByName', res => keyBy(res.users, 'username'));
* const withCommitsTableUpdate = withTableUpdate('commitsById', res => keyBy(res.commits, 'id'));
* const apiWithTableUpdate = withUsersTableUpdate(withCommitsTableUpdate(api));
* ```
*
* @param {Object} store Redux store
*/
export const createTableUpdater = ({dispatch}) => (entitySetName, selectEntities) => {
const dispatchTableUpdate = responseData => {
const entities = selectEntities(responseData);
dispatch({type: UPDATE_ENTITY_TABLE, payload: {entitySetName, entities}});
return responseData;
};
return fetchFunction => (...args) => fetchFunction(...args).then(dispatchTableUpdate);
};
/**
* 与`createTableUpdater`合作使用的reducer函数,具体参考上面的注释说明
*
* @param {Object} [state] 当前的状态
* @param {Object} [action] 收到的Action
*/
export const reducer = (state = {}, action) => {
if (action.type !== UPDATE_ENTITY_TABLE) {
return state;
}
const {payload: {entitySetName, entities}} = action;
const entitySet = state[entitySetName] || {};
const mergedEntitySet = reduce(
entities,
(result, value, key) => {
// 因为当前的系统前后端接口并不一定会返回一个完整的实体,
// 如果之前有一个比较完整的实体已经在`state`中,后来又来了一个不完整的实体,直接覆盖会导致字段丢失,
// 因此针对每个实体,使用的是浅合并的策略,保证第一级的字段是不会丢的
const currentEntity = result[key];
return {
...result,
[key]: {...currentEntity, ...value}
};
},
entitySet
);
const newState = {
...state,
[entitySetName]: mergedEntitySet
};
return newState;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment