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.
291 lines
5.5 KiB
291 lines
5.5 KiB
/** |
|
* 请求 Hook |
|
* 提供统一的请求状态管理 |
|
*/ |
|
|
|
import { useState, useCallback, useRef, useEffect } from 'react'; |
|
import { AxiosError } from 'axios'; |
|
import type { RequestConfig } from '@/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, |
|
}; |
|
}
|
|
|