refactor(hooks): useActionQuery/useActionMutation 接入 QueryClient + 向后兼容
- useActionQuery 新增 queryKey 入参模式:传入 queryKey 走 useQuery 跨页共享缓存;不传回退旧 useEffect + useState 模式。Hook 内部始终声明 useQuery/useState/useEffect 以遵守 React Hooks 规则,通过 isCacheMode 切换启用状态 - useActionMutation 新增 mutationFn + actionId 模式:成功后按 CLIENT_INVALIDATION_MAP[actionId] 自动 invalidateQueries;mutate(action?) 参数可选 - toast 替换为 notify(行为等价,便于未来替换 toast 库) - 同步更新 004/005 架构文档签名说明
This commit is contained in:
@@ -691,8 +691,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
|||||||
|
|
||||||
| Hook | 文件 | 签名 | 用途 | 消费方 |
|
| Hook | 文件 | 签名 | 用途 | 消费方 |
|
||||||
|------|------|------|------|--------|
|
|------|------|------|------|--------|
|
||||||
| `useActionMutation` | `hooks/use-action-mutation.ts` | `useActionMutation<T>(options?): { isWorking, mutate }` | 通用 Server Action mutation Hook,替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式 | 1 个示范(P1-4: schools-view),潜在影响 50+ 文件 |
|
| `useActionMutation` | `hooks/use-action-mutation.ts` | `useActionMutation<T>(options?): { isWorking, mutate }`。Options 联合类型:旧版 `{ successMessage?, errorMessage?, onSuccess?, onError? }` 或新版 `{ mutationFn?, actionId?, params?, invalidateQueryKeys?, ... }`。新版接入 QueryClient,成功后按 `CLIENT_INVALIDATION_MAP[actionId]` 自动 invalidateQueries。`mutate(action?)` 参数可选:传 action 走旧模式,不传则使用 options.mutationFn | 通用 Server Action mutation Hook,替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式。V5 缓存策略重构新增 QueryClient + actionId 自动失效,向后兼容旧调用方 | 1 个示范(P1-4: schools-view),潜在影响 50+ 文件 |
|
||||||
| `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery<T>(action, options?): { data, loading, error, refetch }` | 通用 Server Action 查询 Hook,替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式,内置竞态防护 | 1 个示范(P1-4: create-question-dialog),潜在影响 11 个文件 |
|
| `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery<T>(action, options?): { data, loading, error, refetch }`。Options 联合类型:旧版 `{ deps?, enabled?, errorMessage? }` 或新版 `UseActionQueryWithCacheOptions`(含 `queryKey` + `UseQueryOptions`)。传入 `queryKey` 走 useQuery 跨页共享缓存;不传走旧 useEffect + useState。Hook 内部始终声明 useQuery + useState/useEffect 以遵守 React Hooks 规则 | 通用 Server Action 查询 Hook,替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式,内置竞态防护。V5 缓存策略重构新增 queryKey 入参走 QueryClient,向后兼容旧调用方 | 1 个示范(P1-4: create-question-dialog),潜在影响 11 个文件 |
|
||||||
|
|
||||||
**V5 状态管理统一专项基础设施**(阶段 1 新增,零业务行为变更):
|
**V5 状态管理统一专项基础设施**(阶段 1 新增,零业务行为变更):
|
||||||
|
|
||||||
|
|||||||
@@ -1495,8 +1495,8 @@
|
|||||||
{
|
{
|
||||||
"name": "useActionMutation",
|
"name": "useActionMutation",
|
||||||
"file": "hooks/use-action-mutation.ts",
|
"file": "hooks/use-action-mutation.ts",
|
||||||
"signature": "useActionMutation<T>(options?: { successMessage?, errorMessage?, onSuccess?, onError? }): { isWorking: boolean; mutate: (action: () => Promise<ActionState<T>>) => Promise<ActionState<T> | undefined> }",
|
"signature": "useActionMutation<T>(options?: UseActionMutationOptions<T>): { isWorking: boolean; mutate: (action?: () => Promise<ActionState<T>>) => Promise<ActionState<T> | undefined> }。Options 为联合类型:旧版 { successMessage?, errorMessage?, onSuccess?, onError? } 或新版 { mutationFn?, actionId?, params?, invalidateQueryKeys?, successMessage?, errorMessage?, onSuccess?, onError? }。新版接入 QueryClient,成功后按 CLIENT_INVALIDATION_MAP[actionId] 自动 invalidateQueries。mutate 参数可选:传 action 走旧模式,不传则使用 options.mutationFn",
|
||||||
"purpose": "通用 Server Action mutation Hook,P1-4 重构从 50+ 个文件中重复的 setIsWorking(true) + try/catch/finally + toast 模式抽取。支持 successMessage/errorMessage/onSuccess/onError 配置",
|
"purpose": "通用 Server Action mutation Hook,P1-4 重构从 50+ 个文件中重复的 setIsWorking(true) + try/catch/finally + toast 模式抽取。V5 缓存策略重构:接入 QueryClient + actionId 自动 invalidate,向后兼容旧调用方(mutate(action) 仍可用)",
|
||||||
"usedBy": [
|
"usedBy": [
|
||||||
"school/components/school-form-dialog.tsx",
|
"school/components/school-form-dialog.tsx",
|
||||||
"school/components/school-delete-dialog.tsx"
|
"school/components/school-delete-dialog.tsx"
|
||||||
@@ -1505,8 +1505,8 @@
|
|||||||
{
|
{
|
||||||
"name": "useActionQuery",
|
"name": "useActionQuery",
|
||||||
"file": "hooks/use-action-query.ts",
|
"file": "hooks/use-action-query.ts",
|
||||||
"signature": "useActionQuery<T>(action: () => Promise<ActionState<T>>, options?: { deps?, enabled?, errorMessage? }): { data, loading, error, refetch }",
|
"signature": "useActionQuery<T>(action: () => Promise<ActionState<T>>, options?: UseActionQueryOptions<T>): { data, loading, error, refetch }。Options 为联合类型:旧版 { deps?, enabled?, errorMessage? } 或新版 UseActionQueryWithCacheOptions(含 queryKey + UseQueryOptions)。传入 queryKey 走 useQuery 跨页共享缓存;不传走旧 useEffect + useState。Hook 内部始终声明 useQuery + useState/useEffect 以遵守 React Hooks 规则",
|
||||||
"purpose": "通用 Server Action 查询 Hook,P1-4 重构从 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式抽取。内置竞态防护(cancelled flag)",
|
"purpose": "通用 Server Action 查询 Hook,P1-4 重构从 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式抽取。V5 缓存策略重构:新增 queryKey 入参走 QueryClient,向后兼容旧调用方。内置竞态防护(requestId ref)",
|
||||||
"usedBy": [
|
"usedBy": [
|
||||||
"questions/components/create-question-dialog.tsx"
|
"questions/components/create-question-dialog.tsx"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { notify } from "@/shared/lib/notify"
|
||||||
|
import { CLIENT_INVALIDATION_MAP } from "@/shared/lib/cache/client-invalidation-map"
|
||||||
import type { ActionState } from "@/shared/types/action-state"
|
import type { ActionState } from "@/shared/types/action-state"
|
||||||
|
|
||||||
export interface UseActionMutationOptions<T> {
|
/** 旧版选项(向后兼容,无 mutationFn / actionId) */
|
||||||
|
export interface UseActionMutationLegacyOptions<T> {
|
||||||
/** 成功时显示的 toast 文案。不传则使用 result.message。传 false 则不显示。 */
|
/** 成功时显示的 toast 文案。不传则使用 result.message。传 false 则不显示。 */
|
||||||
successMessage?: string | false
|
successMessage?: string | false
|
||||||
/** 失败时显示的 toast 文案。不传则使用 result.message。传 false 则不显示。 */
|
/** 失败时显示的 toast 文案。不传则使用 result.message。传 false 则不显示。 */
|
||||||
@@ -15,51 +18,146 @@ export interface UseActionMutationOptions<T> {
|
|||||||
onError?: (error: unknown) => void
|
onError?: (error: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 新版选项(含 mutationFn + actionId 自动 invalidate) */
|
||||||
|
export interface UseActionMutationWithCacheOptions<T> {
|
||||||
|
/** mutation 函数(新模式必填) */
|
||||||
|
mutationFn?: () => Promise<ActionState<T>>
|
||||||
|
/** 关联的 INVALIDATION_MAP actionId,成功后自动失效相关 queryKey */
|
||||||
|
actionId?: string
|
||||||
|
/** 模板参数,如 { id: "cls_123" } */
|
||||||
|
params?: Record<string, string>
|
||||||
|
/** 显式额外失效的 queryKey(不通过 actionId 时使用) */
|
||||||
|
invalidateQueryKeys?: readonly (readonly (string | number | object)[])[]
|
||||||
|
/** 成功时显示的 toast 文案。不传则使用 result.message。传 false 则不显示。 */
|
||||||
|
successMessage?: string | false
|
||||||
|
/** 失败时显示的 toast 文案。不传则使用 result.message。传 false 则不显示。 */
|
||||||
|
errorMessage?: string | false
|
||||||
|
/** 成功回调 */
|
||||||
|
onSuccess?: (data: T | undefined) => void
|
||||||
|
/** 失败回调(result.success === false 或抛出异常时) */
|
||||||
|
onError?: (error: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseActionMutationOptions<T> =
|
||||||
|
| UseActionMutationLegacyOptions<T>
|
||||||
|
| UseActionMutationWithCacheOptions<T>
|
||||||
|
|
||||||
export interface UseActionMutationResult<T> {
|
export interface UseActionMutationResult<T> {
|
||||||
/** 是否正在执行(try/catch/finally 期间为 true) */
|
/** 是否正在执行 */
|
||||||
isWorking: boolean
|
isWorking: boolean
|
||||||
/** 执行 mutation。返回 ActionState 以便调用方进一步处理。 */
|
/** 执行 mutation。
|
||||||
mutate: (action: () => Promise<ActionState<T>>) => Promise<ActionState<T> | undefined>
|
* - 旧模式:传入 action 函数
|
||||||
|
* - 新模式(options.mutationFn 已设):可不传参数
|
||||||
|
*/
|
||||||
|
mutate: (
|
||||||
|
action?: () => Promise<ActionState<T>>,
|
||||||
|
) => Promise<ActionState<T> | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWithCacheOptions<T>(
|
||||||
|
opts: UseActionMutationOptions<T>,
|
||||||
|
): opts is UseActionMutationWithCacheOptions<T> {
|
||||||
|
return (
|
||||||
|
opts !== null &&
|
||||||
|
typeof opts === "object" &&
|
||||||
|
("mutationFn" in opts || "actionId" in opts || "invalidateQueryKeys" in opts)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 Server Action mutation Hook。
|
* 通用 Server Action mutation Hook。
|
||||||
*
|
*
|
||||||
* 用于替代各组件中重复的 `setIsWorking(true) + try/catch/finally + toast` 模式。
|
* 两种模式:
|
||||||
|
* - 旧模式:mutate(action),手动传入 action 函数,toast + onSuccess/onError
|
||||||
|
* - 新模式:options.mutationFn + actionId,自动 invalidateQueries
|
||||||
*
|
*
|
||||||
* @example
|
* @example 旧模式
|
||||||
* const { mutate, isWorking } = useActionMutation({
|
* const { mutate } = useActionMutation({ onSuccess: () => setOpen(false) })
|
||||||
* onSuccess: () => { setOpen(false); router.refresh() }
|
* const handleDelete = () => mutate(() => deleteAction(id))
|
||||||
|
*
|
||||||
|
* @example 新模式
|
||||||
|
* const { mutate } = useActionMutation({
|
||||||
|
* mutationFn: () => updateClassAction(input),
|
||||||
|
* actionId: "classes.update",
|
||||||
|
* params: { id: input.classId },
|
||||||
* })
|
* })
|
||||||
*
|
* const handleSubmit = () => mutate()
|
||||||
* const handleDelete = () => mutate(() => deleteSchoolAction(id))
|
|
||||||
*/
|
*/
|
||||||
export function useActionMutation<T = unknown>(
|
export function useActionMutation<T = unknown>(
|
||||||
options: UseActionMutationOptions<T> = {}
|
options: UseActionMutationOptions<T> = {},
|
||||||
): UseActionMutationResult<T> {
|
): UseActionMutationResult<T> {
|
||||||
const { successMessage, errorMessage, onSuccess, onError } = options
|
const queryClient = useQueryClient()
|
||||||
const [isWorking, setIsWorking] = useState(false)
|
const [isWorking, setIsWorking] = useState(false)
|
||||||
|
const {
|
||||||
|
successMessage,
|
||||||
|
errorMessage,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
} = options as UseActionMutationLegacyOptions<T>
|
||||||
|
|
||||||
|
const runInvalidations = useCallback(
|
||||||
|
async (opts: UseActionMutationWithCacheOptions<T>) => {
|
||||||
|
const keysToInvalidate: (string | number | object)[][] = []
|
||||||
|
if (opts.actionId) {
|
||||||
|
const rule = (
|
||||||
|
CLIENT_INVALIDATION_MAP as Record<
|
||||||
|
string,
|
||||||
|
{ queryKeys: readonly (readonly (string | number)[])[] }
|
||||||
|
>
|
||||||
|
)[opts.actionId]
|
||||||
|
if (rule) {
|
||||||
|
for (const qk of rule.queryKeys) {
|
||||||
|
keysToInvalidate.push([...qk])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opts.invalidateQueryKeys) {
|
||||||
|
for (const qk of opts.invalidateQueryKeys) {
|
||||||
|
keysToInvalidate.push([...qk])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
keysToInvalidate.map((qk) =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: qk }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[queryClient],
|
||||||
|
)
|
||||||
|
|
||||||
const mutate = useCallback(
|
const mutate = useCallback(
|
||||||
async (action: () => Promise<ActionState<T>>): Promise<ActionState<T> | undefined> => {
|
async (
|
||||||
|
action?: () => Promise<ActionState<T>>,
|
||||||
|
): Promise<ActionState<T> | undefined> => {
|
||||||
|
const cacheOpts = isWithCacheOptions(options) ? options : null
|
||||||
|
const actionToRun = action ?? cacheOpts?.mutationFn
|
||||||
|
if (!actionToRun) {
|
||||||
|
throw new Error(
|
||||||
|
"useActionMutation: must provide action to mutate() or set options.mutationFn",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
setIsWorking(true)
|
setIsWorking(true)
|
||||||
try {
|
try {
|
||||||
const result = await action()
|
const result = await actionToRun()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (successMessage !== false) {
|
if (successMessage !== false) {
|
||||||
toast.success(successMessage ?? result.message ?? "Operation succeeded")
|
notify.success(successMessage ?? result.message ?? "Operation succeeded")
|
||||||
|
}
|
||||||
|
if (cacheOpts) {
|
||||||
|
await runInvalidations(cacheOpts)
|
||||||
}
|
}
|
||||||
onSuccess?.(result.data)
|
onSuccess?.(result.data)
|
||||||
} else {
|
} else {
|
||||||
if (errorMessage !== false) {
|
if (errorMessage !== false) {
|
||||||
toast.error(errorMessage ?? result.message ?? "Operation failed")
|
notify.error(errorMessage ?? result.message ?? "Operation failed")
|
||||||
}
|
}
|
||||||
onError?.(new Error(result.message ?? "Action returned failure"))
|
onError?.(new Error(result.message ?? "Action returned failure"))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (errorMessage !== false) {
|
if (errorMessage !== false) {
|
||||||
toast.error(errorMessage ?? "Operation failed")
|
notify.error(errorMessage ?? "Operation failed")
|
||||||
}
|
}
|
||||||
onError?.(error)
|
onError?.(error)
|
||||||
return undefined
|
return undefined
|
||||||
@@ -67,7 +165,14 @@ export function useActionMutation<T = unknown>(
|
|||||||
setIsWorking(false)
|
setIsWorking(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[successMessage, errorMessage, onSuccess, onError]
|
[
|
||||||
|
options,
|
||||||
|
successMessage,
|
||||||
|
errorMessage,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
runInvalidations,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return { isWorking, mutate }
|
return { isWorking, mutate }
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { useQuery, type UseQueryOptions } from "@tanstack/react-query"
|
||||||
|
import { notify } from "@/shared/lib/notify"
|
||||||
import type { ActionState } from "@/shared/types/action-state"
|
import type { ActionState } from "@/shared/types/action-state"
|
||||||
|
|
||||||
export interface UseActionQueryOptions {
|
/** 旧版选项(向后兼容,无 queryKey) */
|
||||||
|
export interface UseActionQueryLegacyOptions {
|
||||||
/** 依赖数组,默认 []。当依赖变化时重新获取。 */
|
/** 依赖数组,默认 []。当依赖变化时重新获取。 */
|
||||||
deps?: ReadonlyArray<unknown>
|
deps?: ReadonlyArray<unknown>
|
||||||
/** 是否启用查询,默认 true。为 false 时不发起请求。 */
|
/** 是否启用查询,默认 true。为 false 时不发起请求。 */
|
||||||
@@ -13,6 +15,19 @@ export interface UseActionQueryOptions {
|
|||||||
errorMessage?: string | false
|
errorMessage?: string | false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 新版选项(传入 queryKey 走 QueryClient) */
|
||||||
|
export interface UseActionQueryWithCacheOptions<T>
|
||||||
|
extends Omit<UseQueryOptions<ActionState<T>>, "queryKey" | "queryFn"> {
|
||||||
|
/** queryKey 工厂返回的元组,如 queryKeys.classes.detail(id)。传入后走 useQuery,跨页共享缓存 */
|
||||||
|
queryKey: readonly (string | number | object)[]
|
||||||
|
/** 是否启用,默认 true */
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseActionQueryOptions<T> =
|
||||||
|
| UseActionQueryLegacyOptions
|
||||||
|
| UseActionQueryWithCacheOptions<T>
|
||||||
|
|
||||||
export interface UseActionQueryResult<T> {
|
export interface UseActionQueryResult<T> {
|
||||||
data: T | undefined
|
data: T | undefined
|
||||||
loading: boolean
|
loading: boolean
|
||||||
@@ -20,65 +35,126 @@ export interface UseActionQueryResult<T> {
|
|||||||
refetch: () => void
|
refetch: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWithCacheOptions<T>(
|
||||||
|
opts: UseActionQueryOptions<T>,
|
||||||
|
): opts is UseActionQueryWithCacheOptions<T> {
|
||||||
|
return opts !== null && typeof opts === "object" && "queryKey" in opts
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 Server Action 查询 Hook。
|
* 通用 Server Action 查询 Hook。
|
||||||
*
|
*
|
||||||
* 用于替代各组件中重复的 `useEffect + useState(loading) + Action().then().catch().finally()` 模式。
|
* 两种模式:
|
||||||
* 内置竞态防护(通过 ref 跟踪最新请求)。
|
* - 旧模式(不传 queryKey):useEffect + useState,请求内去重,不跨页共享
|
||||||
|
* - 新模式(传 queryKey):useQuery,跨页共享缓存,支持自动 invalidate
|
||||||
*
|
*
|
||||||
* @example
|
* 实现说明:为遵守 React Hooks 规则,useQuery 与 legacy state/effect 始终被声明,
|
||||||
* const { data, loading } = useActionQuery(
|
* 通过 `isCacheMode` 切换启用状态,二选一返回结果。
|
||||||
* () => getKnowledgePointOptionsAction(),
|
*
|
||||||
* { deps: [open], enabled: open }
|
* @example 旧模式
|
||||||
|
* const { data } = useActionQuery(() => getXAction(), { deps: [id] })
|
||||||
|
*
|
||||||
|
* @example 新模式
|
||||||
|
* const { data } = useActionQuery(
|
||||||
|
* () => getClassDetailAction({ classId }),
|
||||||
|
* { queryKey: queryKeys.classes.detail(classId) }
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
export function useActionQuery<T>(
|
export function useActionQuery<T>(
|
||||||
action: () => Promise<ActionState<T>>,
|
action: () => Promise<ActionState<T>>,
|
||||||
options: UseActionQueryOptions = {}
|
options: UseActionQueryOptions<T> = {},
|
||||||
): UseActionQueryResult<T> {
|
): UseActionQueryResult<T> {
|
||||||
const { deps = [], enabled = true, errorMessage = "Failed to load data" } = options
|
const isCacheMode = isWithCacheOptions(options)
|
||||||
|
const cacheOpts = (isCacheMode ? options : {}) as Partial<UseActionQueryWithCacheOptions<T>>
|
||||||
|
const legacyOpts = (isCacheMode ? {} : options) as UseActionQueryLegacyOptions
|
||||||
|
|
||||||
|
const { deps = [], enabled: legacyEnabled = true, errorMessage = "Failed to load data" } = legacyOpts
|
||||||
|
const { queryKey: cacheQueryKey, enabled: cacheEnabledFlag, ...restCache } = cacheOpts
|
||||||
|
|
||||||
|
// useQuery 始终声明;legacy 模式下 enabled:false 避免发起请求
|
||||||
|
const query = useQuery<ActionState<T>>({
|
||||||
|
...restCache,
|
||||||
|
queryKey: cacheQueryKey ?? ["__useActionQueryLegacy__"],
|
||||||
|
queryFn: action,
|
||||||
|
enabled: isCacheMode ? (cacheEnabledFlag ?? true) : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// legacy state 始终声明;cache 模式下 effect 直接 return
|
||||||
const [data, setData] = useState<T | undefined>(undefined)
|
const [data, setData] = useState<T | undefined>(undefined)
|
||||||
const [loading, setLoading] = useState<boolean>(enabled)
|
|
||||||
const [error, setError] = useState<Error | null>(null)
|
const [error, setError] = useState<Error | null>(null)
|
||||||
const [refetchCount, setRefetchCount] = useState(0)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const requestIdRef = useRef(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (isCacheMode || !legacyEnabled) return
|
||||||
|
const requestId = ++requestIdRef.current
|
||||||
let cancelled = false
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
action()
|
action()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (cancelled) return
|
if (requestId !== requestIdRef.current) return
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setData(result.data)
|
setData(result.data)
|
||||||
} else {
|
} else {
|
||||||
setError(new Error(result.message ?? "Action failed"))
|
setError(new Error(result.message ?? "Action failed"))
|
||||||
if (errorMessage !== false) {
|
if (errorMessage !== false) {
|
||||||
toast.error(errorMessage)
|
notify.error(errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
if (cancelled) return
|
if (requestId !== requestIdRef.current) return
|
||||||
setError(err instanceof Error ? err : new Error(String(err)))
|
setError(err instanceof Error ? err : new Error(String(err)))
|
||||||
if (errorMessage !== false) {
|
if (errorMessage !== false) {
|
||||||
toast.error(errorMessage)
|
notify.error(errorMessage)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setLoading(false)
|
if (requestId === requestIdRef.current) setLoading(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [...deps, refetchCount])
|
}, [isCacheMode, legacyEnabled, errorMessage, ...deps])
|
||||||
|
|
||||||
const refetch = () => setRefetchCount((c) => c + 1)
|
const legacyRefetch = () => {
|
||||||
|
if (isCacheMode) return
|
||||||
|
const requestId = ++requestIdRef.current
|
||||||
|
setLoading(true)
|
||||||
|
action()
|
||||||
|
.then((result) => {
|
||||||
|
if (requestId !== requestIdRef.current) return
|
||||||
|
if (result.success) {
|
||||||
|
setData(result.data)
|
||||||
|
} else {
|
||||||
|
setError(new Error(result.message ?? "Action failed"))
|
||||||
|
if (errorMessage !== false) notify.error(errorMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (requestId !== requestIdRef.current) return
|
||||||
|
setError(err instanceof Error ? err : new Error(String(err)))
|
||||||
|
if (errorMessage !== false) notify.error(errorMessage)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (requestId === requestIdRef.current) setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return { data, loading, error, refetch }
|
if (isCacheMode) {
|
||||||
|
const cacheData = query.data?.success ? query.data.data : undefined
|
||||||
|
const cacheError =
|
||||||
|
query.error ??
|
||||||
|
(query.data && !query.data.success
|
||||||
|
? new Error(query.data.message ?? "Action failed")
|
||||||
|
: null)
|
||||||
|
return {
|
||||||
|
data: cacheData,
|
||||||
|
loading: query.isLoading,
|
||||||
|
error: cacheError,
|
||||||
|
refetch: () => {
|
||||||
|
void query.refetch()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, loading, error, refetch: legacyRefetch }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user