@hirely/hooks 0.1.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,503 @@
1
+ # @hirely/hooks
2
+
3
+ <div align="center">
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@hirely/hooks.svg?style=flat-square&color=blue)](https://www.npmjs.com/package/@hirely/hooks)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
+ [![React](https://img.shields.io/badge/React-18_%7C_19-61dafb?style=flat-square&logo=react&logoColor=black)](https://react.dev/)
9
+ [![Bun](https://img.shields.io/badge/Bun-Optimized-fbf0df?style=flat-square&logo=bun&logoColor=black)](https://bun.sh)
10
+ [![Tests](https://img.shields.io/badge/Tests-100%25_Passing-brightgreen?style=flat-square)](https://github.com/hirely/hooks)
11
+
12
+ <p align="center">
13
+ <strong>Production-ready, type-safe React form hook with Zod validation, seamless HTTP integration (Axios/fetch/Server Actions), and toast notifications.</strong>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <em>Zero forced runtime dependencies &bull; Built-in render optimizations &bull; Full React 18 & 19 Support</em>
18
+ </p>
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ## Highlights
25
+
26
+ - 🛡️ **End-to-End Type Safety** &ndash; Define your Zod schema once; get instant compile-time validation and form inference.
27
+ - ⚡ **High Performance & Zero-Render Overhead** &ndash; Internal callback ref stabilization guarantees that `onSubmit` and handlers remain referentially stable across re-renders even when passing inline functions.
28
+ - 🧩 **Zero Forced Dependencies** &ndash; Works out of the box with custom services, Next.js Server Actions, or native `fetch`. Axios and Sonner are dynamically loaded **only if requested**.
29
+ - 🚀 **Cached Module Loading** &ndash; Dynamic imports for optional libraries are cached in memory for sub-millisecond subsequent submissions.
30
+ - 🔄 **TanStack Query v5 Ready** &ndash; First-class `useFormMutation` hook combining form management and async mutations.
31
+ - 🌐 **Global Defaults** &ndash; Configure once via `createFormHandler` factory or wrap sections with `FormHandlerProvider`.
32
+ - 📦 **Modern Dual ESM/CJS Bundle** &ndash; Native ESM (`.mjs`/`.js`), CommonJS (`.cjs`), complete TypeScript definitions (`.d.ts`), and `"use client";` directives for Next.js App Router.
33
+
34
+ ---
35
+
36
+ ## Table of Contents
37
+
38
+ - [Installation](#installation)
39
+ - [Quick Start](#quick-start)
40
+ - [Core API](#core-api)
41
+ - [`useFormHandler`](#useformhandleroptions)
42
+ - [Return Value](#return-value)
43
+ - [Next.js Server Actions](#using-with-nextjs-server-actions)
44
+ - [React Query Integration (`useFormMutation`)](#react-query-integration-useformmutation)
45
+ - [Global Configuration](#global-configuration)
46
+ - [Factory Pattern (`createFormHandler`)](#1-factory-pattern-createformhandler)
47
+ - [React Context (`FormHandlerProvider`)](#2-react-context-formhandlerprovider)
48
+ - [Higher-Order Component (`withFormHandler`)](#higher-order-component-withformhandler)
49
+ - [Recipes & Advanced Usage](#recipes--advanced-usage)
50
+ - [File Uploads (`useFormData`)](#file-uploads-with-formdata)
51
+ - [Request Cancellation (`enableAbort`)](#request-cancellation)
52
+ - [Optimistic Updates](#optimistic-updates)
53
+ - [Native `fetch` Client](#using-native-fetch)
54
+ - [Custom Response & Error Parsers](#custom-response--error-parsers)
55
+ - [License](#license)
56
+
57
+ ---
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ # Bun
63
+ bun add @hirely/hooks
64
+
65
+ # npm
66
+ npm install @hirely/hooks
67
+
68
+ # pnpm
69
+ pnpm add @hirely/hooks
70
+
71
+ # Yarn
72
+ yarn add @hirely/hooks
73
+ ```
74
+
75
+ ### Peer Dependencies
76
+
77
+ Install the core dependencies:
78
+
79
+ ```bash
80
+ # Required
81
+ bun add react react-hook-form zod
82
+ ```
83
+
84
+ **Optional dependencies** (only required if you use their respective features):
85
+
86
+ ```bash
87
+ # For default HTTP client and toast notifications:
88
+ bun add axios sonner
89
+
90
+ # For TanStack React Query integration:
91
+ bun add @tanstack/react-query
92
+ ```
93
+
94
+ > **Note:** If you pass your own async `service` or `notify` handler, you don't even need `axios` or `sonner`.
95
+
96
+ ---
97
+
98
+ ## Quick Start
99
+
100
+ ```tsx
101
+ import { useFormHandler } from "@hirely/hooks";
102
+ import { z } from "zod";
103
+
104
+ const loginSchema = z.object({
105
+ email: z.string().email("Invalid email address"),
106
+ password: z.string().min(8, "Password must be at least 8 characters"),
107
+ });
108
+
109
+ export function LoginForm() {
110
+ const { register, onSubmit, loading, error } = useFormHandler({
111
+ schema: loginSchema,
112
+ endpoint: "/api/auth/login",
113
+ onSuccess: (data) => {
114
+ console.log("Logged in successfully:", data);
115
+ },
116
+ });
117
+
118
+ return (
119
+ <form onSubmit={onSubmit} className="form-container">
120
+ <input {...register("email")} type="email" placeholder="Email" />
121
+ <input {...register("password")} type="password" placeholder="Password" />
122
+
123
+ {error && <p className="error-text">{error.message}</p>}
124
+
125
+ <button type="submit" disabled={loading}>
126
+ {loading ? "Signing in..." : "Sign In"}
127
+ </button>
128
+ </form>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Core API
136
+
137
+ ### `useFormHandler(options)`
138
+
139
+ ```ts
140
+ const form = useFormHandler(options);
141
+ ```
142
+
143
+ #### Options
144
+
145
+ | Option | Type | Default | Description |
146
+ |---|---|---|---|
147
+ | `schema` | `ZodType<FieldValues>` | *Required* | Zod schema used for form validation and type inference. |
148
+ | `endpoint` | `string` | `undefined` | Target URL (used with Axios if no custom `service` is provided). |
149
+ | `method` | `'post' \| 'patch' \| 'put' \| 'delete'` | `'post'` | HTTP method for `endpoint`. |
150
+ | `service` | `(data: TData) => Promise<any>` | `undefined` | Custom async function (Server Action, `fetch`, SDK). Overrides `endpoint`. |
151
+ | `defaultValues` | `DefaultValues<TData>` | `undefined` | Default values for form fields. |
152
+ | `values` | `TData` | `undefined` | Reactive external values synced to the form. |
153
+ | `transformData` | `(data: TData) => any` | `undefined` | Transform payload prior to submission. |
154
+ | `onMutate` | `(data: TData) => any \| Promise<any>` | `undefined` | Pre-submission hook. Return value is passed as `context` to `onSuccess`/`onError`. |
155
+ | `onSuccess` | `(data: any, context?: any) => void` | `undefined` | Callback invoked after a successful submission. |
156
+ | `onError` | `(error: any, context?: any) => void` | `undefined` | Callback invoked upon submission failure. |
157
+ | `onSubmitStart` | `() => void` | `undefined` | Triggered immediately when submission initiates. |
158
+ | `onSubmitEnd` | `() => void` | `undefined` | Triggered when submission concludes (success or error). |
159
+ | `notify` | `(message: string, type: 'success' \| 'error') => void` | Sonner toast | Custom notification function. Falls back to Sonner if installed. |
160
+ | `resetOptions` | `object` | `{ resetAfterSuccess: true, keepDefaultValues: true }` | Controls form reset behavior on success. |
161
+ | `useFormData` | `boolean` | `false` | Automatically converts payload to `FormData` (supports `File` and `Blob`). |
162
+ | `enableAbort` | `boolean` | `false` | Enables `abort()` method to cancel in-flight HTTP requests. |
163
+ | `axiosConfig` | `Record<string, any>` | `{}` | Additional configuration passed to Axios. |
164
+ | `mode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | `'onSubmit'` | React Hook Form validation mode. |
165
+ | `reValidateMode`| `'onChange' \| 'onBlur' \| 'onSubmit'` | `'onChange'` | Validation mode on re-renders after submit. |
166
+ | `parseResponse` | `(res: any) => { success: boolean, message?: string, data?: any }` | Standard parser | Custom response validator and extractor. |
167
+ | `parseError` | `(err: any) => { message: string }` | Axios error parser | Custom error message extractor. |
168
+
169
+ ---
170
+
171
+ ### Return Value
172
+
173
+ Returns **everything from React Hook Form's `useForm`** (`register`, `watch`, `setValue`, `getValues`, `formState`, `control`, etc.) along with:
174
+
175
+ | Property | Type | Description |
176
+ |---|---|---|
177
+ | `onSubmit` | `(e?: unknown) => Promise<void>` | Stable submit handler ready to attach to `<form onSubmit={onSubmit}>`. |
178
+ | `loading` | `boolean` | `true` while the async request or service is active. |
179
+ | `error` | `Error \| null` | Error object if submission failed. |
180
+ | `setError` | `React.Dispatch<React.SetStateAction<Error \| null>>` | Manually set or clear the form error state. |
181
+ | `abort` | `(() => void) \| undefined` | Cancels the active in-flight request (active when `enableAbort: true`). |
182
+
183
+ ---
184
+
185
+ ## Using with Next.js Server Actions
186
+
187
+ Because `service` accepts any standard async function, you can plug in **Next.js Server Actions** directly with full type safety:
188
+
189
+ ```ts
190
+ // app/actions/user.ts
191
+ "use server";
192
+
193
+ import { z } from "zod";
194
+
195
+ const createUserSchema = z.object({
196
+ name: z.string().min(2),
197
+ email: z.string().email(),
198
+ });
199
+
200
+ export async function createUserAction(data: z.infer<typeof createUserSchema>) {
201
+ // Database or external API call
202
+ return { success: true, message: "Account created!", id: "user_123" };
203
+ }
204
+ ```
205
+
206
+ ```tsx
207
+ // app/components/SignupForm.tsx
208
+ "use client";
209
+
210
+ import { useFormHandler } from "@hirely/hooks";
211
+ import { createUserAction } from "@/app/actions/user";
212
+ import { z } from "zod";
213
+
214
+ const schema = z.object({
215
+ name: z.string().min(2),
216
+ email: z.string().email(),
217
+ });
218
+
219
+ export function SignupForm() {
220
+ const { register, onSubmit, loading } = useFormHandler({
221
+ schema,
222
+ service: createUserAction,
223
+ onSuccess: (data) => console.log("Created user ID:", data.id),
224
+ });
225
+
226
+ return (
227
+ <form onSubmit={onSubmit}>
228
+ <input {...register("name")} placeholder="Your name" />
229
+ <input {...register("email")} placeholder="Your email" />
230
+ <button type="submit" disabled={loading}>
231
+ {loading ? "Creating..." : "Create Account"}
232
+ </button>
233
+ </form>
234
+ );
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ ## React Query Integration (`useFormMutation`)
241
+
242
+ For projects using **@tanstack/react-query**, `useFormMutation` unites form management with React Query's cache invalidation, mutation tracking, and retry logic:
243
+
244
+ ```tsx
245
+ import { useFormMutation } from "@hirely/hooks";
246
+ import { useQueryClient } from "@tanstack/react-query";
247
+ import { z } from "zod";
248
+
249
+ const postSchema = z.object({
250
+ title: z.string().min(1),
251
+ content: z.string().min(10),
252
+ });
253
+
254
+ export function CreatePost() {
255
+ const queryClient = useQueryClient();
256
+
257
+ const {
258
+ register,
259
+ mutate,
260
+ isPending,
261
+ isLoading, // backward compatible alias for isPending
262
+ error,
263
+ reset, // resets both mutation and form fields
264
+ } = useFormMutation({
265
+ schema: postSchema,
266
+ endpoint: "/api/posts",
267
+ mutationOptions: {
268
+ onSuccess: () => {
269
+ queryClient.invalidateQueries({ queryKey: ["posts"] });
270
+ },
271
+ },
272
+ });
273
+
274
+ return (
275
+ <form onSubmit={mutate}>
276
+ <input {...register("title")} placeholder="Title" />
277
+ <textarea {...register("content")} placeholder="Content" />
278
+ <button type="submit" disabled={isPending}>
279
+ {isPending ? "Publishing..." : "Publish Post"}
280
+ </button>
281
+ </form>
282
+ );
283
+ }
284
+ ```
285
+
286
+ ### `useFormMutation` Return Properties
287
+
288
+ In addition to all form handler properties, it includes:
289
+ - `mutate` & `mutateAsync` &ndash; Trigger mutation with typed variables.
290
+ - `isPending` &ndash; TanStack Query v5 pending state boolean.
291
+ - `isLoading` &ndash; Backwards-compatible alias for `isPending`.
292
+ - `isError` & `error` &ndash; Mutation error state.
293
+ - `formError` &ndash; Dedicated form-level error (if distinct).
294
+ - `data` &ndash; Response data from mutation.
295
+ - `reset` &ndash; Atomically resets **both** the form fields and the mutation state.
296
+ - `resetForm` & `resetMutation` &ndash; Independent reset triggers.
297
+ - `status` &ndash; Current mutation status (`'idle' | 'pending' | 'success' | 'error'`).
298
+
299
+ ---
300
+
301
+ ## Global Configuration
302
+
303
+ ### 1. Factory Pattern (`createFormHandler`)
304
+
305
+ Create a pre-configured hook with company-wide defaults (such as custom notification libraries or API interceptors):
306
+
307
+ ```tsx
308
+ // lib/form.ts
309
+ import { createFormHandler } from "@hirely/hooks";
310
+
311
+ export const useAppForm = createFormHandler({
312
+ mode: "onBlur",
313
+ resetOptions: { resetAfterSuccess: true, keepDefaultValues: false },
314
+ notify: (msg, type) => {
315
+ if (type === "success") console.log("[Success]", msg);
316
+ else console.error("[Error]", msg);
317
+ },
318
+ });
319
+ ```
320
+
321
+ ```tsx
322
+ // FeatureComponent.tsx
323
+ import { useAppForm } from "@/lib/form";
324
+ import { z } from "zod";
325
+
326
+ const schema = z.object({ query: z.string() });
327
+
328
+ export function SearchForm() {
329
+ // Inherits global defaults, but you can override any option locally:
330
+ const { register, onSubmit } = useAppForm({
331
+ schema,
332
+ endpoint: "/api/search",
333
+ });
334
+
335
+ return <form onSubmit={onSubmit}>...</form>;
336
+ }
337
+ ```
338
+
339
+ ### 2. React Context (`FormHandlerProvider`)
340
+
341
+ Scope configurations to sub-trees of your component hierarchy:
342
+
343
+ ```tsx
344
+ import { FormHandlerProvider } from "@hirely/hooks";
345
+
346
+ export function AdminLayout({ children }: { children: React.ReactNode }) {
347
+ return (
348
+ <FormHandlerProvider
349
+ defaultOptions={{
350
+ axiosConfig: { headers: { "X-Admin-Scope": "true" } },
351
+ resetOptions: { resetAfterSuccess: false },
352
+ }}
353
+ >
354
+ {children}
355
+ </FormHandlerProvider>
356
+ );
357
+ }
358
+ ```
359
+
360
+ Any `useFormHandler` within this tree automatically merges provider defaults with local options.
361
+
362
+ ---
363
+
364
+ ## Higher-Order Component (`withFormHandler`)
365
+
366
+ For legacy class components or HOC architecture:
367
+
368
+ ```tsx
369
+ import React from "react";
370
+ import { withFormHandler } from "@hirely/hooks";
371
+ import type { useFormHandler } from "@hirely/hooks";
372
+ import { z } from "zod";
373
+
374
+ const feedbackSchema = z.object({
375
+ rating: z.number().min(1).max(5),
376
+ notes: z.string().optional(),
377
+ });
378
+
379
+ type FeedbackProps = {
380
+ formHandler: ReturnType<typeof useFormHandler<typeof feedbackSchema>>;
381
+ category: string;
382
+ };
383
+
384
+ class FeedbackView extends React.Component<FeedbackProps> {
385
+ render() {
386
+ const { formHandler, category } = this.props;
387
+ return (
388
+ <form onSubmit={formHandler.onSubmit}>
389
+ <h3>Category: {category}</h3>
390
+ <input type="number" {...formHandler.register("rating", { valueAsNumber: true })} />
391
+ <button type="submit" disabled={formHandler.loading}>Submit</button>
392
+ </form>
393
+ );
394
+ }
395
+ }
396
+
397
+ export default withFormHandler(FeedbackView, {
398
+ schema: feedbackSchema,
399
+ endpoint: "/api/feedback",
400
+ });
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Recipes & Advanced Usage
406
+
407
+ ### File Uploads with `FormData`
408
+
409
+ Enable `useFormData: true` to serialize payloads containing `File`, `Blob`, arrays of files, or text fields into standard `FormData`:
410
+
411
+ ```tsx
412
+ const uploadSchema = z.object({
413
+ title: z.string(),
414
+ avatar: z.instanceof(File),
415
+ });
416
+
417
+ const { register, onSubmit, setValue } = useFormHandler({
418
+ schema: uploadSchema,
419
+ endpoint: "/api/upload",
420
+ useFormData: true,
421
+ });
422
+ ```
423
+
424
+ ### Request Cancellation
425
+
426
+ Set `enableAbort: true` to get an `abort()` handle that cleanly cancels running requests:
427
+
428
+ ```tsx
429
+ const { onSubmit, abort, loading } = useFormHandler({
430
+ schema: largeReportSchema,
431
+ endpoint: "/api/generate-report",
432
+ enableAbort: true,
433
+ });
434
+
435
+ return (
436
+ <form onSubmit={onSubmit}>
437
+ <button type="submit" disabled={loading}>Generate</button>
438
+ {loading && <button type="button" onClick={abort}>Cancel</button>}
439
+ </form>
440
+ );
441
+ ```
442
+
443
+ ### Optimistic Updates
444
+
445
+ Use `onMutate` to perform client-side updates before the request completes and rollback on error:
446
+
447
+ ```tsx
448
+ const { onSubmit } = useFormHandler({
449
+ schema: updateTaskSchema,
450
+ endpoint: "/api/tasks/1",
451
+ onMutate: async (newData) => {
452
+ const previous = currentTask;
453
+ setTask((prev) => ({ ...prev, ...newData }));
454
+ return { previous }; // passed as `context`
455
+ },
456
+ onError: (error, context) => {
457
+ // Rollback to original state
458
+ setTask(context.previous);
459
+ },
460
+ });
461
+ ```
462
+
463
+ ### Using Native `fetch`
464
+
465
+ ```tsx
466
+ const { onSubmit } = useFormHandler({
467
+ schema: itemSchema,
468
+ service: async (data) => {
469
+ const res = await fetch("/api/items", {
470
+ method: "POST",
471
+ headers: { "Content-Type": "application/json" },
472
+ body: JSON.stringify(data),
473
+ });
474
+ if (!res.ok) throw new Error("Request failed with status " + res.status);
475
+ return res.json();
476
+ },
477
+ });
478
+ ```
479
+
480
+ ### Custom Response & Error Parsers
481
+
482
+ Adapt `@hirely/hooks` to any API response schema:
483
+
484
+ ```tsx
485
+ useFormHandler({
486
+ schema: mySchema,
487
+ endpoint: "/api/custom",
488
+ parseResponse: (res) => ({
489
+ success: res.code === 200,
490
+ message: res.statusText,
491
+ data: res.payload,
492
+ }),
493
+ parseError: (err) => ({
494
+ message: err.response?.data?.errorDescription || err.message || "Operation failed",
495
+ }),
496
+ });
497
+ ```
498
+
499
+ ---
500
+
501
+ ## License
502
+
503
+ [MIT](LICENSE) © Hirely
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ import type { UseFormHandlerOptions } from "./useFormHandler";
3
+ import type { ZodType } from "zod";
4
+ import type { FieldValues } from "react-hook-form";
5
+ export declare const FormHandlerProvider: <TSchema extends ZodType<FieldValues, any, any> = ZodType<FieldValues, any, any>>({ children, defaultOptions, }: {
6
+ children: ReactNode;
7
+ defaultOptions: Partial<UseFormHandlerOptions<TSchema>>;
8
+ }) => import("react").JSX.Element;
9
+ export declare const useFormHandlerContext: () => Partial<UseFormHandlerOptions<any, any>>;
10
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,OAAO,CAAC,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,iCAGhH;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC;CACzD,gCAOA,CAAC;AAEF,eAAO,MAAM,qBAAqB,gDAAuC,CAAC"}