Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: TheEdoRan/next-safe-action
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v7.0.0
Choose a base ref
...
head repository: TheEdoRan/next-safe-action
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.0.1
Choose a head ref
  • 1 commit
  • 9 files changed
  • 1 contributor

Commits on Jun 13, 2024

  1. fix(hooks): export useStateAction hook from /stateful-hooks path (#…

    TheEdoRan authored Jun 13, 2024
    Copy the full SHA
    7389fb9 View commit details
2 changes: 1 addition & 1 deletion apps/playground/src/app/(examples)/stateful-form/page.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { ResultBox } from "@/app/_components/result-box";
import { StyledButton } from "@/app/_components/styled-button";
import { StyledHeading } from "@/app/_components/styled-heading";
import { StyledInput } from "@/app/_components/styled-input";
import { useStateAction } from "next-safe-action/hooks";
import { useStateAction } from "next-safe-action/stateful-hooks";
import { statefulFormAction } from "./stateful-form-action";

export default function StatefulFormPage() {
6 changes: 5 additions & 1 deletion packages/next-safe-action/package.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,8 @@
"exports": {
".": "./dist/index.mjs",
"./typeschema": "./dist/typeschema.mjs",
"./hooks": "./dist/hooks.mjs"
"./hooks": "./dist/hooks.mjs",
"./stateful-hooks": "./dist/stateful-hooks.mjs"
},
"typesVersions": {
"*": {
@@ -24,6 +25,9 @@
],
"hooks": [
"./dist/hooks.d.mts"
],
"stateful-hooks": [
"./dist/stateful-hooks.d.mts"
]
}
},
96 changes: 96 additions & 0 deletions packages/next-safe-action/src/hooks-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { InferIn, Schema } from "@typeschema/main";
import * as React from "react";
import {} from "react/experimental";
import type {} from "zod";
import type { HookActionStatus, HookCallbacks, HookResult } from "./hooks.types";

export const getActionStatus = <
ServerError,
S extends Schema | undefined,
const BAS extends readonly Schema[],
CVE,
CBAVE,
Data,
>({
isIdle,
isExecuting,
result,
}: {
isIdle: boolean;
isExecuting: boolean;
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
}): HookActionStatus => {
if (isIdle) {
return "idle";
} else if (isExecuting) {
return "executing";
} else if (
typeof result.validationErrors !== "undefined" ||
typeof result.bindArgsValidationErrors !== "undefined" ||
typeof result.serverError !== "undefined" ||
typeof result.fetchError !== "undefined"
) {
return "hasErrored";
} else {
return "hasSucceeded";
}
};

export const getActionShorthandStatusObject = (status: HookActionStatus) => {
return {
isIdle: status === "idle",
isExecuting: status === "executing",
hasSucceeded: status === "hasSucceeded",
hasErrored: status === "hasErrored",
};
};

export const useActionCallbacks = <
ServerError,
S extends Schema | undefined,
const BAS extends readonly Schema[],
CVE,
CBAVE,
Data,
>({
result,
input,
status,
cb,
}: {
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
input: S extends Schema ? InferIn<S> : undefined;
status: HookActionStatus;
cb?: HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>;
}) => {
const onExecuteRef = React.useRef(cb?.onExecute);
const onSuccessRef = React.useRef(cb?.onSuccess);
const onErrorRef = React.useRef(cb?.onError);
const onSettledRef = React.useRef(cb?.onSettled);

// Execute the callback when the action status changes.
React.useEffect(() => {
const onExecute = onExecuteRef.current;
const onSuccess = onSuccessRef.current;
const onError = onErrorRef.current;
const onSettled = onSettledRef.current;

const executeCallbacks = async () => {
switch (status) {
case "executing":
await Promise.resolve(onExecute?.({ input }));
break;
case "hasSucceeded":
await Promise.resolve(onSuccess?.({ data: result?.data, input }));
await Promise.resolve(onSettled?.({ result, input }));
break;
case "hasErrored":
await Promise.resolve(onError?.({ error: result, input }));
await Promise.resolve(onSettled?.({ result, input }));
break;
}
};

executeCallbacks().catch(console.error);
}, [status, result, input]);
};
167 changes: 2 additions & 165 deletions packages/next-safe-action/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -7,106 +7,10 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import {} from "react/experimental";
import type {} from "zod";
import type {
HookActionStatus,
HookCallbacks,
HookResult,
HookSafeActionFn,
HookSafeStateActionFn,
} from "./hooks.types";
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks } from "./hooks-utils";
import type { HookCallbacks, HookResult, HookSafeActionFn } from "./hooks.types";
import { isError } from "./utils";

const getActionStatus = <
ServerError,
S extends Schema | undefined,
const BAS extends readonly Schema[],
CVE,
CBAVE,
Data,
>({
isIdle,
isExecuting,
result,
}: {
isIdle: boolean;
isExecuting: boolean;
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
}): HookActionStatus => {
if (isIdle) {
return "idle";
} else if (isExecuting) {
return "executing";
} else if (
typeof result.validationErrors !== "undefined" ||
typeof result.bindArgsValidationErrors !== "undefined" ||
typeof result.serverError !== "undefined" ||
typeof result.fetchError !== "undefined"
) {
return "hasErrored";
} else {
return "hasSucceeded";
}
};

const getActionShorthandStatusObject = (status: HookActionStatus) => {
return {
isIdle: status === "idle",
isExecuting: status === "executing",
hasSucceeded: status === "hasSucceeded",
hasErrored: status === "hasErrored",
};
};

const useActionCallbacks = <
ServerError,
S extends Schema | undefined,
const BAS extends readonly Schema[],
CVE,
CBAVE,
Data,
>({
result,
input,
status,
cb,
}: {
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
input: S extends Schema ? InferIn<S> : undefined;
status: HookActionStatus;
cb?: HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>;
}) => {
const onExecuteRef = React.useRef(cb?.onExecute);
const onSuccessRef = React.useRef(cb?.onSuccess);
const onErrorRef = React.useRef(cb?.onError);
const onSettledRef = React.useRef(cb?.onSettled);

// Execute the callback when the action status changes.
React.useEffect(() => {
const onExecute = onExecuteRef.current;
const onSuccess = onSuccessRef.current;
const onError = onErrorRef.current;
const onSettled = onSettledRef.current;

const executeCallbacks = async () => {
switch (status) {
case "executing":
await Promise.resolve(onExecute?.({ input }));
break;
case "hasSucceeded":
await Promise.resolve(onSuccess?.({ data: result?.data, input }));
await Promise.resolve(onSettled?.({ result, input }));
break;
case "hasErrored":
await Promise.resolve(onError?.({ error: result, input }));
await Promise.resolve(onSettled?.({ result, input }));
break;
}
};

executeCallbacks().catch(console.error);
}, [status, result, input]);
};

// HOOKS

/**
@@ -345,71 +249,4 @@ export const useOptimisticAction = <
};
};

/**
* Use the stateful action from a Client Component via hook. Used for actions defined with [`stateAction`](https://next-safe-action.dev/docs/safe-action-client/instance-methods#action--stateaction).
* @param safeActionFn The action function
* @param utils Optional `initResult`, `permalink` and callbacks
*
* {@link https://next-safe-action.dev/docs/execution/hooks/usestateaction See docs for more information}
*/
export const useStateAction = <
ServerError,
S extends Schema | undefined,
const BAS extends readonly Schema[],
CVE,
CBAVE,
Data,
>(
safeActionFn: HookSafeStateActionFn<ServerError, S, BAS, CVE, CBAVE, Data>,
utils?: {
initResult?: Awaited<ReturnType<typeof safeActionFn>>;
permalink?: string;
} & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
) => {
const [result, dispatcher, isExecuting] = React.useActionState(
safeActionFn,
utils?.initResult ?? {},
utils?.permalink
);
const [isIdle, setIsIdle] = React.useState(true);
const [clientInput, setClientInput] = React.useState<S extends Schema ? InferIn<S> : void>();
const status = getActionStatus<ServerError, S, BAS, CVE, CBAVE, Data>({
isExecuting,
result: result ?? {},
isIdle,
});

const execute = React.useCallback(
(input: S extends Schema ? InferIn<S> : void) => {
dispatcher(input as S extends Schema ? InferIn<S> : undefined);

ReactDOM.flushSync(() => {
setIsIdle(false);
setClientInput(input);
});
},
[dispatcher]
);

useActionCallbacks({
result: result ?? {},
input: clientInput as S extends Schema ? InferIn<S> : undefined,
status,
cb: {
onExecute: utils?.onExecute,
onSuccess: utils?.onSuccess,
onError: utils?.onError,
onSettled: utils?.onSettled,
},
});

return {
execute,
input: clientInput,
result,
status,
...getActionShorthandStatusObject(status),
};
};

export type * from "./hooks.types";
75 changes: 75 additions & 0 deletions packages/next-safe-action/src/stateful-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import type { InferIn, Schema } from "@typeschema/main";
import * as React from "react";
import * as ReactDOM from "react-dom";
import {} from "react/experimental";
import type {} from "zod";
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks } from "./hooks-utils";
import type { HookCallbacks, HookSafeStateActionFn } from "./hooks.types";
/**
* Use the stateful action from a Client Component via hook. Used for actions defined with [`stateAction`](https://next-safe-action.dev/docs/safe-action-client/instance-methods#action--stateaction).
* @param safeActionFn The action function
* @param utils Optional `initResult`, `permalink` and callbacks
*
* {@link https://next-safe-action.dev/docs/execution/hooks/usestateaction See docs for more information}
*/
export const useStateAction = <
ServerError,
S extends Schema | undefined,
const BAS extends readonly Schema[],
CVE,
CBAVE,
Data,
>(
safeActionFn: HookSafeStateActionFn<ServerError, S, BAS, CVE, CBAVE, Data>,
utils?: {
initResult?: Awaited<ReturnType<typeof safeActionFn>>;
permalink?: string;
} & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
) => {
const [result, dispatcher, isExecuting] = React.useActionState(
safeActionFn,
utils?.initResult ?? {},
utils?.permalink
);
const [isIdle, setIsIdle] = React.useState(true);
const [clientInput, setClientInput] = React.useState<S extends Schema ? InferIn<S> : void>();
const status = getActionStatus<ServerError, S, BAS, CVE, CBAVE, Data>({
isExecuting,
result: result ?? {},
isIdle,
});

const execute = React.useCallback(
(input: S extends Schema ? InferIn<S> : void) => {
dispatcher(input as S extends Schema ? InferIn<S> : undefined);

ReactDOM.flushSync(() => {
setIsIdle(false);
setClientInput(input);
});
},
[dispatcher]
);

useActionCallbacks({
result: result ?? {},
input: clientInput as S extends Schema ? InferIn<S> : undefined,
status,
cb: {
onExecute: utils?.onExecute,
onSuccess: utils?.onSuccess,
onError: utils?.onError,
onSettled: utils?.onSettled,
},
});

return {
execute,
input: clientInput,
result,
status,
...getActionShorthandStatusObject(status),
};
};
2 changes: 1 addition & 1 deletion packages/next-safe-action/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts", "src/typeschema.ts", "src/hooks.ts"],
entry: ["src/index.ts", "src/typeschema.ts", "src/hooks.ts", "src/stateful-hooks.ts"],
format: ["esm"],
clean: true,
splitting: false,
Loading