next-safe-action
Version:
Type safe and validated Server Actions in your Next.js project.
363 lines (362 loc) • 11.8 kB
JavaScript
import { t as FrameworkErrorHandler } from "./errors-DSpBUWAx.mjs";
import * as React from "react";
//#region src/hooks-utils.ts
const getActionStatus = ({ isIdle, isExecuting, result, hasNavigated, hasThrownError }) => {
if (isIdle) return "idle";
else if (isExecuting) return "executing";
else if (hasThrownError || typeof result.validationErrors !== "undefined" || typeof result.serverError !== "undefined") return "hasErrored";
else if (hasNavigated) return "hasNavigated";
else return "hasSucceeded";
};
const getActionShorthandStatusObject = ({ status, isTransitioning }) => {
return {
isIdle: status === "idle",
isExecuting: status === "executing",
isTransitioning,
isPending: status === "executing" || isTransitioning,
hasSucceeded: status === "hasSucceeded",
hasErrored: status === "hasErrored",
hasNavigated: status === "hasNavigated"
};
};
/**
* Converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
function useCallbackRef(callback) {
const callbackRef = React.useRef(callback);
React.useEffect(() => {
callbackRef.current = callback;
});
return React.useMemo(() => ((arg) => callbackRef.current?.(arg)), []);
}
const useActionCallbacks = ({ result, input, status, cb, throwOnNavigation, navigationError, thrownError }) => {
const onExecute = useCallbackRef(cb?.onExecute);
const onSuccess = useCallbackRef(cb?.onSuccess);
const onError = useCallbackRef(cb?.onError);
const onSettled = useCallbackRef(cb?.onSettled);
const onNavigation = useCallbackRef(cb?.onNavigation);
React.useEffect(() => {
const executeCallbacks = async () => {
switch (status) {
case "executing":
await Promise.resolve(onExecute?.({ input })).then(() => {});
break;
case "hasSucceeded":
if (navigationError || thrownError) break;
await Promise.all([Promise.resolve(onSuccess?.({
data: result.data,
input
})), Promise.resolve(onSettled?.({
result,
input
}))]);
break;
case "hasErrored":
await Promise.all([Promise.resolve(onError?.({
error: {
...result,
...thrownError ? { thrownError } : {}
},
input
})), Promise.resolve(onSettled?.({
result,
input
}))]);
break;
}
if (throwOnNavigation || !navigationError) return;
if (FrameworkErrorHandler.getNavigationKind(navigationError) === "redirect" || status === "hasNavigated") {
const actualNavigationKind = FrameworkErrorHandler.getNavigationKind(navigationError);
await Promise.all([Promise.resolve(onNavigation?.({
input,
navigationKind: actualNavigationKind
})), Promise.resolve(onSettled?.({
result,
input,
navigationKind: actualNavigationKind
}))]);
}
};
executeCallbacks().catch(console.error);
}, [
input,
status,
result,
throwOnNavigation,
navigationError,
thrownError,
onExecute,
onSuccess,
onSettled,
onError,
onNavigation
]);
};
//#endregion
//#region src/hooks-shared.ts
/**
* Shared base hook for `useAction` and `useOptimisticAction`.
* Extracts common state management, execution logic, and callback wiring.
*
* @param onTransitionStart Optional callback invoked inside `startTransition` before the action runs.
* Used by `useOptimisticAction` to call `setOptimisticValue`.
*/
function useActionBase(safeActionFn, opts, onTransitionStart) {
const [isTransitioning, startTransition] = React.useTransition();
const [result, setResult] = React.useState({});
const [clientInput, setClientInput] = React.useState();
const [isExecuting, setIsExecuting] = React.useState(false);
const [navigationError, setNavigationError] = React.useState(null);
const [thrownError, setThrownError] = React.useState(null);
const [isIdle, setIsIdle] = React.useState(true);
const requestIdRef = React.useRef(0);
const onTransitionStartRef = React.useRef(onTransitionStart);
onTransitionStartRef.current = onTransitionStart;
const status = getActionStatus({
isExecuting,
result,
isIdle,
hasNavigated: navigationError !== null,
hasThrownError: thrownError !== null
});
const execute = React.useCallback((input) => {
const thisRequestId = ++requestIdRef.current;
setIsIdle(false);
setNavigationError(null);
setThrownError(null);
setClientInput(input);
setIsExecuting(true);
startTransition(() => {
onTransitionStartRef.current?.(input);
safeActionFn(input).then((res) => {
if (thisRequestId !== requestIdRef.current) return;
setResult(res ?? {});
}).catch((e) => {
if (thisRequestId === requestIdRef.current) {
setResult({});
if (FrameworkErrorHandler.isNavigationError(e)) setNavigationError(e);
else setThrownError(e);
}
if (!FrameworkErrorHandler.isNavigationError(e)) throw e;
}).finally(() => {
if (thisRequestId !== requestIdRef.current) return;
setIsExecuting(false);
});
});
}, [safeActionFn]);
const executeAsync = React.useCallback((input) => {
return new Promise((resolve, reject) => {
const thisRequestId = ++requestIdRef.current;
setIsIdle(false);
setNavigationError(null);
setThrownError(null);
setClientInput(input);
setIsExecuting(true);
startTransition(() => {
onTransitionStartRef.current?.(input);
safeActionFn(input).then((res) => {
if (thisRequestId === requestIdRef.current) setResult(res ?? {});
resolve(res);
}).catch((e) => {
if (thisRequestId === requestIdRef.current) {
setResult({});
if (FrameworkErrorHandler.isNavigationError(e)) setNavigationError(e);
else setThrownError(e);
}
reject(e);
if (!FrameworkErrorHandler.isNavigationError(e)) throw e;
}).finally(() => {
if (thisRequestId !== requestIdRef.current) return;
setIsExecuting(false);
});
});
});
}, [safeActionFn]);
const reset = React.useCallback(() => {
setIsIdle(true);
setNavigationError(null);
setThrownError(null);
setClientInput(void 0);
setResult({});
}, []);
useActionCallbacks({
result: result ?? {},
input: clientInput,
status,
throwOnNavigation: opts?.throwOnNavigation === true,
navigationError,
thrownError,
cb: opts
});
if (opts?.throwOnNavigation === true && navigationError !== null) throw navigationError;
return {
isTransitioning,
result,
clientInput,
status,
execute,
executeAsync,
reset,
shorthandStatus: getActionShorthandStatusObject({
status,
isTransitioning
})
};
}
//#endregion
//#region src/hooks.ts
/**
* Use the action from a Client Component via hook.
* @param safeActionFn The action function
* @param opts Optional configuration and callbacks
*
* {@link https://next-safe-action.dev/docs/execute-actions/hooks/useaction See docs for more information}
*/
const useAction = (safeActionFn, opts) => {
const { result, clientInput, status, execute, executeAsync, reset, shorthandStatus } = useActionBase(safeActionFn, opts);
return {
execute,
executeAsync,
input: clientInput,
result,
reset,
status,
...shorthandStatus
};
};
/**
* Use the action from a Client Component via hook, with optimistic data update.
* @param safeActionFn The action function
* @param utils Required `currentData` and `updateFn` and optional callbacks
*
* {@link https://next-safe-action.dev/docs/execute-actions/hooks/useoptimisticaction See docs for more information}
*/
const useOptimisticAction = (safeActionFn, utils) => {
const [optimisticState, setOptimisticValue] = React.useOptimistic(utils.currentState, utils.updateFn);
const { currentState: _, updateFn: __, ...hookOpts } = utils;
const { result, clientInput, status, execute, executeAsync, reset, shorthandStatus } = useActionBase(safeActionFn, hookOpts, setOptimisticValue);
return {
execute,
executeAsync,
input: clientInput,
result,
optimisticState,
reset,
status,
...shorthandStatus
};
};
/**
* Use the stateful action from a Client Component via hook. Used for actions defined with
* [`stateAction`](https://next-safe-action.dev/docs/define-actions/instance-methods#action--stateaction).
*
* Provides full lifecycle control: callbacks, status tracking, navigation error handling,
* `executeAsync`, `reset`, and `formAction` for `<form action={formAction}>` integration.
*
* Requires React 19+ (Next.js 15+). On older versions, a runtime error is thrown with guidance.
*
* @param safeActionFn The stateful action function created with `.stateAction()`.
* @param opts Optional configuration: `initResult` for initial state, plus all hook options and callbacks.
*
* {@link https://next-safe-action.dev/docs/execute-actions/hooks/usestateaction See docs for more information}
*/
const useStateAction = (safeActionFn, opts) => {
if (typeof React.useActionState !== "function") throw new Error("useStateAction requires React 19+ (Next.js 15+). For older versions, use React's useActionState directly with your safe action.");
const initResult = opts?.initResult;
const asyncResolverRef = React.useRef(null);
const prevResultOverrideRef = React.useRef(null);
const [navigationError, setNavigationError] = React.useState(null);
const [thrownError, setThrownError] = React.useState(null);
const [isIdle, setIsIdle] = React.useState(true);
const [isReset, setIsReset] = React.useState(false);
const [clientInput, setClientInput] = React.useState();
const [isTransitioning, startTransition] = React.useTransition();
const wrappedAction = React.useCallback(async (prevResult, input) => {
setIsIdle(false);
setIsReset(false);
setClientInput(input);
setNavigationError(null);
setThrownError(null);
const effectivePrevResult = prevResultOverrideRef.current ?? prevResult;
prevResultOverrideRef.current = null;
try {
const result = await safeActionFn(effectivePrevResult, input);
asyncResolverRef.current?.resolve(result);
return result;
} catch (e) {
if (FrameworkErrorHandler.isNavigationError(e)) {
setNavigationError(e);
asyncResolverRef.current?.reject(e);
return {};
}
setThrownError(e);
asyncResolverRef.current?.reject(e);
throw e;
} finally {
asyncResolverRef.current = null;
}
}, [safeActionFn]);
const [rawResult, dispatcher, isExecuting] = React.useActionState(wrappedAction, initResult ?? {});
const execute = React.useCallback((input) => {
setIsIdle(false);
setIsReset(false);
setNavigationError(null);
setThrownError(null);
setClientInput(input);
startTransition(() => {
dispatcher(input);
});
}, [dispatcher]);
const executeAsync = React.useCallback((input) => {
return new Promise((resolve, reject) => {
asyncResolverRef.current = {
resolve,
reject
};
execute(input);
});
}, [execute]);
const reset = React.useCallback(() => {
setIsIdle(true);
setIsReset(true);
setNavigationError(null);
setThrownError(null);
setClientInput(void 0);
prevResultOverrideRef.current = initResult ?? {};
}, [initResult]);
const result = isReset ? initResult ?? {} : rawResult ?? {};
const status = getActionStatus({
isExecuting,
result,
isIdle: isIdle && !isExecuting,
hasNavigated: navigationError !== null,
hasThrownError: thrownError !== null
});
useActionCallbacks({
result,
input: clientInput,
status,
cb: opts,
throwOnNavigation: opts?.throwOnNavigation === true,
navigationError,
thrownError
});
if (opts?.throwOnNavigation === true && navigationError !== null) throw navigationError;
return {
execute,
executeAsync,
formAction: dispatcher,
input: clientInput,
result,
reset,
status,
...getActionShorthandStatusObject({
status,
isTransitioning
})
};
};
//#endregion
export { useOptimisticAction as n, useStateAction as r, useAction as t };
//# sourceMappingURL=hooks-RMagaUHm.mjs.map