import { useCallback, useMemo } from "react"
import {
useQuery,
useMutation,
type UseQueryResult,
type UseMutationResult,
type QueryKey,
type QueryObserverOptions,
} from "@tanstack/react-query"
import {
Reactor,
BaseActor,
FunctionName,
TransformKey,
ReactorArgs,
ReactorReturnOk,
ReactorReturnErr,
FunctionType,
} from "@ic-reactor/core"
import { CallConfig } from "@icp-sdk/core/agent"
/**
* Configuration for useActorMethod hook.
* Extends react-query's QueryObserverOptions with custom reactor params.
*
* This is a unified hook that handles both query and mutation methods.
* Query-specific options (like refetchInterval) only apply to query methods.
* Mutation-specific options (like invalidateQueries) only apply to mutation methods.
*/
export interface UseActorMethodParameters<
A = BaseActor,
M extends FunctionName = FunctionName,
T extends TransformKey = "candid",
> extends Omit<
QueryObserverOptions<
ReactorReturnOk,
ReactorReturnErr,
ReactorReturnOk,
ReactorReturnOk,
QueryKey
>,
"queryKey" | "queryFn"
> {
/** The reactor instance to use for method calls */
reactor: Reactor
/** The method name to call on the canister */
functionName: M
/** Arguments to pass to the method (optional for parameterless methods) */
args?: ReactorArgs
/** Agent call configuration (effectiveCanisterId, etc.) */
callConfig?: CallConfig
/** Custom query key (auto-generated if not provided) */
queryKey?: QueryKey
/**
* Callback when the method call succeeds.
* Works for both query and mutation methods.
*/
onSuccess?: (data: ReactorReturnOk) => void
/**
* Callback when the method call fails.
* Works for both query and mutation methods.
*/
onError?: (error: ReactorReturnErr) => void
/**
* Query keys to invalidate after a successful mutation.
* Only applies to mutation methods (updates).
*/
invalidateQueries?: QueryKey[]
}
/**
* Configuration type for bound useActorMethod hook (reactor omitted).
* For use with createActorHooks.
*/
export type UseActorMethodConfig<
A = BaseActor,
M extends FunctionName = FunctionName,
T extends TransformKey = "candid",
> = Omit, "reactor">
/**
* Result type for useActorMethod hook.
* Provides a unified interface for both query and mutation methods.
*/
export interface UseActorMethodResult<
A = BaseActor,
M extends FunctionName = FunctionName,
T extends TransformKey = "candid",
> {
/** The returned data from the method call */
data: ReactorReturnOk | undefined
/** Whether the method is currently executing */
isLoading: boolean
/** Alias for isLoading - whether a mutation is pending */
isPending: boolean
/** Whether there was an error */
isError: boolean
/** Whether the method has successfully completed at least once */
isSuccess: boolean
/** The error if one occurred */
error: ReactorReturnErr | null
/** Whether this is a query method (true) or mutation method (false) */
isQuery: boolean
/** The function type (query, update, composite_query) */
functionType: FunctionType
/**
* Call the method with optional arguments.
* For queries: triggers a refetch
* For mutations: executes the mutation with the provided args
*/
call: (
args?: ReactorArgs
) => Promise | undefined>
/**
* Reset the state (clear data and error).
* For queries: removes the query from cache
* For mutations: resets the mutation state
*/
reset: () => void
/**
* For queries only: Refetch the query
*/
refetch: () => Promise | undefined>
// Expose underlying results for advanced use cases
/** The raw query result (only available for query methods) */
queryResult?: UseQueryResult<
ReactorReturnOk,
ReactorReturnErr
>
/** The raw mutation result (only available for mutation methods) */
mutationResult?: UseMutationResult<
ReactorReturnOk,
ReactorReturnErr,
ReactorArgs
>
}
/**
* A unified hook for calling canister methods that automatically handles
* both query and mutation methods based on the Candid interface.
*/
export function useActorMethod<
A = BaseActor,
M extends FunctionName = FunctionName,
T extends TransformKey = "candid",
>({
reactor,
functionName,
args,
callConfig,
queryKey: customQueryKey,
enabled = true,
onSuccess,
onError,
invalidateQueries,
...queryOptions
}: UseActorMethodParameters): UseActorMethodResult {
// Determine if this is a query method by checking the IDL
const isQuery = useMemo(() => {
if (!reactor) throw new Error("Reactor instance is required")
return reactor.isQueryMethod(functionName)
}, [reactor, functionName])
const functionType: FunctionType = isQuery ? "query" : "update"
// Generate query key
const queryKey = useMemo(() => {
if (customQueryKey) return customQueryKey
return reactor.generateQueryKey(
{
functionName,
args,
},
callConfig
)
}, [reactor, functionName, args, callConfig, customQueryKey])
// ============================================================================
// Query Implementation
// ============================================================================
const queryResult = useQuery<
ReactorReturnOk,
ReactorReturnErr
>(
{
queryKey,
queryFn: async () => {
try {
const result = await reactor.callMethod({
functionName,
args,
callConfig,
})
onSuccess?.(result)
return result
} catch (error) {
onError?.(error as ReactorReturnErr)
throw error
}
},
enabled: isQuery && enabled,
...queryOptions,
},
reactor.queryClient
)
// ============================================================================
// Mutation Implementation
// ============================================================================
const mutationResult = useMutation<
ReactorReturnOk,
ReactorReturnErr,
ReactorArgs
>(
{
mutationKey: queryKey,
mutationFn: async (mutationArgs) => {
const result = await reactor.callMethod({
functionName,
args: mutationArgs ?? args,
callConfig,
})
return result
},
onSuccess: (data) => {
onSuccess?.(data)
// Invalidate specified queries after successful mutation
if (invalidateQueries && invalidateQueries.length > 0) {
invalidateQueries.forEach((key) => {
reactor.queryClient.invalidateQueries({ queryKey: key })
})
}
},
onError: (error) => {
onError?.(error)
},
},
reactor.queryClient
)
// ============================================================================
// Unified Call Function
// ============================================================================
const call = useCallback(
async (
callArgs?: ReactorArgs
): Promise | undefined> => {
if (isQuery) {
// For queries, refetch with new args if provided
if (callArgs !== undefined) {
try {
const result = await reactor.queryClient.fetchQuery({
queryKey,
queryFn: () =>
reactor.callMethod({
functionName,
args: callArgs,
callConfig,
}),
staleTime: 0,
})
onSuccess?.(result)
return result
} catch (error) {
onError?.(error as ReactorReturnErr)
return undefined
}
}
// Otherwise just refetch
const { data } = await queryResult.refetch()
return data
} else {
// For mutations, execute with provided args
return mutationResult
.mutateAsync(callArgs as ReactorArgs)
.catch(() => undefined)
}
},
[
isQuery,
reactor,
functionName,
callConfig,
queryKey,
queryResult,
mutationResult,
onSuccess,
onError,
]
)
// ============================================================================
// Reset Function
// ============================================================================
const reset = useCallback(() => {
if (isQuery) {
reactor.queryClient.removeQueries({ queryKey })
} else {
mutationResult.reset()
}
}, [isQuery, reactor, queryKey, mutationResult])
// ============================================================================
// Refetch Function
// ============================================================================
const refetch = useCallback(async () => {
if (isQuery) {
const result = await queryResult.refetch()
return result.data
}
return undefined
}, [isQuery, queryResult])
// ============================================================================
// Return Unified Result
// ============================================================================
if (isQuery) {
return {
data: queryResult.data,
isLoading: queryResult.isLoading,
isPending: queryResult.isLoading,
isError: queryResult.isError,
isSuccess: queryResult.isSuccess,
error: queryResult.error,
isQuery: true,
functionType,
call,
reset,
refetch,
queryResult,
} as UseActorMethodResult
} else {
return {
data: mutationResult.data,
isLoading: mutationResult.isPending,
isPending: mutationResult.isPending,
isError: mutationResult.isError,
isSuccess: mutationResult.isSuccess,
error: mutationResult.error,
isQuery: false,
functionType,
call,
reset,
refetch,
mutationResult,
} as UseActorMethodResult
}
}
/**
* Creates a bound useMethod hook for a specific reactor instance.
*
* @example
* ```tsx
* const { useMethod } = createActorMethodHooks(reactor)
* ```
*/
export function createActorMethodHooks<
A = BaseActor,
T extends TransformKey = "candid",
>(reactor: Reactor) {
return {
/**
* Hook for calling methods on the bound reactor.
*/
useMethod: >(
config: Omit, "reactor">
) =>
useActorMethod({ ...config, reactor } as UseActorMethodParameters<
A,
M,
T
>),
}
}