Skip to content

Commit 7389fb9

Browse files
authoredJun 13, 2024··
fix(hooks): export useStateAction hook from /stateful-hooks path (#166)
1 parent b5a54f2 commit 7389fb9

File tree

9 files changed

+188
-172
lines changed

9 files changed

+188
-172
lines changed
 

‎apps/playground/src/app/(examples)/stateful-form/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ResultBox } from "@/app/_components/result-box";
44
import { StyledButton } from "@/app/_components/styled-button";
55
import { StyledHeading } from "@/app/_components/styled-heading";
66
import { StyledInput } from "@/app/_components/styled-input";
7-
import { useStateAction } from "next-safe-action/hooks";
7+
import { useStateAction } from "next-safe-action/stateful-hooks";
88
import { statefulFormAction } from "./stateful-form-action";
99

1010
export default function StatefulFormPage() {

‎packages/next-safe-action/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"exports": {
1313
".": "./dist/index.mjs",
1414
"./typeschema": "./dist/typeschema.mjs",
15-
"./hooks": "./dist/hooks.mjs"
15+
"./hooks": "./dist/hooks.mjs",
16+
"./stateful-hooks": "./dist/stateful-hooks.mjs"
1617
},
1718
"typesVersions": {
1819
"*": {
@@ -24,6 +25,9 @@
2425
],
2526
"hooks": [
2627
"./dist/hooks.d.mts"
28+
],
29+
"stateful-hooks": [
30+
"./dist/stateful-hooks.d.mts"
2731
]
2832
}
2933
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { InferIn, Schema } from "@typeschema/main";
2+
import * as React from "react";
3+
import {} from "react/experimental";
4+
import type {} from "zod";
5+
import type { HookActionStatus, HookCallbacks, HookResult } from "./hooks.types";
6+
7+
export const getActionStatus = <
8+
ServerError,
9+
S extends Schema | undefined,
10+
const BAS extends readonly Schema[],
11+
CVE,
12+
CBAVE,
13+
Data,
14+
>({
15+
isIdle,
16+
isExecuting,
17+
result,
18+
}: {
19+
isIdle: boolean;
20+
isExecuting: boolean;
21+
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
22+
}): HookActionStatus => {
23+
if (isIdle) {
24+
return "idle";
25+
} else if (isExecuting) {
26+
return "executing";
27+
} else if (
28+
typeof result.validationErrors !== "undefined" ||
29+
typeof result.bindArgsValidationErrors !== "undefined" ||
30+
typeof result.serverError !== "undefined" ||
31+
typeof result.fetchError !== "undefined"
32+
) {
33+
return "hasErrored";
34+
} else {
35+
return "hasSucceeded";
36+
}
37+
};
38+
39+
export const getActionShorthandStatusObject = (status: HookActionStatus) => {
40+
return {
41+
isIdle: status === "idle",
42+
isExecuting: status === "executing",
43+
hasSucceeded: status === "hasSucceeded",
44+
hasErrored: status === "hasErrored",
45+
};
46+
};
47+
48+
export const useActionCallbacks = <
49+
ServerError,
50+
S extends Schema | undefined,
51+
const BAS extends readonly Schema[],
52+
CVE,
53+
CBAVE,
54+
Data,
55+
>({
56+
result,
57+
input,
58+
status,
59+
cb,
60+
}: {
61+
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
62+
input: S extends Schema ? InferIn<S> : undefined;
63+
status: HookActionStatus;
64+
cb?: HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>;
65+
}) => {
66+
const onExecuteRef = React.useRef(cb?.onExecute);
67+
const onSuccessRef = React.useRef(cb?.onSuccess);
68+
const onErrorRef = React.useRef(cb?.onError);
69+
const onSettledRef = React.useRef(cb?.onSettled);
70+
71+
// Execute the callback when the action status changes.
72+
React.useEffect(() => {
73+
const onExecute = onExecuteRef.current;
74+
const onSuccess = onSuccessRef.current;
75+
const onError = onErrorRef.current;
76+
const onSettled = onSettledRef.current;
77+
78+
const executeCallbacks = async () => {
79+
switch (status) {
80+
case "executing":
81+
await Promise.resolve(onExecute?.({ input }));
82+
break;
83+
case "hasSucceeded":
84+
await Promise.resolve(onSuccess?.({ data: result?.data, input }));
85+
await Promise.resolve(onSettled?.({ result, input }));
86+
break;
87+
case "hasErrored":
88+
await Promise.resolve(onError?.({ error: result, input }));
89+
await Promise.resolve(onSettled?.({ result, input }));
90+
break;
91+
}
92+
};
93+
94+
executeCallbacks().catch(console.error);
95+
}, [status, result, input]);
96+
};

‎packages/next-safe-action/src/hooks.ts

+2-165
Original file line numberDiff line numberDiff line change
@@ -7,106 +7,10 @@ import * as React from "react";
77
import * as ReactDOM from "react-dom";
88
import {} from "react/experimental";
99
import type {} from "zod";
10-
import type {
11-
HookActionStatus,
12-
HookCallbacks,
13-
HookResult,
14-
HookSafeActionFn,
15-
HookSafeStateActionFn,
16-
} from "./hooks.types";
10+
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks } from "./hooks-utils";
11+
import type { HookCallbacks, HookResult, HookSafeActionFn } from "./hooks.types";
1712
import { isError } from "./utils";
1813

19-
const getActionStatus = <
20-
ServerError,
21-
S extends Schema | undefined,
22-
const BAS extends readonly Schema[],
23-
CVE,
24-
CBAVE,
25-
Data,
26-
>({
27-
isIdle,
28-
isExecuting,
29-
result,
30-
}: {
31-
isIdle: boolean;
32-
isExecuting: boolean;
33-
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
34-
}): HookActionStatus => {
35-
if (isIdle) {
36-
return "idle";
37-
} else if (isExecuting) {
38-
return "executing";
39-
} else if (
40-
typeof result.validationErrors !== "undefined" ||
41-
typeof result.bindArgsValidationErrors !== "undefined" ||
42-
typeof result.serverError !== "undefined" ||
43-
typeof result.fetchError !== "undefined"
44-
) {
45-
return "hasErrored";
46-
} else {
47-
return "hasSucceeded";
48-
}
49-
};
50-
51-
const getActionShorthandStatusObject = (status: HookActionStatus) => {
52-
return {
53-
isIdle: status === "idle",
54-
isExecuting: status === "executing",
55-
hasSucceeded: status === "hasSucceeded",
56-
hasErrored: status === "hasErrored",
57-
};
58-
};
59-
60-
const useActionCallbacks = <
61-
ServerError,
62-
S extends Schema | undefined,
63-
const BAS extends readonly Schema[],
64-
CVE,
65-
CBAVE,
66-
Data,
67-
>({
68-
result,
69-
input,
70-
status,
71-
cb,
72-
}: {
73-
result: HookResult<ServerError, S, BAS, CVE, CBAVE, Data>;
74-
input: S extends Schema ? InferIn<S> : undefined;
75-
status: HookActionStatus;
76-
cb?: HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>;
77-
}) => {
78-
const onExecuteRef = React.useRef(cb?.onExecute);
79-
const onSuccessRef = React.useRef(cb?.onSuccess);
80-
const onErrorRef = React.useRef(cb?.onError);
81-
const onSettledRef = React.useRef(cb?.onSettled);
82-
83-
// Execute the callback when the action status changes.
84-
React.useEffect(() => {
85-
const onExecute = onExecuteRef.current;
86-
const onSuccess = onSuccessRef.current;
87-
const onError = onErrorRef.current;
88-
const onSettled = onSettledRef.current;
89-
90-
const executeCallbacks = async () => {
91-
switch (status) {
92-
case "executing":
93-
await Promise.resolve(onExecute?.({ input }));
94-
break;
95-
case "hasSucceeded":
96-
await Promise.resolve(onSuccess?.({ data: result?.data, input }));
97-
await Promise.resolve(onSettled?.({ result, input }));
98-
break;
99-
case "hasErrored":
100-
await Promise.resolve(onError?.({ error: result, input }));
101-
await Promise.resolve(onSettled?.({ result, input }));
102-
break;
103-
}
104-
};
105-
106-
executeCallbacks().catch(console.error);
107-
}, [status, result, input]);
108-
};
109-
11014
// HOOKS
11115

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

348-
/**
349-
* 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).
350-
* @param safeActionFn The action function
351-
* @param utils Optional `initResult`, `permalink` and callbacks
352-
*
353-
* {@link https://next-safe-action.dev/docs/execution/hooks/usestateaction See docs for more information}
354-
*/
355-
export const useStateAction = <
356-
ServerError,
357-
S extends Schema | undefined,
358-
const BAS extends readonly Schema[],
359-
CVE,
360-
CBAVE,
361-
Data,
362-
>(
363-
safeActionFn: HookSafeStateActionFn<ServerError, S, BAS, CVE, CBAVE, Data>,
364-
utils?: {
365-
initResult?: Awaited<ReturnType<typeof safeActionFn>>;
366-
permalink?: string;
367-
} & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
368-
) => {
369-
const [result, dispatcher, isExecuting] = React.useActionState(
370-
safeActionFn,
371-
utils?.initResult ?? {},
372-
utils?.permalink
373-
);
374-
const [isIdle, setIsIdle] = React.useState(true);
375-
const [clientInput, setClientInput] = React.useState<S extends Schema ? InferIn<S> : void>();
376-
const status = getActionStatus<ServerError, S, BAS, CVE, CBAVE, Data>({
377-
isExecuting,
378-
result: result ?? {},
379-
isIdle,
380-
});
381-
382-
const execute = React.useCallback(
383-
(input: S extends Schema ? InferIn<S> : void) => {
384-
dispatcher(input as S extends Schema ? InferIn<S> : undefined);
385-
386-
ReactDOM.flushSync(() => {
387-
setIsIdle(false);
388-
setClientInput(input);
389-
});
390-
},
391-
[dispatcher]
392-
);
393-
394-
useActionCallbacks({
395-
result: result ?? {},
396-
input: clientInput as S extends Schema ? InferIn<S> : undefined,
397-
status,
398-
cb: {
399-
onExecute: utils?.onExecute,
400-
onSuccess: utils?.onSuccess,
401-
onError: utils?.onError,
402-
onSettled: utils?.onSettled,
403-
},
404-
});
405-
406-
return {
407-
execute,
408-
input: clientInput,
409-
result,
410-
status,
411-
...getActionShorthandStatusObject(status),
412-
};
413-
};
414-
415252
export type * from "./hooks.types";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
3+
import type { InferIn, Schema } from "@typeschema/main";
4+
import * as React from "react";
5+
import * as ReactDOM from "react-dom";
6+
import {} from "react/experimental";
7+
import type {} from "zod";
8+
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks } from "./hooks-utils";
9+
import type { HookCallbacks, HookSafeStateActionFn } from "./hooks.types";
10+
/**
11+
* 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).
12+
* @param safeActionFn The action function
13+
* @param utils Optional `initResult`, `permalink` and callbacks
14+
*
15+
* {@link https://next-safe-action.dev/docs/execution/hooks/usestateaction See docs for more information}
16+
*/
17+
export const useStateAction = <
18+
ServerError,
19+
S extends Schema | undefined,
20+
const BAS extends readonly Schema[],
21+
CVE,
22+
CBAVE,
23+
Data,
24+
>(
25+
safeActionFn: HookSafeStateActionFn<ServerError, S, BAS, CVE, CBAVE, Data>,
26+
utils?: {
27+
initResult?: Awaited<ReturnType<typeof safeActionFn>>;
28+
permalink?: string;
29+
} & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
30+
) => {
31+
const [result, dispatcher, isExecuting] = React.useActionState(
32+
safeActionFn,
33+
utils?.initResult ?? {},
34+
utils?.permalink
35+
);
36+
const [isIdle, setIsIdle] = React.useState(true);
37+
const [clientInput, setClientInput] = React.useState<S extends Schema ? InferIn<S> : void>();
38+
const status = getActionStatus<ServerError, S, BAS, CVE, CBAVE, Data>({
39+
isExecuting,
40+
result: result ?? {},
41+
isIdle,
42+
});
43+
44+
const execute = React.useCallback(
45+
(input: S extends Schema ? InferIn<S> : void) => {
46+
dispatcher(input as S extends Schema ? InferIn<S> : undefined);
47+
48+
ReactDOM.flushSync(() => {
49+
setIsIdle(false);
50+
setClientInput(input);
51+
});
52+
},
53+
[dispatcher]
54+
);
55+
56+
useActionCallbacks({
57+
result: result ?? {},
58+
input: clientInput as S extends Schema ? InferIn<S> : undefined,
59+
status,
60+
cb: {
61+
onExecute: utils?.onExecute,
62+
onSuccess: utils?.onSuccess,
63+
onError: utils?.onError,
64+
onSettled: utils?.onSettled,
65+
},
66+
});
67+
68+
return {
69+
execute,
70+
input: clientInput,
71+
result,
72+
status,
73+
...getActionShorthandStatusObject(status),
74+
};
75+
};

‎packages/next-safe-action/tsup.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineConfig } from "tsup";
22

33
export default defineConfig({
4-
entry: ["src/index.ts", "src/typeschema.ts", "src/hooks.ts"],
4+
entry: ["src/index.ts", "src/typeschema.ts", "src/hooks.ts", "src/stateful-hooks.ts"],
55
format: ["esm"],
66
clean: true,
77
splitting: false,

0 commit comments

Comments
 (0)
Please sign in to comment.