Skip to content

Commit e5cdcc9

Browse files
committed
feat(packages): @sa/hooks add useRequest
1 parent 8795b2f commit e5cdcc9

22 files changed

+1077
-1
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck
3+
4+
import type { MutableRefObject } from 'react';
5+
import { isFunction } from './utils';
6+
import type { FetchState, Options, PluginReturn, Service, Subscribe } from './type';
7+
8+
export default class Fetch<TData, TParams extends any[]> {
9+
pluginImpls: PluginReturn<TData, TParams>[];
10+
11+
count: number = 0;
12+
13+
state: FetchState<TData, TParams> = {
14+
loading: false,
15+
params: undefined,
16+
data: undefined,
17+
error: undefined
18+
};
19+
20+
// eslint-disable-next-line max-params
21+
constructor(
22+
public serviceRef: MutableRefObject<Service<TData, TParams>>,
23+
public options: Options<TData, TParams>,
24+
public subscribe: Subscribe,
25+
public initState: Partial<FetchState<TData, TParams>> = {}
26+
) {
27+
this.state = {
28+
...this.state,
29+
loading: !options.manual,
30+
...initState
31+
};
32+
}
33+
34+
setState(s: Partial<FetchState<TData, TParams>> = {}) {
35+
this.state = {
36+
...this.state,
37+
...s
38+
};
39+
this.subscribe();
40+
}
41+
42+
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
43+
const r = this.pluginImpls.map(i => i[event]?.(...rest)).filter(Boolean);
44+
return Object.assign({}, ...r);
45+
}
46+
47+
async runAsync(...params: TParams): Promise<TData> {
48+
this.count += 1;
49+
const currentCount = this.count;
50+
51+
const { stopNow = false, returnNow = false, ...state } = this.runPluginHandler('onBefore', params);
52+
53+
// stop request
54+
if (stopNow) {
55+
return new Promise(() => {});
56+
}
57+
58+
this.setState({
59+
loading: true,
60+
params,
61+
...state
62+
});
63+
64+
// return now
65+
if (returnNow) {
66+
return Promise.resolve(state.data);
67+
}
68+
69+
this.options.onBefore?.(params);
70+
71+
try {
72+
// replace service
73+
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
74+
75+
if (!servicePromise) {
76+
servicePromise = this.serviceRef.current(...params);
77+
}
78+
79+
const res = await servicePromise;
80+
81+
if (currentCount !== this.count) {
82+
// prevent run.then when request is canceled
83+
return new Promise(() => {});
84+
}
85+
86+
// const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;
87+
88+
this.setState({
89+
data: res,
90+
error: undefined,
91+
loading: false
92+
});
93+
94+
this.options.onSuccess?.(res, params);
95+
this.runPluginHandler('onSuccess', res, params);
96+
97+
this.options.onFinally?.(params, res, undefined);
98+
99+
if (currentCount === this.count) {
100+
this.runPluginHandler('onFinally', params, res, undefined);
101+
}
102+
103+
return res;
104+
} catch (error) {
105+
if (currentCount !== this.count) {
106+
// prevent run.then when request is canceled
107+
return new Promise(() => {});
108+
}
109+
110+
this.setState({
111+
error,
112+
loading: false
113+
});
114+
115+
this.options.onError?.(error, params);
116+
this.runPluginHandler('onError', error, params);
117+
118+
this.options.onFinally?.(params, undefined, error);
119+
120+
if (currentCount === this.count) {
121+
this.runPluginHandler('onFinally', params, undefined, error);
122+
}
123+
124+
throw error;
125+
}
126+
}
127+
128+
run(...params: TParams) {
129+
this.runAsync(...params).catch(error => {
130+
if (!this.options.onError) {
131+
console.error(error);
132+
}
133+
});
134+
}
135+
136+
cancel() {
137+
this.count += 1;
138+
this.setState({
139+
loading: false
140+
});
141+
142+
this.runPluginHandler('onCancel');
143+
}
144+
145+
refresh() {
146+
this.run(...(this.state.params || []));
147+
}
148+
149+
refreshAsync() {
150+
return this.runAsync(...(this.state.params || []));
151+
}
152+
153+
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
154+
const targetData = isFunction(data) ? data(this.state.data) : data;
155+
this.runPluginHandler('onMutate', targetData);
156+
this.setState({
157+
data: targetData
158+
});
159+
}
160+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useRef } from 'react';
2+
import { useUpdateEffect } from 'ahooks';
3+
import type { Plugin } from '../type';
4+
5+
// support refreshDeps & ready
6+
const useAutoRunPlugin: Plugin<any, any[]> = (
7+
fetchInstance,
8+
{ manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction }
9+
) => {
10+
const hasAutoRun = useRef(false);
11+
hasAutoRun.current = false;
12+
13+
useUpdateEffect(() => {
14+
if (!manual && ready) {
15+
hasAutoRun.current = true;
16+
fetchInstance.run(...defaultParams);
17+
}
18+
}, [ready]);
19+
20+
useUpdateEffect(() => {
21+
if (hasAutoRun.current) {
22+
return;
23+
}
24+
if (!manual) {
25+
hasAutoRun.current = true;
26+
if (refreshDepsAction) {
27+
refreshDepsAction();
28+
} else {
29+
fetchInstance.refresh();
30+
}
31+
}
32+
}, [...refreshDeps]);
33+
34+
return {
35+
onBefore: () => {
36+
if (!ready) {
37+
return {
38+
stopNow: true
39+
};
40+
}
41+
return null;
42+
}
43+
};
44+
};
45+
46+
useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
47+
return {
48+
loading: !manual && ready
49+
};
50+
};
51+
52+
export default useAutoRunPlugin;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { useRef } from 'react';
2+
import { useCreation, useUnmount } from 'ahooks';
3+
import type { Plugin } from '../type';
4+
import { getCache, setCache } from '../utils/cache';
5+
import type { CachedData } from '../utils/cache';
6+
import { getCachePromise, setCachePromise } from '../utils/cachePromise';
7+
import { subscribe, trigger } from '../utils/cacheSubscribe';
8+
9+
const useCachePlugin: Plugin<any, any[]> = (
10+
fetchInstance,
11+
{ cacheKey, cacheTime = 5 * 60 * 1000, staleTime = 0, setCache: customSetCache, getCache: customGetCache }
12+
) => {
13+
const unSubscribeRef = useRef<() => void>();
14+
15+
const currentPromiseRef = useRef<Promise<any>>();
16+
17+
const _setCache = (key: string, cachedData: CachedData) => {
18+
if (customSetCache) {
19+
customSetCache(cachedData);
20+
} else {
21+
setCache(key, cacheTime, cachedData);
22+
}
23+
trigger(key, cachedData.data);
24+
};
25+
26+
const _getCache = (key: string, params: any[] = []) => {
27+
if (customGetCache) {
28+
return customGetCache(params);
29+
}
30+
return getCache(key);
31+
};
32+
33+
useCreation(() => {
34+
if (!cacheKey) {
35+
return;
36+
}
37+
38+
// get data from cache when init
39+
const cacheData = _getCache(cacheKey);
40+
if (cacheData && Object.hasOwn(cacheData, 'data')) {
41+
fetchInstance.state.data = cacheData.data;
42+
fetchInstance.state.params = cacheData.params;
43+
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
44+
fetchInstance.state.loading = false;
45+
}
46+
}
47+
48+
// subscribe same cachekey update, trigger update
49+
unSubscribeRef.current = subscribe(cacheKey, data => {
50+
fetchInstance.setState({ data });
51+
});
52+
}, []);
53+
54+
useUnmount(() => {
55+
unSubscribeRef.current?.();
56+
});
57+
58+
if (!cacheKey) {
59+
return {};
60+
}
61+
62+
return {
63+
onBefore: params => {
64+
const cacheData = _getCache(cacheKey, params);
65+
66+
if (!cacheData || !Object.hasOwn(cacheData, 'data')) {
67+
return {};
68+
}
69+
70+
// If the data is fresh, stop request
71+
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
72+
return {
73+
loading: false,
74+
data: cacheData?.data,
75+
error: undefined,
76+
returnNow: true
77+
};
78+
}
79+
// If the data is stale, return data, and request continue
80+
return {
81+
data: cacheData?.data,
82+
error: undefined
83+
};
84+
},
85+
onRequest: (service, args) => {
86+
let servicePromise = getCachePromise(cacheKey);
87+
88+
// If has servicePromise, and is not trigger by self, then use it
89+
if (servicePromise && servicePromise !== currentPromiseRef.current) {
90+
return { servicePromise };
91+
}
92+
93+
servicePromise = service(...args);
94+
currentPromiseRef.current = servicePromise;
95+
setCachePromise(cacheKey, servicePromise);
96+
return { servicePromise };
97+
},
98+
onSuccess: (data, params) => {
99+
if (cacheKey) {
100+
// cancel subscribe, avoid trgger self
101+
unSubscribeRef.current?.();
102+
_setCache(cacheKey, {
103+
data,
104+
params,
105+
time: new Date().getTime()
106+
});
107+
// resubscribe
108+
unSubscribeRef.current = subscribe(cacheKey, d => {
109+
fetchInstance.setState({ data: d });
110+
});
111+
}
112+
},
113+
onMutate: data => {
114+
if (cacheKey) {
115+
// cancel subscribe, avoid trigger self
116+
unSubscribeRef.current?.();
117+
_setCache(cacheKey, {
118+
data,
119+
params: fetchInstance.state.params,
120+
time: new Date().getTime()
121+
});
122+
// resubscribe
123+
unSubscribeRef.current = subscribe(cacheKey, d => {
124+
fetchInstance.setState({ data: d });
125+
});
126+
}
127+
}
128+
};
129+
};
130+
131+
export default useCachePlugin;

0 commit comments

Comments
 (0)