Skip to content

Commit aa11577

Browse files
authoredApr 17, 2024··
feat: allow omitting schema argument in schema method (#102)
This PR adds the ability to omit passing a validation schema to the `schema` method.
1 parent fccd480 commit aa11577

File tree

11 files changed

+203
-66
lines changed

11 files changed

+203
-66
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use server";
2+
3+
import { action } from "@/lib/safe-action";
4+
5+
export const emptyAction = action
6+
.metadata({ actionName: "onboardUser" })
7+
.schema()
8+
.action(async () => {
9+
await new Promise((res) => setTimeout(res, 500));
10+
11+
return {
12+
message: "Well done!",
13+
};
14+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use client";
2+
3+
import { StyledButton } from "@/app/_components/styled-button";
4+
import { StyledHeading } from "@/app/_components/styled-heading";
5+
import { useAction } from "next-safe-action/hooks";
6+
import { ResultBox } from "../../_components/result-box";
7+
import { emptyAction } from "./empty-action";
8+
9+
export default function EmptySchema() {
10+
const { execute, result, status, reset } = useAction(emptyAction, {
11+
onSuccess({ data, input, reset }) {
12+
console.log("HELLO FROM ONSUCCESS", data, input);
13+
14+
// You can reset result object by calling `reset`.
15+
// reset();
16+
},
17+
onError({ error, input, reset }) {
18+
console.log("OH NO FROM ONERROR", error, input);
19+
20+
// You can reset result object by calling `reset`.
21+
// reset();
22+
},
23+
onSettled({ result, input, reset }) {
24+
console.log("HELLO FROM ONSETTLED", result, input);
25+
26+
// You can reset result object by calling `reset`.
27+
// reset();
28+
},
29+
onExecute({ input }) {
30+
console.log("HELLO FROM ONEXECUTE", input);
31+
},
32+
});
33+
34+
console.log("status:", status);
35+
36+
return (
37+
<main className="w-96 max-w-full px-4">
38+
<StyledHeading>Action without schema</StyledHeading>
39+
<form
40+
className="flex flex-col mt-8 space-y-4"
41+
onSubmit={(e) => {
42+
e.preventDefault();
43+
const formData = new FormData(e.currentTarget);
44+
45+
// Action call.
46+
execute();
47+
}}>
48+
<StyledButton type="submit">Execute action</StyledButton>
49+
<StyledButton type="button" onClick={reset}>
50+
Reset
51+
</StyledButton>
52+
</form>
53+
<ResultBox result={result} status={status} />
54+
</main>
55+
);
56+
}

‎packages/example-app/src/app/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default function Home() {
1515
<span className="font-mono">useOptimisticAction</span> hook
1616
</ExampleLink>
1717
<ExampleLink href="/bind-arguments">Bind arguments</ExampleLink>
18+
<ExampleLink href="/empty-schema">Empty schema</ExampleLink>
1819
<ExampleLink href="/server-form">Server Form</ExampleLink>
1920
<ExampleLink href="/client-form">Client Form</ExampleLink>
2021
<ExampleLink href="/react-hook-form">React Hook Form</ExampleLink>

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

+31-19
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const DEFAULT_RESULT = {
1919

2020
const getActionStatus = <
2121
const ServerError,
22-
const S extends Schema,
22+
const S extends Schema | undefined,
2323
const BAS extends readonly Schema[],
2424
const FVE,
2525
const FBAVE,
@@ -49,7 +49,7 @@ const getActionStatus = <
4949

5050
const useActionCallbacks = <
5151
const ServerError,
52-
const S extends Schema,
52+
const S extends Schema | undefined,
5353
const BAS extends readonly Schema[],
5454
const FVE,
5555
const FBAVE,
@@ -62,7 +62,7 @@ const useActionCallbacks = <
6262
cb,
6363
}: {
6464
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
65-
input: InferIn<S>;
65+
input: S extends Schema ? InferIn<S> : undefined;
6666
status: HookActionStatus;
6767
reset: () => void;
6868
cb?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>;
@@ -110,7 +110,7 @@ const useActionCallbacks = <
110110
*/
111111
export const useAction = <
112112
const ServerError,
113-
const S extends Schema,
113+
const S extends Schema | undefined,
114114
const BAS extends readonly Schema[],
115115
const FVE,
116116
const FBAVE,
@@ -122,18 +122,18 @@ export const useAction = <
122122
const [, startTransition] = React.useTransition();
123123
const [result, setResult] =
124124
React.useState<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>>(DEFAULT_RESULT);
125-
const [input, setInput] = React.useState<InferIn<S>>();
125+
const [input, setInput] = React.useState<S extends Schema ? InferIn<S> : void>();
126126
const [isExecuting, setIsExecuting] = React.useState(false);
127127

128128
const status = getActionStatus<ServerError, S, BAS, FVE, FBAVE, Data>({ isExecuting, result });
129129

130130
const execute = React.useCallback(
131-
(input: InferIn<S>) => {
131+
(input: S extends Schema ? InferIn<S> : void) => {
132132
setInput(input);
133133
setIsExecuting(true);
134134

135135
return startTransition(() => {
136-
return safeActionFn(input)
136+
return safeActionFn(input as S extends Schema ? InferIn<S> : undefined)
137137
.then((res) => setResult(res ?? DEFAULT_RESULT))
138138
.catch((e) => {
139139
if (isRedirectError(e) || isNotFoundError(e)) {
@@ -154,7 +154,13 @@ export const useAction = <
154154
setResult(DEFAULT_RESULT);
155155
}, []);
156156

157-
useActionCallbacks({ result, input, status, reset, cb: callbacks });
157+
useActionCallbacks({
158+
result,
159+
input: input as S extends Schema ? InferIn<S> : undefined,
160+
status,
161+
reset,
162+
cb: callbacks,
163+
});
158164

159165
return {
160166
execute,
@@ -177,38 +183,38 @@ export const useAction = <
177183
*/
178184
export const useOptimisticAction = <
179185
const ServerError,
180-
const S extends Schema,
186+
const S extends Schema | undefined,
181187
const BAS extends readonly Schema[],
182188
const FVE,
183189
const FBAVE,
184190
const Data,
185191
>(
186192
safeActionFn: HookSafeActionFn<ServerError, S, BAS, FVE, FBAVE, Data>,
187193
initialOptimisticData: Data,
188-
reducer: (state: Data, input: InferIn<S>) => Data,
194+
reducer: (state: Data, input: S extends Schema ? InferIn<S> : undefined) => Data,
189195
callbacks?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>
190196
) => {
191197
const [, startTransition] = React.useTransition();
192198
const [result, setResult] =
193199
React.useState<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>>(DEFAULT_RESULT);
194-
const [input, setInput] = React.useState<InferIn<S>>();
200+
const [input, setInput] = React.useState<S extends Schema ? InferIn<S> : void>();
195201
const [isExecuting, setIsExecuting] = React.useState(false);
196202

197-
const [optimisticData, setOptimisticState] = React.useOptimistic<Data, InferIn<S>>(
198-
initialOptimisticData,
199-
reducer
200-
);
203+
const [optimisticData, setOptimisticState] = React.useOptimistic<
204+
Data,
205+
S extends Schema ? InferIn<S> : undefined
206+
>(initialOptimisticData, reducer);
201207

202208
const status = getActionStatus<ServerError, S, BAS, FVE, FBAVE, Data>({ isExecuting, result });
203209

204210
const execute = React.useCallback(
205-
(input: InferIn<S>) => {
211+
(input: S extends Schema ? InferIn<S> : void) => {
206212
setInput(input);
207213
setIsExecuting(true);
208214

209215
return startTransition(() => {
210-
setOptimisticState(input);
211-
return safeActionFn(input)
216+
setOptimisticState(input as S extends Schema ? InferIn<S> : undefined);
217+
return safeActionFn(input as S extends Schema ? InferIn<S> : undefined)
212218
.then((res) => setResult(res ?? DEFAULT_RESULT))
213219
.catch((e) => {
214220
if (isRedirectError(e) || isNotFoundError(e)) {
@@ -229,7 +235,13 @@ export const useOptimisticAction = <
229235
setResult(DEFAULT_RESULT);
230236
}, []);
231237

232-
useActionCallbacks({ result, input, status, reset, cb: callbacks });
238+
useActionCallbacks({
239+
result,
240+
input: input as S extends Schema ? InferIn<S> : undefined,
241+
status,
242+
reset,
243+
cb: callbacks,
244+
});
233245

234246
return {
235247
execute,

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

+14-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { MaybePromise } from "./utils";
88
*/
99
export type HookResult<
1010
ServerError,
11-
S extends Schema,
11+
S extends Schema | undefined,
1212
BAS extends readonly Schema[],
1313
FVE,
1414
FBAVE,
@@ -22,22 +22,26 @@ export type HookResult<
2222
*/
2323
export type HookCallbacks<
2424
ServerError,
25-
S extends Schema,
25+
S extends Schema | undefined,
2626
BAS extends readonly Schema[],
2727
FVE,
2828
FBAVE,
2929
Data,
3030
> = {
31-
onExecute?: (args: { input: InferIn<S> }) => MaybePromise<void>;
32-
onSuccess?: (args: { data: Data; input: InferIn<S>; reset: () => void }) => MaybePromise<void>;
31+
onExecute?: (args: { input: S extends Schema ? InferIn<S> : undefined }) => MaybePromise<void>;
32+
onSuccess?: (args: {
33+
data: Data;
34+
input: S extends Schema ? InferIn<S> : undefined;
35+
reset: () => void;
36+
}) => MaybePromise<void>;
3337
onError?: (args: {
3438
error: Omit<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>, "data">;
35-
input: InferIn<S>;
39+
input: S extends Schema ? InferIn<S> : undefined;
3640
reset: () => void;
3741
}) => MaybePromise<void>;
3842
onSettled?: (args: {
3943
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
40-
input: InferIn<S>;
44+
input: S extends Schema ? InferIn<S> : undefined;
4145
reset: () => void;
4246
}) => MaybePromise<void>;
4347
};
@@ -48,12 +52,14 @@ export type HookCallbacks<
4852
*/
4953
export type HookSafeActionFn<
5054
ServerError,
51-
S extends Schema,
55+
S extends Schema | undefined,
5256
BAS extends readonly Schema[],
5357
FVE,
5458
FBAVE,
5559
Data,
56-
> = (clientInput: InferIn<S>) => Promise<SafeActionResult<ServerError, S, BAS, FVE, FBAVE, Data>>;
60+
> = (
61+
clientInput: S extends Schema ? InferIn<S> : undefined
62+
) => Promise<SafeActionResult<ServerError, S, BAS, FVE, FBAVE, Data>>;
5763

5864
/**
5965
* Type of the action status returned by `useAction` and `useOptimisticAction` hooks.

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

+37-11
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
8787
this.#metadata = data;
8888

8989
return {
90-
schema: <const S extends Schema, const FVE = ValidationErrors<S>>(
91-
schema: S,
90+
schema: <const S extends Schema | undefined = undefined, const FVE = ValidationErrors<S>>(
91+
schema?: S,
9292
utils?: {
9393
formatValidationErrors?: FormatValidationErrorsFn<S, FVE>;
9494
}
@@ -101,8 +101,12 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
101101
* @param schema An input schema supported by [TypeSchema](https://typeschema.com/#coverage).
102102
* @returns {Function} The `define` function, which is used to define a new safe action.
103103
*/
104-
public schema<const S extends Schema, const FVE = ValidationErrors<S>, const MD = null>(
105-
schema: S,
104+
public schema<
105+
const S extends Schema | undefined = undefined,
106+
const FVE = ValidationErrors<S>,
107+
const MD = null,
108+
>(
109+
schema?: S,
106110
utils?: {
107111
formatValidationErrors?: FormatValidationErrorsFn<S, FVE>;
108112
}
@@ -134,13 +138,13 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
134138
}
135139

136140
#bindArgsSchemas<
137-
const S extends Schema,
141+
const S extends Schema | undefined,
138142
const BAS extends readonly Schema[],
139143
const FVE,
140144
const FBAVE,
141145
const MD = null,
142146
>(args: {
143-
mainSchema: S;
147+
mainSchema?: S;
144148
bindArgsSchemas: BAS;
145149
formatValidationErrors?: FormatValidationErrorsFn<S, FVE>;
146150
formatBindArgsValidationErrors?: FormatBindArgsValidationErrorsFn<BAS, FBAVE>;
@@ -163,14 +167,14 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
163167
* @returns {SafeActionFn}
164168
*/
165169
#action<
166-
const S extends Schema,
170+
const S extends Schema | undefined,
167171
const BAS extends readonly Schema[],
168172
const FVE,
169173
const FBAVE = undefined,
170174
const Data = null,
171175
const MD = null,
172176
>(args: {
173-
schema: S;
177+
schema?: S;
174178
bindArgsSchemas: BAS;
175179
serverCodeFn: ServerCodeFn<S, BAS, Data, Ctx, MD>;
176180
formatValidationErrors?: FormatValidationErrorsFn<S, FVE>;
@@ -181,6 +185,14 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
181185
let frameworkError: Error | undefined = undefined;
182186
const middlewareResult: MiddlewareResult<ServerError, any> = { success: false };
183187

188+
// If the number of bind args schemas + 1 (which is the optional main arg schema) is greater
189+
// than the number of provided client inputs, it means that the main argument is missing.
190+
// This happens when the main schema is missing (since it's optional), or if a void main schema
191+
// is provided along with bind args schemas.
192+
if (args.bindArgsSchemas.length + 1 > clientInputs.length) {
193+
clientInputs.push(undefined);
194+
}
195+
184196
// Execute the middleware stack.
185197
const executeMiddlewareChain = async (idx = 0) => {
186198
const currentFn = this.#middlewareFns[idx];
@@ -204,8 +216,22 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
204216
// Validate the client inputs in parallel.
205217
const parsedInputs = await Promise.all(
206218
clientInputs.map((input, i) => {
207-
const s = i === clientInputs.length - 1 ? args.schema : args.bindArgsSchemas[i]!;
208-
return validate(s, input);
219+
// Last client input in the array, main argument (no bind arg).
220+
if (i === clientInputs.length - 1) {
221+
// If schema is undefined, set parsed data to undefined.
222+
if (typeof args.schema === "undefined") {
223+
return {
224+
success: true,
225+
data: undefined,
226+
} as const;
227+
}
228+
229+
// Otherwise, parse input with the schema.
230+
return validate(args.schema, input);
231+
}
232+
233+
// Otherwise, we're processing bind args client inputs.
234+
return validate(args.bindArgsSchemas[i]!, input);
209235
})
210236
);
211237

@@ -256,7 +282,7 @@ class SafeActionClient<const ServerError, const Ctx = null, const Metadata = nul
256282

257283
const data =
258284
(await args.serverCodeFn({
259-
parsedInput: parsedInputDatas.at(-1) as Infer<S>,
285+
parsedInput: parsedInputDatas.at(-1) as S extends Schema ? Infer<S> : undefined,
260286
bindArgsParsedInputs: parsedInputDatas.slice(0, -1) as InferArray<BAS>,
261287
ctx: prevCtx,
262288
metadata: this.#metadata as any as MD,

0 commit comments

Comments
 (0)
Please sign in to comment.