Skip to content

Commit df902d4

Browse files
committed
fix(hooks): thrown errors set hasErrored status and trigger onError callback
1 parent c280512 commit df902d4

File tree

5 files changed

+43
-26
lines changed

5 files changed

+43
-26
lines changed
Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use server";
22

33
import { ActionError, action } from "@/lib/safe-action";
4-
import { flattenValidationErrors } from "next-safe-action";
54
import { z } from "zod";
65

76
const schema = z.object({
@@ -10,24 +9,15 @@ const schema = z.object({
109

1110
export const deleteUser = action
1211
.metadata({ actionName: "deleteUser" })
13-
.inputSchema(schema, { handleValidationErrorsShape: async (ve) => flattenValidationErrors(ve) })
14-
.action(
15-
async ({ parsedInput: { userId } }) => {
16-
await new Promise((res) => setTimeout(res, 1000));
12+
.inputSchema(schema)
13+
.action(async ({ parsedInput: { userId } }) => {
14+
await new Promise((res) => setTimeout(res, 1000));
1715

18-
if (Math.random() > 0.5) {
19-
throw new ActionError("Could not delete user!");
20-
}
21-
22-
return {
23-
deletedUserId: userId,
24-
};
25-
},
26-
{
27-
throwValidationErrors: {
28-
async overrideErrorMessage(validationErrors) {
29-
return validationErrors.fieldErrors.userId?.[0] ?? "Invalid user ID";
30-
},
31-
},
16+
if (Math.random() > 0.5) {
17+
throw new ActionError("Could not delete user!");
3218
}
33-
);
19+
20+
return {
21+
deletedUserId: userId,
22+
};
23+
});

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ export const getActionStatus = <ServerError, S extends StandardSchemaV1 | undefi
1010
isExecuting,
1111
result,
1212
hasNavigated,
13+
hasThrownError,
1314
}: {
1415
isIdle: boolean;
1516
isExecuting: boolean;
1617
hasNavigated: boolean;
18+
hasThrownError: boolean;
1719
result: SafeActionResult<ServerError, S, CVE, Data>;
1820
}): HookActionStatus => {
1921
if (isIdle) {
2022
return "idle";
2123
} else if (isExecuting) {
2224
return "executing";
23-
} else if (typeof result.validationErrors !== "undefined" || typeof result.serverError !== "undefined") {
25+
} else if (
26+
hasThrownError ||
27+
typeof result.validationErrors !== "undefined" ||
28+
typeof result.serverError !== "undefined"
29+
) {
2430
return "hasErrored";
2531
} else if (hasNavigated) {
2632
return "hasNavigated";
@@ -53,12 +59,14 @@ export const useActionCallbacks = <ServerError, S extends StandardSchemaV1 | und
5359
status,
5460
cb,
5561
navigationError,
62+
thrownError,
5663
}: {
5764
result: SafeActionResult<ServerError, S, CVE, Data>;
5865
input: InferInputOrDefault<S, undefined>;
5966
status: HookActionStatus;
6067
cb?: HookCallbacks<ServerError, S, CVE, Data>;
61-
navigationError?: Error | null;
68+
navigationError: Error | null;
69+
thrownError: Error | null;
6270
}) => {
6371
const onExecuteRef = React.useRef(cb?.onExecute);
6472
const onSuccessRef = React.useRef(cb?.onSuccess);
@@ -80,7 +88,7 @@ export const useActionCallbacks = <ServerError, S extends StandardSchemaV1 | und
8088
await Promise.resolve(onExecute?.({ input })).then(() => {});
8189
break;
8290
case "hasSucceeded":
83-
if (navigationError) {
91+
if (navigationError || thrownError) {
8492
break;
8593
}
8694

@@ -91,7 +99,7 @@ export const useActionCallbacks = <ServerError, S extends StandardSchemaV1 | und
9199
break;
92100
case "hasErrored":
93101
await Promise.all([
94-
Promise.resolve(onError?.({ error: result, input })),
102+
Promise.resolve(onError?.({ error: { ...result, ...(thrownError ? { thrownError } : {}) }, input })),
95103
Promise.resolve(onSettled?.({ result, input })),
96104
]);
97105
break;
@@ -113,5 +121,5 @@ export const useActionCallbacks = <ServerError, S extends StandardSchemaV1 | und
113121
};
114122

115123
executeCallbacks().catch(console.error);
116-
}, [input, status, result, navigationError]);
124+
}, [input, status, result, navigationError, thrownError]);
117125
};

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,23 @@ export const useAction = <ServerError, S extends StandardSchemaV1 | undefined, C
3131
const [clientInput, setClientInput] = React.useState<InferInputOrDefault<S, void>>();
3232
const [isExecuting, setIsExecuting] = React.useState(false);
3333
const [navigationError, setNavigationError] = React.useState<Error | null>(null);
34+
const [thrownError, setThrownError] = React.useState<Error | null>(null);
3435
const [isIdle, setIsIdle] = React.useState(true);
3536

3637
const status = getActionStatus<ServerError, S, CVE, Data>({
3738
isExecuting,
3839
result,
3940
isIdle,
4041
hasNavigated: navigationError !== null,
42+
hasThrownError: thrownError !== null,
4143
});
4244

4345
const execute = React.useCallback(
4446
(input: InferInputOrDefault<S, void>) => {
4547
setTimeout(() => {
4648
setIsIdle(false);
4749
setNavigationError(null);
50+
setThrownError(null);
4851
setClientInput(input);
4952
setIsExecuting(true);
5053
}, 0);
@@ -60,6 +63,7 @@ export const useAction = <ServerError, S extends StandardSchemaV1 | undefined, C
6063
return;
6164
}
6265

66+
setThrownError(e as Error);
6367
throw e;
6468
})
6569
.finally(() => {
@@ -76,6 +80,7 @@ export const useAction = <ServerError, S extends StandardSchemaV1 | undefined, C
7680
setTimeout(() => {
7781
setIsIdle(false);
7882
setNavigationError(null);
83+
setThrownError(null);
7984
setClientInput(input);
8085
setIsExecuting(true);
8186
}, 0);
@@ -94,6 +99,7 @@ export const useAction = <ServerError, S extends StandardSchemaV1 | undefined, C
9499
return;
95100
}
96101

102+
setThrownError(e as Error);
97103
reject(e);
98104
})
99105
.finally(() => {
@@ -110,6 +116,7 @@ export const useAction = <ServerError, S extends StandardSchemaV1 | undefined, C
110116
const reset = React.useCallback(() => {
111117
setIsIdle(true);
112118
setNavigationError(null);
119+
setThrownError(null);
113120
setClientInput(undefined);
114121
setResult({});
115122
}, []);
@@ -119,6 +126,7 @@ export const useAction = <ServerError, S extends StandardSchemaV1 | undefined, C
119126
input: clientInput as InferInputOrDefault<S, undefined>,
120127
status,
121128
navigationError,
129+
thrownError,
122130
cb,
123131
});
124132

@@ -152,6 +160,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
152160
const [clientInput, setClientInput] = React.useState<InferInputOrDefault<S, void>>();
153161
const [isExecuting, setIsExecuting] = React.useState(false);
154162
const [navigationError, setNavigationError] = React.useState<Error | null>(null);
163+
const [thrownError, setThrownError] = React.useState<Error | null>(null);
155164
const [isIdle, setIsIdle] = React.useState(true);
156165
const [optimisticState, setOptimisticValue] = React.useOptimistic<State, InferInputOrDefault<S, undefined>>(
157166
utils.currentState,
@@ -163,6 +172,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
163172
result,
164173
isIdle,
165174
hasNavigated: navigationError !== null,
175+
hasThrownError: thrownError !== null,
166176
});
167177

168178
const execute = React.useCallback(
@@ -171,6 +181,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
171181
setIsIdle(false);
172182
setClientInput(input);
173183
setNavigationError(null);
184+
setThrownError(null);
174185
setIsExecuting(true);
175186
}, 0);
176187

@@ -186,6 +197,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
186197
return;
187198
}
188199

200+
setThrownError(e as Error);
189201
throw e;
190202
})
191203
.finally(() => {
@@ -203,6 +215,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
203215
setIsIdle(false);
204216
setClientInput(input);
205217
setNavigationError(null);
218+
setThrownError(null);
206219
setIsExecuting(true);
207220
}, 0);
208221

@@ -221,6 +234,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
221234
return;
222235
}
223236

237+
setThrownError(e as Error);
224238
reject(e);
225239
})
226240
.finally(() => {
@@ -238,6 +252,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
238252
setIsIdle(true);
239253
setClientInput(undefined);
240254
setNavigationError(null);
255+
setThrownError(null);
241256
setResult({});
242257
}, []);
243258

@@ -246,6 +261,7 @@ export const useOptimisticAction = <ServerError, S extends StandardSchemaV1 | un
246261
input: clientInput as InferInputOrDefault<S, undefined>,
247262
status,
248263
navigationError,
264+
thrownError,
249265
cb: {
250266
onExecute: utils.onExecute,
251267
onSuccess: utils.onSuccess,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type HookCallbacks<ServerError, S extends StandardSchemaV1 | undefined, C
99
onExecute?: (args: { input: InferInputOrDefault<S, undefined> }) => MaybePromise<unknown>;
1010
onSuccess?: (args: { data?: Data; input: InferInputOrDefault<S, undefined> }) => MaybePromise<unknown>;
1111
onError?: (args: {
12-
error: Prettify<Omit<SafeActionResult<ServerError, S, CVE, Data>, "data">>;
12+
error: Prettify<Omit<SafeActionResult<ServerError, S, CVE, Data>, "data">> & { thrownError?: Error };
1313
input: InferInputOrDefault<S, undefined>;
1414
}) => MaybePromise<unknown>;
1515
onNavigation?: (args: {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const useStateAction = <ServerError, S extends StandardSchemaV1 | undefin
3535
isIdle,
3636
// FIXME: This is a workaround to avoid the status being "hasNavigated" when the action is executed.
3737
hasNavigated: false,
38+
hasThrownError: false,
3839
});
3940

4041
const execute = React.useCallback(
@@ -61,6 +62,8 @@ export const useStateAction = <ServerError, S extends StandardSchemaV1 | undefin
6162
onError: utils?.onError,
6263
onSettled: utils?.onSettled,
6364
},
65+
navigationError: null,
66+
thrownError: null,
6467
});
6568

6669
return {

0 commit comments

Comments
 (0)