next-safe-form 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ <!-- @format -->
2
+
3
+ # 🚀 next-safe-form
4
+
5
+ > Simplify React 19 Server Actions forms in Next.js — without the boilerplate.
6
+
7
+ ---
8
+
9
+ ## ✨ Why this library?
10
+
11
+ Using `useActionState` with forms in React 19 + Next.js can quickly become repetitive and messy:
12
+
13
+ - Handling loading states
14
+ - Managing field errors vs server errors
15
+ - Syncing form values
16
+ - Triggering side effects (toast, redirect, etc.)
17
+
18
+ 👉 **next-safe-form solves all of this with a single hook.**
19
+
20
+ ---
21
+
22
+ ## ⚡ Features
23
+
24
+ - ✅ Clean abstraction over `useActionState`
25
+ - ✅ Automatic handling of:
26
+
27
+ - loading state
28
+ - success state
29
+ - field errors
30
+ - server errors
31
+
32
+ - ✅ Works seamlessly with Next.js Server Actions
33
+ - ✅ Type-safe
34
+ - ✅ Minimal API
35
+ - ✅ Scalable architecture
36
+
37
+ ---
38
+
39
+ ## 📦 Installation
40
+
41
+ ```bash
42
+ npm install next-safe-form
43
+ ```
44
+
45
+ or
46
+
47
+ ```bash
48
+ pnpm add next-safe-form
49
+ ```
50
+
51
+ or
52
+
53
+ ```bash
54
+ yarn add next-safe-form
55
+ ```
56
+
57
+ or
58
+
59
+ ```bash
60
+ bun add next-safe-form
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 🧠 Concept (Simple Explanation)
66
+
67
+ Think of it like this:
68
+
69
+ 👉 Instead of manually handling:
70
+
71
+ - `useActionState`
72
+ - error parsing
73
+ - UI state
74
+ - side effects
75
+
76
+ You just use:
77
+
78
+ ```ts
79
+ useActionForm();
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🚀 Usage
85
+
86
+ ### 1. Server Action (standard format)
87
+
88
+ ```ts
89
+ export async function loginAction(_, formData: FormData) {
90
+ const data = Object.fromEntries(formData);
91
+
92
+ // Example validation error
93
+ if (!data.email) {
94
+ return {
95
+ success: false,
96
+ error: { email: "Email is required" },
97
+ type: "fields",
98
+ };
99
+ }
100
+
101
+ // Example server error
102
+ if (data.email !== "test@test.com") {
103
+ return {
104
+ success: false,
105
+ error: "Invalid credentials",
106
+ type: "server",
107
+ };
108
+ }
109
+
110
+ return {
111
+ success: true,
112
+ data,
113
+ };
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ### 2. Client Form
120
+
121
+ ```tsx
122
+ "use client";
123
+
124
+ import { useActionForm } from "next-safe-form";
125
+
126
+ export default function LoginForm() {
127
+ const { formAction, isPending, fieldsErrors, serverError } = useActionForm({
128
+ action: loginAction,
129
+ initialValues: {
130
+ email: "",
131
+ password: "",
132
+ },
133
+ onSuccess: () => {
134
+ alert("Login successful!");
135
+ },
136
+ onError: (err) => {
137
+ alert(err);
138
+ },
139
+ });
140
+
141
+ return (
142
+ <form action={formAction}>
143
+ <input name="email" placeholder="Email" />
144
+ {fieldsErrors?.email && <p>{fieldsErrors.email}</p>}
145
+
146
+ <input name="password" type="password" placeholder="Password" />
147
+
148
+ {serverError && <p>{serverError}</p>}
149
+
150
+ <button disabled={isPending}>{isPending ? "Loading..." : "Login"}</button>
151
+ </form>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 🧩 API
159
+
160
+ ### `useActionForm(options)`
161
+
162
+ | Option | Type | Description |
163
+ | --------------- | -------- | ---------------------- |
164
+ | `action` | function | Server action |
165
+ | `initialValues` | object | Initial form values |
166
+ | `onSuccess` | function | Called on success |
167
+ | `onError` | function | Called on server error |
168
+
169
+ ---
170
+
171
+ ## 🔁 Returned values
172
+
173
+ | Property | Description |
174
+ | -------------- | ----------------------- |
175
+ | `formAction` | Form action handler |
176
+ | `isPending` | Loading state |
177
+ | `fieldsErrors` | Field validation errors |
178
+ | `serverError` | Server error message |
179
+ | `data` | Returned data |
180
+ | `isSuccess` | Success state |
181
+ | `reset` | Reset form |
182
+
183
+ ---
184
+
185
+ ## 🏗️ Roadmap
186
+
187
+ - [ ] Zod integration
188
+ - [ ] Client-side validation
189
+ - [ ] Server helpers (`createSafeAction`)
190
+ - [ ] Devtools
191
+
192
+ ---
193
+
194
+ ## 🤝 Contributing
195
+
196
+ Contributions are welcome!
197
+
198
+ ---
199
+
200
+ ## 📄 License
201
+
202
+ MIT
203
+
204
+ ---
205
+
206
+ ## 👤 Author
207
+
208
+ Built with ❤️ for Next.js developers
package/dist/index.cjs ADDED
@@ -0,0 +1,85 @@
1
+ /** @format */
2
+
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __export = (target, all) => {
11
+ for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if ((from && typeof from === "object") || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (
21
+ (target = mod != null ? __create(__getProtoOf(mod)) : {}),
22
+ __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ )
30
+ );
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/index.ts
34
+ var index_exports = {};
35
+ __export(index_exports, {
36
+ useActionForm: () => use_action_form_default,
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/hooks/use-action-form.ts
41
+ var import_react = __toESM(require("react"), 1);
42
+ function useActionForm({ action, initialValues, onSuccess, onError }) {
43
+ const [state, formAction, isPending] = import_react.default.useActionState(action, {
44
+ success: false,
45
+ data: initialValues,
46
+ });
47
+ const [fieldsErrors, setFieldsErrors] = import_react.default.useState(void 0);
48
+ const [serverError, setServerError] = import_react.default.useState(void 0);
49
+ const [data, setData] = import_react.default.useState(initialValues);
50
+ import_react.default.useEffect(() => {
51
+ if (!state) return;
52
+ if (state.success) {
53
+ setData(state.data);
54
+ onSuccess?.(state.data);
55
+ return;
56
+ }
57
+ if (state.error?.type === "fields") {
58
+ setFieldsErrors(state.error.fields);
59
+ }
60
+ if (state.error?.type === "server") {
61
+ setServerError(state.error.message);
62
+ onError?.(state.error.message);
63
+ }
64
+ }, [state]);
65
+ const reset = import_react.default.useCallback(() => {
66
+ setData(initialValues);
67
+ setFieldsErrors(void 0);
68
+ setServerError(void 0);
69
+ }, [initialValues]);
70
+ return {
71
+ formAction,
72
+ isPending,
73
+ fieldsErrors,
74
+ serverError,
75
+ data,
76
+ isSuccess: state?.success ?? false,
77
+ reset,
78
+ };
79
+ }
80
+ var use_action_form_default = useActionForm;
81
+ // Annotate the CommonJS export names for ESM import in node:
82
+ 0 &&
83
+ (module.exports = {
84
+ useActionForm,
85
+ });
@@ -0,0 +1,34 @@
1
+ /** @format */
2
+ type TSafeActionResult<T> = {
3
+ success: boolean;
4
+ data: T;
5
+ error?: {
6
+ type: "fields" | "server";
7
+ message?: string;
8
+ fields?: Record<string, string>;
9
+ code?: number;
10
+ };
11
+ };
12
+ interface UseActionFormOptions<T> {
13
+ action: (state: any, formData: FormData) => Promise<TSafeActionResult<T>>;
14
+ initialValues: T;
15
+ onSuccess?: (data: T) => void;
16
+ onError?: (error: any) => void;
17
+ }
18
+
19
+ /**
20
+ * eslint-disable @typescript-eslint/no-explicit-any
21
+ * @format
22
+ */
23
+
24
+ declare function useActionForm<T>({ action, initialValues, onSuccess, onError }: UseActionFormOptions<T>): {
25
+ formAction: (payload: FormData) => void;
26
+ isPending: boolean;
27
+ fieldsErrors: Record<string, string> | undefined;
28
+ serverError: string | undefined;
29
+ data: T;
30
+ isSuccess: boolean;
31
+ reset: () => void;
32
+ };
33
+
34
+ export { type TSafeActionResult, useActionForm };
@@ -0,0 +1,34 @@
1
+ /** @format */
2
+ type TSafeActionResult<T> = {
3
+ success: boolean;
4
+ data: T;
5
+ error?: {
6
+ type: "fields" | "server";
7
+ message?: string;
8
+ fields?: Record<string, string>;
9
+ code?: number;
10
+ };
11
+ };
12
+ interface UseActionFormOptions<T> {
13
+ action: (state: any, formData: FormData) => Promise<TSafeActionResult<T>>;
14
+ initialValues: T;
15
+ onSuccess?: (data: T) => void;
16
+ onError?: (error: any) => void;
17
+ }
18
+
19
+ /**
20
+ * eslint-disable @typescript-eslint/no-explicit-any
21
+ * @format
22
+ */
23
+
24
+ declare function useActionForm<T>({ action, initialValues, onSuccess, onError }: UseActionFormOptions<T>): {
25
+ formAction: (payload: FormData) => void;
26
+ isPending: boolean;
27
+ fieldsErrors: Record<string, string> | undefined;
28
+ serverError: string | undefined;
29
+ data: T;
30
+ isSuccess: boolean;
31
+ reset: () => void;
32
+ };
33
+
34
+ export { type TSafeActionResult, useActionForm };
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ /** @format */
2
+
3
+ // src/hooks/use-action-form.ts
4
+ import React from "react";
5
+ function useActionForm({ action, initialValues, onSuccess, onError }) {
6
+ const [state, formAction, isPending] = React.useActionState(action, {
7
+ success: false,
8
+ data: initialValues,
9
+ });
10
+ const [fieldsErrors, setFieldsErrors] = React.useState(void 0);
11
+ const [serverError, setServerError] = React.useState(void 0);
12
+ const [data, setData] = React.useState(initialValues);
13
+ React.useEffect(() => {
14
+ if (!state) return;
15
+ if (state.success) {
16
+ setData(state.data);
17
+ onSuccess?.(state.data);
18
+ return;
19
+ }
20
+ if (state.error?.type === "fields") {
21
+ setFieldsErrors(state.error.fields);
22
+ }
23
+ if (state.error?.type === "server") {
24
+ setServerError(state.error.message);
25
+ onError?.(state.error.message);
26
+ }
27
+ }, [state]);
28
+ const reset = React.useCallback(() => {
29
+ setData(initialValues);
30
+ setFieldsErrors(void 0);
31
+ setServerError(void 0);
32
+ }, [initialValues]);
33
+ return {
34
+ formAction,
35
+ isPending,
36
+ fieldsErrors,
37
+ serverError,
38
+ data,
39
+ isSuccess: state?.success ?? false,
40
+ reset,
41
+ };
42
+ }
43
+ var use_action_form_default = useActionForm;
44
+ export { use_action_form_default as useActionForm };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "next-safe-form",
3
+ "version": "1.0.0",
4
+ "description": "Simplify React 19 Server Actions forms in Next.js",
5
+ "types": "dist/index.d.ts",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "keywords": [
12
+ "nextjs",
13
+ "react",
14
+ "forms",
15
+ "server-actions"
16
+ ],
17
+ "author": "4develhoper",
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "devDependencies": {
21
+ "@types/bun": "latest",
22
+ "tsup": "^8.5.1"
23
+ },
24
+ "peerDependencies": {
25
+ "typescript": "^5"
26
+ },
27
+ "dependencies": {
28
+ "@types/react": "^19.2.14",
29
+ "react": "^19.2.4"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
33
+ "publish": "npm publish",
34
+ "publish-public": "npm publish --access public",
35
+ "publish-private": "npm publish --access restricted"
36
+ }
37
+ }