You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

292 lines
5.5 KiB

1 month ago
/**
* Hook
*
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { AxiosError } from 'axios';
import type { RequestConfig } from '@/src/utils/network/api';
/**
*
*/
export interface RequestState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
/**
*
*/
export interface UseRequestOptions<T> extends RequestConfig {
/** 是否立即执行 */
immediate?: boolean;
/** 成功回调 */
onSuccess?: (data: T) => void;
/** 失败回调 */
onError?: (error: Error) => void;
/** 完成回调(无论成功失败) */
onFinally?: () => void;
/** 默认数据 */
defaultData?: T;
}
/**
* Hook
*
* @example
* ```tsx
* const { data, loading, error, run, refresh } = useRequest(
* () => request.get('/api/users'),
* { immediate: true }
* );
* ```
*/
export function useRequest<T = any>(
requestFn: () => Promise<T>,
options: UseRequestOptions<T> = {}
) {
const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options;
const [state, setState] = useState<RequestState<T>>({
data: defaultData,
loading: false,
error: null,
});
const requestRef = useRef(requestFn);
requestRef.current = requestFn;
const abortControllerRef = useRef<AbortController | null>(null);
/**
*
*/
const run = useCallback(
async (...args: any[]) => {
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
setState((prev) => ({
...prev,
loading: true,
error: null,
}));
try {
const data = await requestRef.current();
setState({
data,
loading: false,
error: null,
});
onSuccess?.(data);
return data;
} catch (error) {
const err = error as Error;
setState((prev) => ({
...prev,
loading: false,
error: err,
}));
onError?.(err);
throw error;
} finally {
onFinally?.();
}
},
[onSuccess, onError, onFinally]
);
/**
*
*/
const refresh = useCallback(() => {
return run();
}, [run]);
/**
*
*/
const reset = useCallback(() => {
setState({
data: defaultData,
loading: false,
error: null,
});
}, [defaultData]);
/**
*
*/
const cancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
}, []);
// 立即执行
useEffect(() => {
if (immediate) {
run();
}
// 组件卸载时取消请求
return () => {
cancel();
};
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
return {
...state,
run,
refresh,
reset,
cancel,
};
}
/**
* Hook
*
* @example
* ```tsx
* const { data, loading, loadMore, refresh, hasMore } = usePagination(
* (page, pageSize) => request.get('/api/users', { params: { page, pageSize } })
* );
* ```
*/
export function usePagination<T = any>(
requestFn: (
page: number,
pageSize: number
) => Promise<{
list: T[];
total: number;
hasMore: boolean;
}>,
options: {
pageSize?: number;
immediate?: boolean;
onSuccess?: (data: T[]) => void;
onError?: (error: Error) => void;
} = {}
) {
const { pageSize = 20, immediate = false, onSuccess, onError } = options;
const [state, setState] = useState({
data: [] as T[],
loading: false,
loadingMore: false,
error: null as Error | null,
page: 1,
total: 0,
hasMore: true,
});
/**
*
*/
const load = useCallback(
async (page: number, append = false) => {
setState((prev) => ({
...prev,
loading: !append,
loadingMore: append,
error: null,
}));
try {
const result = await requestFn(page, pageSize);
setState((prev) => ({
...prev,
data: append ? [...prev.data, ...result.list] : result.list,
loading: false,
loadingMore: false,
page,
total: result.total,
hasMore: result.hasMore,
}));
onSuccess?.(result.list);
return result;
} catch (error) {
const err = error as Error;
setState((prev) => ({
...prev,
loading: false,
loadingMore: false,
error: err,
}));
onError?.(err);
throw error;
}
},
[requestFn, pageSize, onSuccess, onError]
);
/**
*
*/
const loadMore = useCallback(async () => {
if (state.loadingMore || !state.hasMore) {
return;
}
return load(state.page + 1, true);
}, [state.loadingMore, state.hasMore, state.page, load]);
/**
*
*/
const refresh = useCallback(async () => {
return load(1, false);
}, [load]);
/**
*
*/
const reset = useCallback(() => {
setState({
data: [],
loading: false,
loadingMore: false,
error: null,
page: 1,
total: 0,
hasMore: true,
});
}, []);
// 立即执行
useEffect(() => {
if (immediate) {
load(1, false);
}
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
return {
...state,
loadMore,
refresh,
reset,
};
}