chore: redux cache controller

pull/13/head
moonrailgun 4 years ago
parent 62ae641a6f
commit 0f81918954

@ -46,6 +46,11 @@ export {
} from './model/user';
// redux
export { useAppSelector, useAppDispatch } from './redux/hooks/useAppSelector';
export {
useCachedUserInfo,
useCachedUserInfoList,
} from './redux/hooks/useReduxCache';
export { userActions } from './redux/slices';
export { setupRedux } from './redux/setup';
export { createStore } from './redux/store';

@ -72,3 +72,17 @@ export async function searchUserWithUniqueName(
return data;
}
/**
*
* @param userId ID
*/
export async function fetchUserInfo(userId: string): Promise<UserBaseInfo> {
const { data } = await request.get('/api/user/getUserInfo', {
params: {
userId,
},
});
return data;
}

@ -16,6 +16,7 @@
"lodash": "^4.17.21",
"react-i18next": "^11.11.0",
"react-native-storage": "npm:@trpgengine/react-native-storage@^1.0.1",
"react-redux": "^7.2.4",
"redux": "^4.1.0",
"str2int": "^1.0.0",
"url-regex": "^5.0.0",

@ -0,0 +1,138 @@
import _get from 'lodash/get';
import _set from 'lodash/set';
import _isNil from 'lodash/isNil';
import { useEffect, useMemo, useRef } from 'react';
import { fetchUserInfo, UserBaseInfo } from '../../model/user';
import { cacheActions } from '../slices';
import type { CacheKey } from '../slices/cache';
import { useAppDispatch, useAppSelector } from './useAppSelector';
// 检查是否需要跳过处理
const isSkipId = (id: string) =>
_isNil(id) ||
id === '' ||
typeof id !== 'string' ||
id.toString().startsWith('_');
interface CacheHookOptions {
forceFetch?: boolean;
}
type GetCacheDataFn = (id: string) => Promise<unknown>;
function reduxHookCacheFactory<T>(
cacheScope: CacheKey,
getCacheData: GetCacheDataFn
) {
const isFetchingDataIdQueue: string[] = []; // 正在请求的id列表
return function hook(id: string, options?: CacheHookOptions): Partial<T> {
const data = useAppSelector<T>(
(state) => _get(state, ['cache', cacheScope, id]) as any
);
const dispatch = useAppDispatch();
const forceFetchRef = useRef(options?.forceFetch ?? false);
useEffect(() => {
if ((_isNil(data) || forceFetchRef.current === true) && !isSkipId(id)) {
// 如果没有数据或设置了强制重新获取 且 不是内置的UUID
// 从服务端获取缓存信息
if (isFetchingDataIdQueue.indexOf(id) === -1) {
// 没有正在获取缓存信息
console.log(`缓存[${cacheScope}: ${id}]不存在, 自动获取`);
getCacheData(id).then((data) => {
// 从列表中移除
const index = isFetchingDataIdQueue.indexOf(id);
if (index !== -1) {
isFetchingDataIdQueue.splice(index, 1);
}
forceFetchRef.current = false; // 不论怎么样都置为false 表示已经获取过了
dispatch(
cacheActions.setCache({
scope: cacheScope,
id,
data,
})
);
});
isFetchingDataIdQueue.push(id);
}
}
}, [data]);
return data ?? {};
};
}
export const useCachedUserInfo = reduxHookCacheFactory<UserBaseInfo>(
'user',
(userId) => fetchUserInfo(userId)
);
/**
* redux hooks
*
* @param cacheScope
* @param getCacheDispatch dispatch
*/
function reduxHookCacheListFactory<T>(
cacheScope: CacheKey,
getCacheData: GetCacheDataFn
) {
const isFetchingDataIdQueue: string[] = []; // 正在请求的UUID列表
return function hook<R extends Record<string, T> = Record<string, T>>(
idList: string[]
): R {
const cacheList = useAppSelector<R>(
(state) => _get(state, ['cache', cacheScope]) as any
);
const dispatch = useAppDispatch();
const resMap = useMemo<R>(() => {
const map = {} as R;
for (const id of idList) {
if (_isNil(cacheList[id]) && !isSkipId(id)) {
// 如果没有数据则请求数据
// 从服务端获取缓存信息
if (isFetchingDataIdQueue.indexOf(id) === -1) {
// 没有正在获取缓存信息
console.log(`缓存[${cacheScope}: ${id}]不存在, 自动获取`);
getCacheData(id).then((data) => {
// 从列表中移除
const index = isFetchingDataIdQueue.indexOf(id);
if (index !== -1) {
isFetchingDataIdQueue.splice(index, 1);
}
dispatch(
cacheActions.setCache({
scope: cacheScope,
id,
data,
})
);
});
isFetchingDataIdQueue.push(id);
}
continue;
}
// 加入返回的map中
_set(map, id, cacheList[id]);
}
return map;
}, [cacheList, idList.join(',')]);
return resMap;
};
}
export const useCachedUserInfoList = reduxHookCacheListFactory<UserBaseInfo>(
'user',
(userId) => fetchUserInfo(userId)
);

@ -0,0 +1,32 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { UserBaseInfo } from '../../model/user';
import _set from 'lodash/set';
interface CacheState {
user: Record<string, UserBaseInfo>;
}
export type CacheKey = keyof CacheState;
const initialState: CacheState = { user: {} };
const cacheSlice = createSlice({
name: 'cache',
initialState,
reducers: {
setCache(
state,
action: PayloadAction<{
scope: CacheKey;
id: string;
data: unknown;
}>
) {
const { scope, id, data } = action.payload;
_set(state, [scope, id], data);
},
},
});
export const cacheActions = cacheSlice.actions;
export const cacheReducer = cacheSlice.reducer;

@ -1,10 +1,13 @@
import { combineReducers } from '@reduxjs/toolkit';
import { cacheReducer } from './cache';
import { userReducer } from './user';
export const appReducer = combineReducers({
cache: cacheReducer,
user: userReducer,
});
export type AppState = ReturnType<typeof appReducer>;
export { cacheActions } from './cache';
export { userActions } from './user';

@ -1,7 +1,7 @@
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { PillTabPane, PillTabs } from '@/components/PillTabs';
import { AddFriend } from './AddFriend';
import { useAppSelector } from 'pawchat-shared';
/**
*

@ -1,5 +1,5 @@
import React from 'react';
import { useAppSelector } from '../../hooks/useAppSelector';
import { useAppSelector } from 'pawchat-shared';
import { Icon } from '@iconify/react';
import clsx, { ClassValue } from 'clsx';
import { Avatar } from '../../components/Avatar';

@ -7535,7 +7535,7 @@ react-is@^17.0.1:
react-redux@^7.2.4:
version "7.2.4"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
dependencies:
"@babel/runtime" "^7.12.1"

Loading…
Cancel
Save