UNPKG

next-safe-action

Version:

Type safe and validated Server Actions in your Next.js project.

363 lines (362 loc) 11.8 kB
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