diff --git a/packages/react/spec/useAction.spec.tsx b/packages/react/spec/useAction.spec.tsx index 5286091cf..db94d071d 100644 --- a/packages/react/spec/useAction.spec.tsx +++ b/packages/react/spec/useAction.spec.tsx @@ -13,7 +13,7 @@ import { MockClientWrapper, createMockUrqlCient, mockUrqlClient } from "./testWr describe("useAction", () => { // these functions are typechecked but never run to avoid actually making API calls - const TestUseActionCanRunActionsWithVariables = () => { + const TestUseActionCanRunUpdateActionsWithVariables = () => { const [_, mutate] = useAction(relatedProductsApi.user.update); // can call with variables @@ -32,6 +32,22 @@ describe("useAction", () => { void mutate({ foo: "123" }); }; + const TestUseActionCanRunCreateActionsWithVariables = () => { + const [_, mutate] = useAction(relatedProductsApi.user.create); + + // can call with variables + void mutate({ user: { email: "foo@bar.com" } }); + + // can call with no model variables + void mutate({}); + + // can call with no variables at all + void mutate(); + + // @ts-expect-error can't call with variables that don't belong to the model + void mutate({ foo: "123" }); + }; + const TestUseActionCanRunWithoutModelApiIdentifier = () => { const [_, mutate] = useAction(relatedProductsApi.unambiguous.update); @@ -419,4 +435,42 @@ describe("useAction", () => { `[Error: Invalid arguments found in variables. Did you mean to use ({ ambiguous: { ... } })?]` ); }); + + test("can run a mutation which takes no variables without passing any", async () => { + const { result, rerender } = renderHook(() => useAction(relatedProductsApi.user.create), { + wrapper: MockClientWrapper(relatedProductsApi), + }); + + let mutationPromise: any; + act(() => { + mutationPromise = result.current[1](); + }); + + expect(mockUrqlClient.executeMutation).toBeCalledTimes(1); + + mockUrqlClient.executeMutation.pushResponse("createUser", { + data: { + updateUser: { + success: true, + user: { + id: "123", + email: "test@test.com", + }, + }, + }, + stale: false, + hasNext: false, + }); + + await act(async () => { + await mutationPromise; + }); + + const beforeObject = result.current[0]!; + expect(beforeObject).toBeTruthy(); + + rerender(); + + expect(result.current[0]).toBe(beforeObject); + }); }); diff --git a/packages/react/src/useAction.ts b/packages/react/src/useAction.ts index 67a5802c7..17af6cba5 100644 --- a/packages/react/src/useAction.ts +++ b/packages/react/src/useAction.ts @@ -1,7 +1,7 @@ import type { ActionFunction, DefaultSelection, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; import { actionOperation, capitalizeIdentifier, get, hydrateRecord } from "@gadgetinc/api-client-core"; import { useCallback, useContext, useMemo } from "react"; -import type { AnyVariables, UseMutationState } from "urql"; +import type { AnyVariables, OperationContext, UseMutationState } from "urql"; import { GadgetUrqlClientContext } from "./GadgetProvider.js"; import { useGadgetMutation } from "./useGadgetMutation.js"; import { useStructuralMemo } from "./useStructuralMemo.js"; @@ -75,7 +75,8 @@ export const useAction = < return [ transformedResult, useCallback( - async (variables, context) => { + async (variables: F["variablesType"], context?: Partial) => { + variables ??= {}; if (action.hasAmbiguousIdentifier) { if (Object.keys(variables).some((key) => !action.paramOnlyVariables?.includes(key) && key !== action.modelApiIdentifier)) { throw Error(`Invalid arguments found in variables. Did you mean to use ({ ${action.modelApiIdentifier}: { ... } })?`); diff --git a/packages/react/src/useBulkAction.ts b/packages/react/src/useBulkAction.ts index 92cf7c945..a1d5fc804 100644 --- a/packages/react/src/useBulkAction.ts +++ b/packages/react/src/useBulkAction.ts @@ -1,7 +1,7 @@ import type { BulkActionFunction, DefaultSelection, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; import { actionOperation, capitalizeIdentifier, get, hydrateRecordArray } from "@gadgetinc/api-client-core"; import { useCallback, useMemo } from "react"; -import type { UseMutationState } from "urql"; +import type { OperationContext, UseMutationState } from "urql"; import { useGadgetMutation } from "./useGadgetMutation.js"; import { useStructuralMemo } from "./useStructuralMemo.js"; import type { ActionHookResult, OptionsType } from "./utils.js"; @@ -71,7 +71,7 @@ export const useBulkAction = < return [ transformedResult, useCallback( - async (variables, context) => { + async (variables: F["variablesType"], context?: Partial) => { // Adding the model's additional typename ensures document cache will properly refresh, regardless of whether __typename was // selected (and sometimes we can't even select it, like delete actions!) const result = await runMutation(variables, { diff --git a/packages/react/src/useGlobalAction.ts b/packages/react/src/useGlobalAction.ts index 25aaf3c03..ffc506af7 100644 --- a/packages/react/src/useGlobalAction.ts +++ b/packages/react/src/useGlobalAction.ts @@ -1,7 +1,7 @@ import type { GlobalActionFunction } from "@gadgetinc/api-client-core"; import { get, globalActionOperation } from "@gadgetinc/api-client-core"; import { useCallback, useMemo } from "react"; -import type { UseMutationState } from "urql"; +import type { OperationContext, UseMutationState } from "urql"; import { useGadgetMutation } from "./useGadgetMutation.js"; import type { ActionHookResult } from "./utils.js"; import { ErrorWrapper } from "./utils.js"; @@ -41,7 +41,7 @@ export const useGlobalAction = >( return [ transformedResult, useCallback( - async (variables, context) => { + async (variables: F["variablesType"], context?: Partial) => { const result = await runMutation(variables, context); return processResult({ fetching: false, ...result }, action); }, diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts index 9fa3da9e4..20bfe6d2d 100644 --- a/packages/react/src/utils.ts +++ b/packages/react/src/utils.ts @@ -95,14 +95,26 @@ export interface ActionHookState; } +export type RequiredKeysOf = Exclude< + { + [Key in keyof BaseType]: BaseType extends Record ? Key : never; + }[keyof BaseType], + undefined +>; + /** * The return value of a `useAction`, `useGlobalAction`, `useBulkAction` etc hook. * Includes the data result object and a function for running the mutation. **/ -export declare type ActionHookResult = [ - ActionHookState, - (variables: Variables, context?: Partial) => Promise> -]; +export type ActionHookResult = RequiredKeysOf extends never + ? [ + ActionHookState, + (variables?: Variables, context?: Partial) => Promise> + ] + : [ + ActionHookState, + (variables: Variables, context?: Partial) => Promise> + ]; export const noProviderErrorMessage = `Could not find a client in the context of Provider. Please ensure you wrap the root component in a `;