import { useMemo, useCallback } from "react"
import {
useMutation,
UseMutationOptions,
UseMutationResult,
QueryKey,
} from "@tanstack/react-query"
import {
Reactor,
ReactorArgs,
ReactorReturnOk,
FunctionName,
TransformKey,
ReactorReturnErr,
isCanisterError,
CanisterError,
ErrResult,
ActorMethodReturnType,
TransformReturnRegistry,
} from "@ic-reactor/core"
import { CallConfig } from "@icp-sdk/core/agent"
export interface UseActorMutationParameters<
A,
M extends FunctionName,
T extends TransformKey = "candid",
> extends Omit<
UseMutationOptions<
ReactorReturnOk,
ReactorReturnErr,
ReactorArgs
>,
"mutationFn"
> {
reactor: Reactor
functionName: M
callConfig?: CallConfig
invalidateQueries?: QueryKey[]
/**
* Callback for canister-level business logic errors.
* Called when the canister returns a Result { Err: E } variant.
* Separate from `onError`, which fires for all errors including network failures.
*/
onCanisterError?: (
error: CanisterError<
TransformReturnRegistry>>[T]
>,
variables: ReactorArgs
) => void
}
export type UseActorMutationConfig<
A,
M extends FunctionName,
T extends TransformKey = "candid",
> = Omit, "reactor">
export type UseActorMutationResult<
A,
M extends FunctionName,
T extends TransformKey = "candid",
> = UseMutationResult<
ReactorReturnOk,
ReactorReturnErr,
ReactorArgs
>
/**
* Hook for executing mutation calls on a canister.
*
* @example
* const { mutate, isPending } = useActorMutation({
* reactor,
* functionName: "transfer",
* onSuccess: () => console.log("Success!"),
* onCanisterError: (err) => console.error("Canister Err:", err.code),
* })
*/
export const useActorMutation = <
A,
M extends FunctionName,
T extends TransformKey = "candid",
>({
reactor,
functionName,
invalidateQueries,
onSuccess,
onError,
onCanisterError,
callConfig,
...options
}: UseActorMutationParameters): UseActorMutationResult => {
const mutationFn = useCallback(
async (args: ReactorArgs) =>
reactor.callMethod({ functionName, callConfig, args }),
[reactor, functionName, callConfig]
)
const handleSuccess = useCallback(
async (
...params: Parameters<
NonNullable<
UseMutationOptions<
ReactorReturnOk,
ReactorReturnErr,
ReactorArgs
>["onSuccess"]
>
>
) => {
if (invalidateQueries) {
await Promise.all(
invalidateQueries.map((queryKey) =>
reactor.queryClient.invalidateQueries({ queryKey })
)
)
}
await onSuccess?.(...params)
},
[reactor, invalidateQueries, onSuccess]
)
const handleError = useCallback(
(
error: ReactorReturnErr,
variables: ReactorArgs,
context: unknown,
mutation: unknown
) => {
if (isCanisterError(error)) {
onCanisterError?.(error as any, variables)
}
onError?.(error, variables, context as any, mutation as any)
},
[onCanisterError, onError]
)
const mutationOptions = useMemo(
() => ({
...options,
mutationFn,
onSuccess: handleSuccess,
onError: handleError,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[mutationFn, handleSuccess, handleError]
)
return useMutation(mutationOptions, reactor.queryClient)
}