跳到主要内容

实现

useRequest/index.js


import useRequest from "./src/useRequest";

export default useRequest;

useRequest/useRequest.js


import useRequestImplement from "./useRequestImplement";
import useLoadingDelayPlugin from "./plugins/useLoadingDelayPlugin";
import usePollingPlugin from "./plugins/usePollingPlugin";
import useAutoPlugin from "./plugins/useAutoPlugin";
import useRefreshOnWindowFocusPlugin from "./plugins/useRefreshOnWindowFocusPlugin";
import useDebouncePlugin from "./plugins/useDebouncePlugin";
import useThrottlePlugin from "./plugins/useThrottlePlugin";
import useRetryPlugin from "./plugins/useRetryPlugin";

function useRequest(service, options, plugins = []) {
return useRequestImplement(service, options, [
...plugins,
usePollingPlugin,
useLoadingDelayPlugin,
useAutoPlugin,
useRefreshOnWindowFocusPlugin,
useDebouncePlugin,
useThrottlePlugin,
useRetryPlugin
]);
}

export default useRequest;

useRequest/Fetch.js


class Fetch {
constructor(serviceRef, options, subscribe, initialState = {}) {
this.serviceRef = serviceRef;
this.options = options;
this.subscribe = subscribe;
this.state = {
data: undefined,
loading: !options.manual,
error: undefined,
...initialState,
};
this.count = 0;
}
setState(s = {}) {
this.state = { ...this.state, ...s };
this.subscribe();
}
run(...params) {
this.runAsync(...params).catch((error) => {
if (this.options.onError) {
console.error(error);
}
});
}
async runAsync(...params) {
this.count += 1;
const currentCount = this.count;
const { stopNow, ...state } = this.runPluginHandler("onBefore", params);
if (stopNow) {
return new Promise(() => {});
}
this.setState({ loading: true, params, ...state });
this.options.onBefore?.(params);
try {
let { servicePromise } = this.runPluginHandler(
"onRequest",
this.serviceRef.current,
params
);
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
const response = await servicePromise;
if (currentCount !== this.count) {
return new Promise(() => {});
}
this.options.onSuccess?.(response, params);
this.runPluginHandler("onSuccess", response, params);
this.options.onFinally?.(response, params);
if (currentCount === this.count) {
this.runPluginHandler("onFinally", params, response, undefined);
}
this.setState({
loading: false,
data: response,
error: undefined,
params,
});
} catch (error) {
this.setState({ loading: false, data: undefined, error, params });
if (currentCount !== this.count) {
return new Promise(() => {});
}
this.options.onError?.(error, params);
this.runPluginHandler("onError", error, params);
this.options.onFinally?.(error, params);
if (currentCount === this.count) {
this.runPluginHandler("onFinally", params, undefined, error);
}
throw error;
}
}
refresh() {
this.run(this.state.params || []);
}
refreshAsync() {
this.runAsync(this.state.params || []);
}
mutate(data) {
this.runPluginHandler("onMutate", data);
let targetData;
targetData = data;
this.setState({ data: targetData });
}

/**
* @description: 取消请求
*
* 可以取消当前正在进行的请求, 同时 useRequest 会在以下时机自动取消当前请求:
*
* 1. 组件卸载时, 取消正在进行的请求
* 2. 竟态取消, 当上一次请求还没返回时, 又发起了下一次请求, 则会取消上一次请求
*/
cancel() {
this.count += 1;
this.setState({ loading: false });
this.options.onCancel?.();
this.runPluginHandler("onCancel");
}
runPluginHandler(event, ...rest) {
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
}

export default Fetch;

useRequest/useRequestImplement.js


import Fetch from "./Fetch";
import useMount from "../../useMount";
import useUnmount from "../../useUnmount";
import useLatest from "../../useLatest";
import useUpdate from "../../useUpdate";
import useCreation from "../../useCreation";
import useMemoizedFn from "../../useMemoizedFn";

function useRequestImplement(service, options, plugins) {
const { manual, ...rest } = options;
const fetchOptions = {
manual,
...rest,
};
const serviceRef = useLatest(service);
const update = useUpdate();
const fetchInstance = useCreation(() => {
const initStates = plugins
.map((plugin) => plugin.onInit?.(fetchOptions))
.filter(Boolean);
return new Fetch(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initStates)
);
}, []);

fetchInstance.options = fetchOptions;
fetchInstance.pluginImpls = plugins.map((plugin) =>
plugin(fetchInstance, fetchOptions)
);

useMount(() => {
if (!manual) {
const params = fetchInstance.state.params || options.defaultParams || [];
fetchInstance.run(...params);
}
});

useUnmount(() => {
fetchInstance.cancel();
});

return {
data: fetchInstance.state.data,
loading: fetchInstance.state.loading,
error: fetchInstance.state.error,
run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
};
}

export default useRequestImplement;

useRequest/plugins/useAutoPlugin.js


import { useRef } from "react";
import useUpdateEffect from "../../../useUpdateEffect";

function useAutoPlugin(fetchInstance, options) {
const {
ready = true,
manual,
defaultParams = [],
refreshDeps = [],
refreshDepsAction,
} = options;

const hasAutoRun = useRef(false);
hasAutoRun.current = false;

useUpdateEffect(() => {
if (!manual && ready) {
hasAutoRun.current = true;
fetchInstance.run(...defaultParams);
}
}, [ready]);

useUpdateEffect(() => {
if (hasAutoRun.current) {
return;
}
if (!manual) {
hasAutoRun.current = true;
if (refreshDepsAction) {
refreshDepsAction();
} else {
fetchInstance.refresh();
}
}
}, [...refreshDeps]);

return {
onBefore() {
if (!ready) {
return { stopNow: true };
}
},
};
}

useAutoPlugin.onInit = (options) => {
const { ready = true, manual = false } = options;
return {
loading: !manual && ready,
};
};

export default useAutoPlugin;

useRequest/plugins/useDebouncePlugin.js


import { useEffect, useMemo, useRef } from "react";
import { debounce } from "lodash";

function useDebouncePlugin(fetchInstance, options) {
const { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait } =
options;
const debounceRef = useRef();
const debounceOptions = useMemo(() => {
const obj = {};
if (debounceLeading !== undefined) {
obj.leading = debounceLeading;
}
if (debounceTrailing !== undefined) {
obj.trailing = debounceTrailing;
}
if (debounceMaxWait !== undefined) {
obj.maxWait = debounceMaxWait;
}
return obj;
}, [debounceLeading, debounceTrailing, debounceMaxWait]);

useEffect(() => {
if (debounceWait) {
const originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
debounceRef.current = debounce(
(callback) => callback(),
debounceWait,
debounceOptions
);
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
debounceRef.current?.(() =>
originRunAsync(...args).then(resolve, reject)
);
});
};
}
}, [debounceWait, debounceLeading, debounceTrailing]);

return {};
}

export default useDebouncePlugin;

useRequest/plugins/useLoadingDelayPlugin.js


import { useRef } from "react";

function useLoadingDelayPlugin(fetchInstance, options) {
const { loadingDelay } = options;
const timeRef = useRef();
if (!loadingDelay) {
return {};
}

const cancelTimeout = () => {
if (timeRef.current) {
clearTimeout(timeRef.current);
}
};

return {
onBefore() {
timeRef.current = setTimeout(() => {
fetchInstance.setState({ loading: true });
}, loadingDelay);
return { loading: false };
},
onFinally() {
cancelTimeout();
},
onCancel() {
cancelTimeout();
},
};
}

useLoadingDelayPlugin.onInit = (options) => {
return {
...options,
};
};

export default useLoadingDelayPlugin;

useRequest/plugins/usePollingPlugin.js


import { useRef } from "react";
import useUpdateEffect from "../../../useUpdateEffect";
import isDocumentVisible from "../../../utils/isDocumentVisible";
import subscribeReVisible from "../../../utils/subscribeReVisible";

function usePollingPlugin(fetchInstance, options) {
const { pollingInterval, pollingWhenHidden } = options;
if (!pollingInterval) {
return {};
}

const timerRef = useRef();
const unsubscribeRef = useRef();

const stopPolling = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
unsubscribeRef.current?.();
}
};

useUpdateEffect(() => {
if (!pollingInterval) {
stopPolling();
}
}, [pollingInterval]);

return {
onBefore() {
stopPolling();
},
onFinally() {
if (!pollingWhenHidden && !isDocumentVisible()) {
unsubscribeRef.current = subscribeReVisible(() =>
fetchInstance.refresh()
);
return;
}
timerRef.current = setTimeout(() => {
fetchInstance.refresh();
}, pollingInterval);
},
onCancel() {
stopPolling();
},
};
}

usePollingPlugin.onInit = (options) => {
return {
...options,
};
};

export default usePollingPlugin;

useRequest/plugins/useRefreshOnWindowFocusPlugin.js


import { useEffect, useRef } from "react";
import limit from "../../../utils/limit";
import useUnMount from "../../../useUnMount";
import subscribeFocus from "../../../utils/subscribeFocus";

function useRefreshOnWindowFocusPlugin(fetchInstance, options) {
const { refreshOnWindowFocus, focusTimespan } = options;
const unsubscribeRef = useRef();
const stopSubscribe = () => {
unsubscribeRef.current?.();
};

useEffect(() => {
if (refreshOnWindowFocus) {
const limitRefresh = limit(
fetchInstance.refresh.bind(fetchInstance),
focusTimespan
);
unsubscribeRef.current = subscribeFocus(() => limitRefresh());
}
return () => {
stopSubscribe();
};
}, [refreshOnWindowFocus, focusTimespan]);

useUnMount(() => {
stopSubscribe();
});

return {};
}

export default useRefreshOnWindowFocusPlugin;

useRequest/plugins/useRetryPlugin.js


import { useRef } from "react";

function useRetryPlugin(fetchInstance, options) {
const { retryCount, retryInterval } = options;
const timerRef = useRef();
const countRef = useRef();
const triggerByRetry = useRef(false);

if (!retryCount) {
return {};
}

return {
onBefore() {
if (!triggerByRetry.current) {
countRef.current = 0;
}
triggerByRetry.current = false;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
},
onSuccess() {
countRef.current = 0;
},
onError() {
countRef.current += 1;
if (retryCount === -1 || countRef.current <= retryCount) {
const timeout =
retryInterval || Math.min(30000, 1000 * 2 * countRef.current);
timerRef.current = setTimeout(() => {
triggerByRetry.current = true;
fetchInstance.refresh();
}, timeout);
} else {
countRef.current = 0;
}
},
onCancel() {
countRef.current = 0;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
},
};
}

export default useRetryPlugin;

useRequest/plugins/useThrottlePlugin.js


import { useEffect, useRef, useMemo } from "react";
import { throttle } from "lodash";

function useThrottlePlugin(fetchInstance, options) {
const { throttleWait, throttleLeading, throttleTrailing } = options;
const throttleRef = useRef();
const throttleOptions = useMemo(() => {
const obj = {};
if (throttleLeading !== undefined) {
obj.leading = throttleLeading;
}
if (throttleTrailing !== undefined) {
obj.trailing = throttleTrailing;
}
}, [throttleLeading, throttleTrailing]);

useEffect(() => {
const originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
throttleRef.current = throttle(
(callback) => callback(),
throttleWait,
throttleOptions
);
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
throttleRef.current?.(() =>
originRunAsync(...args).then(resolve, reject)
);
});
};
}, [throttleWait, throttleLeading, throttleTrailing]);

return {};
}

export default useThrottlePlugin;