npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@n8x/react-form-utils

v1.2.0

Published

Reuseable form utils for instant JS object to form generation with zod validation, controlled form submission events and state management using react-hook-form

Readme

n8x-form-utils

A powerful React form library that simplifies form creation with instant JavaScript object-to-form generation, Zod validation, controlled form submission events, and state management using React Hook Form. A fastest way to create and manage forms in react.

Features:

  • Rapid form generation from simple field configuration
  • Built-in Zod validation with type safety
  • Multiple pre-built styling themes
  • Grouped form support for complex layouts
  • Controlled form submission handling
  • Support for 10+ field types
  • Query hooks for API integration and form submissions
  • Automatic form progress saving to localStorage (per-field)
  • New: Mobile-first responsive design control

Table of Contents


Installation

npm install @n8x/react-form-utils

Dependencies (auto-installed):

  • React 17+
  • React Hook Form 7+
  • Zod 4+
  • Tailwind CSS 4+
  • Axios

Setup

1. Wrap Your App with FormContextProvider

import { ReactNode } from 'react'
import { FormContext } from '@n8x/react-form-utils'
import FormContextProvider from '@n8x/react-form-utils'

export default function App() {
  return (
    <FormContextProvider>
      {/* Your routes and components */}
    </FormContextProvider>
  )
}

The FormContextProvider manages the global form state and validation schema. It should wrap your entire application or at least the components using N8xForm.


Quick Start

Create a Simple Login Form

Step 1: Define your form fields

// forms/login.ts
import { z } from '@n8x/react-form-utils'
import { FormFieldTypes, FormFieldWidth, type FormFields } from '@n8x/react-form-utils'

export const loginForm: FormFields[] = [
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    label: 'Email',
    placeholder: '[email protected]',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().email('Please enter a valid email')
  },
  {
    name: 'password',
    type: FormFieldTypes.PASSWORD,
    label: 'Password',
    placeholder: 'Enter your password',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(6, 'Password must be at least 6 characters')
  }
]

Step 2: Create the authentication service for your login form submission

// service/auth-service.ts
import axios from './index'; // configured as per your use case

const login = async (payload: AuthRequest) => {
    return await axios.post("/auth/login", payload)
}

export {
    login,
}

Step 3: Use the form in your component

// pages/auth/Login.tsx
import { useContext } from 'react'
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from '@n8x/react-form-utils'
import { loginForm } from '../../forms/login'
import { login } from '../../service/auth-service'

export default function LoginPage() {
  const { data, execute, error, isLoading } = useN8xFormQuery(login)

  const handleLoginSubmit = async (formData: any) => {
    execute(formData)
  }

  return (
    <div className="w-full min-h-screen flex justify-center items-center">
      <div className="md:w-[600px] w-full sm:p-6 p-3">
        <h1 className="text-4xl tracking-tighter">Account Login</h1>
        
        <N8xForm 
          fields={loginForm} 
          name="loginForm"
          onSubmit={handleLoginSubmit}
          defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
        >
          <button 
            disabled={isLoading} 
            type="submit" 
            className="col-span-4 bg-blue-500 disabled:bg-zinc-400 text-white py-2 px-4 rounded font-medium"
          >
            {isLoading ? 'Logging in...' : 'Login'}
          </button>
        </N8xForm>

        {error && <p className="text-red-500 text-center mt-4">{error}</p>}
      </div>
    </div>
  )
}

Field Types

N8x Form Utils supports 10+ field types. Here's a comprehensive example showing all available field types:

Basic Fields

TEXT Field

{
  name: 'username',
  type: FormFieldTypes.TEXT,
  label: 'Username',
  placeholder: 'Enter your username',
  required: true,
  width: FormFieldWidth.FULL,
  validationScheme: z.string().min(3, 'Username must be at least 3 characters')
}

EMAIL Field

{
  name: 'email',
  type: FormFieldTypes.EMAIL,
  label: 'Email Address',
  placeholder: '[email protected]',
  required: true,
  width: FormFieldWidth.FULL,
  validationScheme: z.string().email('Invalid email address')
}

PASSWORD Field

{
  name: 'password',
  type: FormFieldTypes.PASSWORD,
  label: 'Password',
  placeholder: 'Create a strong password',
  required: true,
  width: FormFieldWidth.FULL,
  validationScheme: z.string()
    .min(6, 'Password must be at least 6 characters')
    .max(36, 'Password too long')
}

NUMBER Field

{
  name: 'age',
  type: FormFieldTypes.NUMBER,
  label: 'Age',
  placeholder: 'Enter your age',
  min: 18,
  max: 100,
  required: true,
  width: FormFieldWidth.HALF,
  validationScheme: z.number().min(18, 'Must be 18 or older')
}

DATE Field

{
  name: 'birthDate',
  type: FormFieldTypes.DATE,
  label: 'Date of Birth',
  required: true,
  width: FormFieldWidth.HALF,
  validationScheme: z.string().refine(
    (date) => new Date(date) < new Date(),
    'Date must be in the past'
  )
}

Composite Fields

SELECT Field

{
  name: 'country',
  type: FormFieldTypes.SELECT,
  label: 'Country',
  required: true,
  width: FormFieldWidth.FULL,
  options: [
    { label: 'United States', value: 'US' },
    { label: 'Canada', value: 'CA' },
    { label: 'United Kingdom', value: 'GB' },
    { label: 'Australia', value: 'AU' }
  ],
  validationScheme: z.string().min(1, 'Please select a country')
}

RADIO Field

{
  name: 'accountType',
  type: FormFieldTypes.RADIO,
  label: 'Account Type',
  required: true,
  width: FormFieldWidth.FULL,
  options: [
    { label: 'Personal', value: 'personal' },
    { label: 'Business', value: 'business' },
    { label: 'Enterprise', value: 'enterprise' }
  ],
  validationScheme: z.string().min(1, 'Please select an account type')
}

CHECKBOX Field

{
  name: 'interests',
  type: FormFieldTypes.CHECKBOX,
  label: 'Interests',
  width: FormFieldWidth.FULL,
  options: [
    { label: 'Technology', value: 'tech' },
    { label: 'Finance', value: 'finance' },
    { label: 'Health', value: 'health' },
    { label: 'Travel', value: 'travel' }
  ],
  validationScheme: z.array(z.string()).min(1, 'Select at least one interest')
}

TEXTAREA Field

{
  name: 'bio',
  type: FormFieldTypes.TEXTAREA,
  label: 'Bio',
  placeholder: 'Tell us about yourself',
  width: FormFieldWidth.FULL,
  validationScheme: z.string()
    .min(10, 'Bio must be at least 10 characters')
    .max(500, 'Bio must not exceed 500 characters')
}

FILE Field

{
  name: 'profilePhoto',
  type: FormFieldTypes.FILE,
  label: 'Profile Photo',
  width: FormFieldWidth.FULL,
  validationScheme: z.any()
}

IP address field

{
    name: "ip_address",
    type: FormFieldTypes.TEXT,
    label: "IP Address",
    placeholder: "192.168.0.1",
    validationScheme: z.string().ip("Please enter a valid IP address"),
    width: {
      base: FormFieldWidth.FULL,
      md: FormFieldWidth.HALF,
      lg: FormFieldWidth.QUARTER,
      xl: FormFieldWidth.FULL,
    },
}

Responsive Design

N8x Form Utils provides mobile-first responsive width configuration using Tailwind CSS breakpoints. Fields can adapt their layout across different screen sizes without any additional styling code.

Static Width (No Responsiveness)

For simple cases, use static width values:

{
  name: 'email',
  type: FormFieldTypes.EMAIL,
  label: 'Email',
  width: FormFieldWidth.HALF,  // Always takes 50% width
  validationScheme: z.string().email('Invalid email')
}

Responsive Width Configuration

Use the responsive width object to define different widths for different screen sizes. The form uses a mobile-first approach, starting with a base width and overriding it at larger breakpoints.

{
  name: 'email',
  type: FormFieldTypes.EMAIL,
  label: 'Email',
  width: {
    base: FormFieldWidth.FULL,      // Mobile: 100% width (col-span-4)
    md: FormFieldWidth.HALF,         // Tablet 768px+: 50% width (col-span-2)
    lg: FormFieldWidth.QUARTER       // Desktop 1024px+: 25% width (col-span-1)
  },
  validationScheme: z.string().email('Invalid email')
}

Available Breakpoints

| Breakpoint | Screen Size | Tailwind Class | Usage | |------------|-------------|----|----------| | base | 0px+ (mobile) | - | Default width for all screen sizes | | sm | 640px | sm: | Small mobile devices | | md | 768px | md: | Tablets | | lg | 1024px | lg: | Laptops & desktops | | xl | 1280px | xl: | Large desktops | | 2xl | 1536px | 2xl: | Extra large screens |

FormFieldWidth Values

export enum FormFieldWidth {
  FULL = 'col-span-4',     // 100% width (4 columns)
  HALF = 'col-span-2',     // 50% width (2 columns)
  THIRD = 'col-span-3',    // 75% width (3 columns)
  QUARTER = 'col-span-1'   // 25% width (1 column)
}

Responsive Layout Examples

Example 1: Expanding Layout (Mobile → Desktop)

Fields grow from full width on mobile to quarter width on desktop:

{
  name: 'firstName',
  type: FormFieldTypes.TEXT,
  label: 'First Name',
  width: {
    base: FormFieldWidth.FULL,      // Mobile: full width
    md: FormFieldWidth.HALF,         // Tablet: half width
    lg: FormFieldWidth.QUARTER       // Desktop: quarter width
  }
}

Example 2: Contracting Layout

Fields shrink from full width on mobile to quarter width on tablet:

{
  name: 'phone',
  type: FormFieldTypes.TEXT,
  label: 'Phone Number',
  width: {
    base: FormFieldWidth.FULL,      // Mobile: full width
    md: FormFieldWidth.QUARTER,      // Tablet: quarter width
    lg: FormFieldWidth.HALF          // Desktop: half width
  }
}

Example 3: Complex Responsive Pattern

IP address field with varying widths across all breakpoints:

{
  name: 'ip_address',
  type: FormFieldTypes.TEXT,
  label: 'IP Address',
  placeholder: '192.168.0.1',
  width: {
    base: FormFieldWidth.FULL,      // Mobile: full width
    md: FormFieldWidth.HALF,         // Tablet 768px+: half width
    lg: FormFieldWidth.QUARTER,      // Desktop 1024px+: quarter width  
    xl: FormFieldWidth.FULL          // Large desktop 1280px+: full width
  },
  validationScheme: z.string().ip('Please enter a valid IP address'),
  saveProgress: true
}

Complete Responsive Form Example

import { z } from 'zod/v3'
import { FormFieldTypes, FormFieldWidth, type FormFields } from '@n8x/react-form-utils'

export const responsiveContactForm: FormFields[] = [
  {
    name: 'firstName',
    type: FormFieldTypes.TEXT,
    label: 'First Name',
    required: true,
    width: {
      base: FormFieldWidth.FULL,
      md: FormFieldWidth.HALF,
      lg: FormFieldWidth.QUARTER
    },
    validationScheme: z.string().min(2, 'First name required')
  },
  {
    name: 'lastName',
    type: FormFieldTypes.TEXT,
    label: 'Last Name',
    required: true,
    width: {
      base: FormFieldWidth.FULL,
      md: FormFieldWidth.HALF,
      lg: FormFieldWidth.QUARTER
    },
    validationScheme: z.string().min(2, 'Last name required')
  },
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    label: 'Email Address',
    required: true,
    width: {
      base: FormFieldWidth.FULL,
      md: FormFieldWidth.HALF,
      lg: FormFieldWidth.HALF
    },
    validationScheme: z.string().email('Invalid email')
  },
  {
    name: 'phone',
    type: FormFieldTypes.TEXT,
    label: 'Phone Number',
    width: {
      base: FormFieldWidth.FULL,
      md: FormFieldWidth.HALF,
      lg: FormFieldWidth.QUARTER
    },
    validationScheme: z.string().optional()
  },
  {
    name: 'message',
    type: FormFieldTypes.TEXTAREA,
    label: 'Message',
    placeholder: 'Your message here...',
    width: {
      base: FormFieldWidth.FULL,
      md: FormFieldWidth.FULL,
      lg: FormFieldWidth.FULL
    },
    validationScheme: z.string().min(10, 'Message must be at least 10 characters')
  }
]

Tips for Responsive Design

  1. Always define base - This is your mobile-first default
  2. Build up, not down - Start with mobile and add breakpoints for larger screens
  3. Test all breakpoints - Check your forms on actual devices or use browser dev tools
  4. Keep consistency - Use the same breakpoint structure across related fields
  5. Group related fields - Fields in the same section should have complementary responsive widths

Validation

N8x Form Utils uses Zod for type-safe schema validation. All fields support a validationScheme property where you define your validation rules.

Basic Validation Examples

import { z } from '@n8x/react-form-utils'

// Email validation
validationScheme: z.string().email('Invalid email address')

// Required string with minimum length
validationScheme: z.string().min(3, 'Minimum 3 characters required')

// Number with range
validationScheme: z.number().min(0).max(100, 'Value must be between 0 and 100')

// Custom refine validation
validationScheme: z.string().refine(
  (value) => value !== 'admin',
  'Username cannot be "admin"'
)

// Conditional validation
validationScheme: z.string().or(z.number())

// Array validation
validationScheme: z.array(z.string()).min(1, 'Select at least one option')

Complete Registration Form with Validation

// forms/register.ts
import { z } from '@n8x/react-form-utils'
import { FormFieldTypes, FormFieldWidth, type FormFields } from '@n8x/react-form-utils'

export const registerForm: FormFields[] = [
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    label: 'Email',
    placeholder: '[email protected]',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().email('Please enter a valid email')
  },
  {
    name: 'password',
    type: FormFieldTypes.PASSWORD,
    label: 'Password',
    placeholder: 'Create a strong password',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string()
      .min(6, 'Password must be at least 6 characters')
      .max(36, 'Password too long')
  },
  {
    name: 'confirmPassword',
    type: FormFieldTypes.PASSWORD,
    label: 'Confirm Password',
    placeholder: 'Re-enter your password',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(6, 'Password must be at least 6 characters')
  },
  {
    name: 'fullName',
    type: FormFieldTypes.TEXT,
    label: 'Full Name',
    placeholder: 'John Doe',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(2, 'Name must be at least 2 characters')
  },
  {
    name: 'agreeToTerms',
    type: FormFieldTypes.CHECKBOX,
    label: 'Terms',
    width: FormFieldWidth.FULL,
    options: [
      { label: 'I agree to the Terms and Conditions', value: 'agree' }
    ],
    validationScheme: z.array(z.string()).min(1, 'You must agree to the terms')
  }
]

Styling Themes

N8x Form Utils provides 4 pre-built styling themes via EDefaultFieldStyles. You can apply a theme through the defaultFieldStyles prop in the N8xForm component.

Available Themes

CLEAN (Minimal, professional)

<N8xForm 
  fields={loginForm}
  name="loginForm"
  defaultFieldStyles={EDefaultFieldStyles.CLEAN}
  onSubmit={handleSubmit}
/>

Clean white inputs with thin borders, subtle focus effects, and a professional appearance.

SOFT_GLASS (Modern, glassmorphism)

<N8xForm 
  fields={loginForm}
  name="loginForm"
  defaultFieldStyles={EDefaultFieldStyles.SOFT_GLASS}
  onSubmit={handleSubmit}
/>

Soft, frosted appearance with light background and smooth transitions. Default theme.

DARK (Dark mode)

<N8xForm 
  fields={loginForm}
  name="loginForm"
  defaultFieldStyles={EDefaultFieldStyles.DARK}
  onSubmit={handleSubmit}
/>

Dark background inputs suitable for dark mode interfaces.

FLOATING_LABEL (Material Design)

<N8xForm 
  fields={loginForm}
  name="loginForm"
  defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
  onSubmit={handleSubmit}
/>

Floating label style with animated labels on focus/input. Material Design inspired.

Custom Styling

You can also apply custom Tailwind CSS classes to individual fields:

{
  name: 'customField',
  type: FormFieldTypes.TEXT,
  label: 'Custom Styled',
  _className: 'bg-gradient-to-r from-blue-400 to-blue-600 text-white rounded-xl'
}

Or pass a custom string:

<N8xForm 
  fields={loginForm}
  name="loginForm"
  defaultFieldStyles="custom-class-string"
  onSubmit={handleSubmit}
/>

Grouped Forms

For complex forms with multiple sections, organize fields into logical groups. Grouped forms automatically render section headers.

Example: User Profile Form with Groups

// forms/userProfile.ts
import { z } from '@n8x/react-form-utils'
import { FormFieldTypes, FormFieldWidth, type FormFields } from '@n8x/react-form-utils'

export const userProfileForm = {
  'Personal Information': [
    {
      name: 'firstName',
      type: FormFieldTypes.TEXT,
      label: 'First Name',
      required: true,
      width: FormFieldWidth.HALF,
      validationScheme: z.string().min(2, 'First name is required')
    },
    {
      name: 'lastName',
      type: FormFieldTypes.TEXT,
      label: 'Last Name',
      required: true,
      width: FormFieldWidth.HALF,
      validationScheme: z.string().min(2, 'Last name is required')
    },
    {
      name: 'email',
      type: FormFieldTypes.EMAIL,
      label: 'Email Address',
      required: true,
      width: FormFieldWidth.FULL,
      validationScheme: z.string().email('Invalid email')
    },
    {
      name: 'birthDate',
      type: FormFieldTypes.DATE,
      label: 'Date of Birth',
      required: false,
      width: FormFieldWidth.HALF,
      validationScheme: z.string().optional()
    }
  ],
  'Contact Information': [
    {
      name: 'phone',
      type: FormFieldTypes.TEXT,
      label: 'Phone Number',
      placeholder: '+1 (555) 123-4567',
      width: FormFieldWidth.FULL,
      validationScheme: z.string().optional()
    },
    {
      name: 'country',
      type: FormFieldTypes.SELECT,
      label: 'Country',
      required: true,
      width: FormFieldWidth.HALF,
      options: [
        { label: 'United States', value: 'US' },
        { label: 'Canada', value: 'CA' },
        { label: 'United Kingdom', value: 'GB' }
      ],
      validationScheme: z.string().min(1, 'Select a country')
    },
    {
      name: 'city',
      type: FormFieldTypes.TEXT,
      label: 'City',
      required: true,
      width: FormFieldWidth.HALF,
      validationScheme: z.string().min(2, 'City is required')
    }
  ],
  'Preferences': [
    {
      name: 'language',
      type: FormFieldTypes.SELECT,
      label: 'Preferred Language',
      width: FormFieldWidth.FULL,
      options: [
        { label: 'English', value: 'en' },
        { label: 'Spanish', value: 'es' },
        { label: 'French', value: 'fr' },
        { label: 'German', value: 'de' }
      ],
      validationScheme: z.string().optional()
    },
    {
      name: 'newsletter',
      type: FormFieldTypes.CHECKBOX,
      label: 'Email Preferences',
      width: FormFieldWidth.FULL,
      options: [
        { label: 'Subscribe to newsletter', value: 'newsletter' },
        { label: 'Receive promotional emails', value: 'promo' }
      ],
      validationScheme: z.array(z.string()).optional()
    }
  ]
}

Using Grouped Forms

// pages/profile/UserProfile.tsx
import { useContext } from 'react'
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from '@n8x/react-form-utils'
import { userProfileForm } from '../../forms/userProfile'
import { updateUserProfile } from '../../service/user-service'

export default function UserProfilePage() {
  const { data, execute, error, isLoading } = useN8xFormQuery(updateUserProfile)

  const handleProfileUpdate = async (formData: any) => {
    execute(formData)
  }

  return (
    <div className="w-full p-6">
      <h1 className="text-3xl font-bold mb-6">Edit Profile</h1>
      
      <N8xForm 
        fields={userProfileForm}
        name="userProfileForm"
        onSubmit={handleProfileUpdate}
        defaultFieldStyles={EDefaultFieldStyles.SOFT_GLASS}
      >
        <button 
          disabled={isLoading} 
          type="submit"
          className="col-span-4 bg-blue-500 disabled:bg-zinc-400 text-white py-2 px-4 rounded font-medium"
        >
          {isLoading ? 'Saving...' : 'Save Changes'}
        </button>
      </N8xForm>

      {error && <p className="text-red-500 mt-4">{error}</p>}
    </div>
  )
}

Form Submission

useN8xFormQuery Hook

Execute async operations when the form is submitted. Perfect for API calls.

import { useN8xFormQuery } from '@n8x/react-form-utils'
import { login } from '../service/auth-service'

export default function LoginComponent() {
  const { data, execute, error, isLoading } = useN8xFormQuery(login)

  const handleSubmit = async (formData: any) => {
    execute(formData) // Calls the login service
  }

  return (
    <N8xForm 
      fields={loginForm}
      name="loginForm"
      onSubmit={handleSubmit}
    >
      <button disabled={isLoading} type="submit">
        {isLoading ? 'Loading...' : 'Login'}
      </button>
    </N8xForm>
  )
}

Hook Signature

const { data, execute, error, isLoading } = useN8xFormQuery(
  queryService: (payload: any, signal?: AbortSignal) => Promise<any>
)

Properties:

  • data: Response data from the service after successful execution
  • execute: Function to call the service with form payload
  • error: Error message if the request fails
  • isLoading: Boolean indicating if the request is in progress

Service Function Example

// service/auth-service.ts
import axios from 'axios'

export const login = async (payload: any, signal?: AbortSignal) => {
  return axios.post('/api/auth/login', payload, { signal })
}

export const register = async (payload: any, signal?: AbortSignal) => {
  return axios.post('/api/auth/register', payload, { signal })
}

Complete Submission Example with Side Effects

import { useContext, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from '@n8x/react-form-utils'
import { AuthContext } from '../context/AuthContext'
import { loginForm } from '../forms/login'
import { login } from '../service/auth-service'

export default function LoginPage() {
  const { setUser } = useContext(AuthContext)
  const navigate = useNavigate()
  const { data, execute, error, isLoading } = useN8xFormQuery(login)

  const handleLoginSubmit = async (formData: any) => {
    execute(formData)
  }

  // Handle successful login
  useEffect(() => {
    if (data) {
      setUser(data)
      navigate('/home')
    }
  }, [data, setUser, navigate])

  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="w-full max-w-md">
        <h1 className="text-4xl font-bold mb-6">Login</h1>
        
        <N8xForm 
          fields={loginForm}
          name="loginForm"
          onSubmit={handleLoginSubmit}
          defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
        >
          <button 
            disabled={isLoading}
            type="submit"
            className="col-span-4 bg-blue-500 hover:bg-blue-600 disabled:bg-zinc-400 text-white py-2 rounded font-medium transition"
          >
            {isLoading ? 'Logging in...' : 'Login'}
          </button>
        </N8xForm>

        {error && <p className="text-red-500 text-center mt-4">{error}</p>}
      </div>
    </div>
  )
}

Data Fetching Hooks

useN8xQuery Hook

Auto-execute queries on component mount. Use this for fetching initial data to populate forms.

import { useN8xQuery } from '@n8x/react-form-utils'
import { fetchUserProfile } from '../service/user-service'

export default function ProfilePage() {
  const { data: profile, error, isLoading } = useN8xQuery(fetchUserProfile, userId)

  if (isLoading) return <div>Loading profile...</div>
  if (error) return <div>Error: {error}</div>

  return (
    <N8xForm 
      fields={userProfileForm}
      name="userProfileForm"
      onSubmit={handleProfileUpdate}
    />
  )
}

Hook Signature

const { data, error, isLoading } = useN8xQuery(
  queryService: (payload: any, signal?: AbortSignal) => Promise<any>,
  ...queryParams: any[]
)

Properties:

  • data: Response data from the service
  • error: Error message if the request fails
  • isLoading: Boolean indicating if the request is loading

Key Differences

| Feature | useN8xFormQuery | useN8xQuery | |---------|-----------------|------------| | Runs on mount | ❌ No | ✅ Yes | | Triggered by | Manual call | Auto on mount | | Return state | data, error, isLoading, execute | data, error, isLoading | | Use case | Form submissions | Initial data fetch |


Save Form Progress

N8x Form Utils includes built-in automatic form progress saving to localStorage. This feature allows you to save individual field values automatically, enabling users to resume filling out forms even after browser refresh or accidental navigation.

How It Works

  • Fields with saveProgress: true are automatically saved to localStorage
  • Saves are debounced (2 second delay) for optimal performance
  • Saved values are automatically restored when the form mounts
  • Saved progress is automatically cleared after successful form submission
  • Works per-field (granular control) — only fields you mark will be saved

Basic Usage

Simply add saveProgress: true to any field configuration:

export const checkoutForm: FormFields[] = [
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    label: 'Email Address',
    placeholder: '[email protected]',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().email('Invalid email'),
    saveProgress: true  // Enable progress saving for this field
  },
  {
    name: 'cardNumber',
    type: FormFieldTypes.TEXT,
    label: 'Card Number',
    placeholder: '1234 5678 9012 3456',
    required: true,
    width: FormFieldWidth.FULL,
    saveProgress: true  // Save this field too
  },
  {
    name: 'cardCVC',
    type: FormFieldTypes.TEXT,
    label: 'CVC',
    placeholder: '123',
    required: true,
    width: FormFieldWidth.QUARTER,
    saveProgress: true
  }
]

Use Cases

1. Long Forms - Multi-step checkout, complex registration forms

// Save critical fields in a lengthy form
export const registrationForm: FormFields[] = [
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().email(),
    saveProgress: true  // Don't make users re-enter email
  },
  {
    name: 'fullName',
    type: FormFieldTypes.TEXT,
    required: true,
    width: FormFieldWidth.FULL,
    saveProgress: true
  },
  // ... more fields
]

2. Time-Consuming Forms - Prevent data loss on accidental refresh

export const surveyForm: FormFields[] = [
  {
    name: 'feedback',
    type: FormFieldTypes.TEXTAREA,
    label: 'Your Feedback',
    placeholder: 'Share your thoughts...',
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(10),
    saveProgress: true  // Auto-save in case of browser crash
  }
]

3. Selective Saving - Only save sensitive data that takes time to re-enter

export const profileUpdateForm: FormFields[] = [
  {
    name: 'username',
    type: FormFieldTypes.TEXT,
    required: true,
    saveProgress: true  // Save username
  },
  {
    name: 'bio',
    type: FormFieldTypes.TEXTAREA,
    saveProgress: true  // Save bio
  },
  {
    name: 'profilePicture',
    type: FormFieldTypes.FILE,
    // No saveProgress — files aren't saved to localStorage
  },
  {
    name: 'securityToken',
    type: FormFieldTypes.TEXT,
    // No saveProgress — security tokens should not be persisted
  }
]

Complete Example: Checkout Form with Progress Saving

// forms/checkout.ts
import { z } from '@n8x/react-form-utils'
import { FormFieldTypes, FormFieldWidth, type FormFields } from '@n8x/react-form-utils'

export const checkoutForm: FormFields[] = [
  // Billing Information (all saved)
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    label: 'Email Address',
    placeholder: '[email protected]',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().email('Invalid email'),
    saveProgress: true
  },
  {
    name: 'fullName',
    type: FormFieldTypes.TEXT,
    label: 'Full Name',
    placeholder: 'John Doe',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(2, 'Name too short'),
    saveProgress: true
  },
  {
    name: 'address',
    type: FormFieldTypes.TEXT,
    label: 'Street Address',
    placeholder: '123 Main St',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(5),
    saveProgress: true
  },
  {
    name: 'city',
    type: FormFieldTypes.TEXT,
    label: 'City',
    placeholder: 'New York',
    required: true,
    width: FormFieldWidth.HALF,
    validationScheme: z.string().min(2),
    saveProgress: true
  },
  {
    name: 'zipCode',
    type: FormFieldTypes.TEXT,
    label: 'ZIP Code',
    placeholder: '10001',
    required: true,
    width: FormFieldWidth.HALF,
    validationScheme: z.string().min(5),
    saveProgress: true
  },
  // Payment Information (selectively saved)
  {
    name: 'cardNumber',
    type: FormFieldTypes.TEXT,
    label: 'Card Number',
    placeholder: '1234 5678 9012 3456',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().length(16),
    saveProgress: true
  },
  {
    name: 'cardExpiry',
    type: FormFieldTypes.TEXT,
    label: 'Expiry Date',
    placeholder: 'MM/YY',
    required: true,
    width: FormFieldWidth.HALF,
    validationScheme: z.string().length(5),
    saveProgress: true
  },
  {
    name: 'cardCVC',
    type: FormFieldTypes.TEXT,
    label: 'CVC',
    placeholder: '123',
    required: true,
    width: FormFieldWidth.HALF,
    validationScheme: z.string().length(3),
    // NOT saving CVC for security reasons
    saveProgress: false
  }
]

Using with Component

// pages/checkout/Checkout.tsx
import { useState } from 'react'
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from '@n8x/react-form-utils'
import { checkoutForm } from '../../forms/checkout'
import { processPayment } from '../../service/payment-service'

export default function CheckoutPage() {
  const { data, execute, error, isLoading } = useN8xFormQuery(processPayment)
  const [isRecovering, setIsRecovering] = useState(false)

  const handleCheckoutSubmit = async (formData: any) => {
    setIsRecovering(false)
    execute(formData)
  }

  return (
    <div className="w-full max-w-2xl mx-auto p-6">
      <div className="mb-6">
        <h1 className="text-3xl font-bold mb-2">Checkout</h1>
        {isRecovering && (
          <div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
            <p className="text-sm text-blue-700">
              ✓ Your previous form data has been restored
            </p>
          </div>
        )}
      </div>

      <N8xForm 
        fields={checkoutForm}
        name="checkoutForm"
        onSubmit={handleCheckoutSubmit}
        defaultFieldStyles={EDefaultFieldStyles.SOFT_GLASS}
      >
        <button 
          disabled={isLoading}
          type="submit"
          className="col-span-4 bg-blue-500 hover:bg-blue-600 disabled:bg-zinc-400 text-white py-3 px-4 rounded-lg font-semibold transition"
        >
          {isLoading ? 'Processing...' : 'Complete Purchase'}
        </button>
      </N8xForm>

      {error && (
        <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
          <p className="text-red-700 font-semibold">Payment failed</p>
          <p className="text-red-600 text-sm mt-1">{error}</p>
        </div>
      )}
    </div>
  )
}

Storage Details

  • Storage Key: Form values are stored using the form's name prop as the key
  • Storage Limit: Respects browser localStorage limits (~5-10MB per domain)
  • Format: Data stored as JSON object: { fieldName: fieldValue, ... }
  • Persistence: Data persists across browser sessions until form submission
  • Clearing: Automatic deletion after successful submit (deleteLocalStorage called internally)

Best Practices

  1. Don't Save Sensitive Data

    // ❌ Bad: Saving sensitive information
    saveProgress: true  // For password fields? No!
       
    // ✅ Good: Skip security-sensitive fields
    {
      name: 'cvv',
      type: FormFieldTypes.TEXT,
      saveProgress: false  // Omit for sensitive data
    }
  2. Use for User Convenience

    // ✅ Good: Save fields that take time to fill
    {
      name: 'biography',
      type: FormFieldTypes.TEXTAREA,
      saveProgress: true
    }
       
    // ✅ Good: Save contact information
    {
      name: 'email',
      type: FormFieldTypes.EMAIL,
      saveProgress: true
    }
  3. Monitor Performance

    • Progress saving uses a 1-second debounce
    • Multiple field changes within 1 second are batched into a single save
    • Minimal performance impact for typical forms
  4. Test Data Loss Scenarios

    // Simulate recovery by refreshing browser mid-form fill
    // Your data should persist and be restored automatically

Important Notes

  • localStorage Cleared on Submit: All saved progress for a form is automatically deleted when the form is successfully submitted
  • Per-Field Control: You have granular control — enable it only for fields that benefit from it
  • No Index DB Support: Currently uses localStorage only; large files cannot be saved
  • Browser Dependent: Data is stored per browser/device; not synced across devices
  • Private Browsing: May not work in private/incognito mode depending on browser settings

Error Handling

Display Validation Errors

Validation errors from Zod are automatically displayed below each field with error styling.

<N8xForm 
  fields={loginForm}
  name="loginForm"
  onSubmit={handleSubmit}
/>
// Errors appear in real-time as user types (default: "onChange" mode)

Display API Errors

Handle errors returned from useN8xFormQuery:

const { data, execute, error, isLoading } = useN8xFormQuery(login)

return (
  <>
    <N8xForm 
      fields={loginForm}
      name="loginForm"
      onSubmit={handleSubmit}
    />
    
    {error && (
      <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded">
        <p className="text-red-700 font-semibold">Login failed</p>
        <p className="text-red-600 text-sm">{error}</p>
      </div>
    )}
  </>
)

Custom Error Component

interface ErrorProps {
  message: string
  onDismiss?: () => void
}

function ErrorAlert({ message, onDismiss }: ErrorProps) {
  return (
    <div className="p-4 bg-red-50 border-l-4 border-red-500 rounded">
      <p className="text-red-700">{message}</p>
      {onDismiss && (
        <button onClick={onDismiss} className="mt-2 text-sm text-red-600">
          Dismiss
        </button>
      )}
    </div>
  )
}

export default function LoginPage() {
  const { data, execute, error, isLoading } = useN8xFormQuery(login)
  const [dismissError, setDismissError] = React.useState(false)

  return (
    <>
      {error && !dismissError && (
        <ErrorAlert 
          message={error}
          onDismiss={() => setDismissError(true)}
        />
      )}
      <N8xForm 
        fields={loginForm}
        name="loginForm"
        onSubmit={(data) => {
          setDismissError(false)
          execute(data)
        }}
      />
    </>
  )
}

Error Recovery

const [errorKey, setErrorKey] = React.useState(0)

const handleRetry = () => {
  setErrorKey(prev => prev + 1) // Force form re-render
}

return (
  <>
    {error && (
      <button onClick={handleRetry} className="text-blue-600">
        Retry
      </button>
    )}
    <N8xForm 
      key={errorKey}
      fields={loginForm}
      name="loginForm"
      onSubmit={handleSubmit}
    />
  </>
)

API Reference

N8xForm Component

The main form component that renders fields and handles submission.

interface FormProps {
  fields?: FormFields[] | GroupedFormFields
  name?: string
  defaultFieldStyles?: EDefaultFieldStyles | string
  onSubmit: (data: any) => void
  mode?: Mode
  children?: React.ReactNode
}

<N8xForm 
  fields={loginForm}
  name="loginForm"
  defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
  onSubmit={handleSubmit}
  mode="onBlur"
>
  <button type="submit">Submit</button>
</N8xForm>

Props:

  • fields: Array or object of FormFields to render
  • name: Unique identifier for the form (used for localStorage progress saving)
  • defaultFieldStyles: Theme from EDefaultFieldStyles or custom Tailwind string
  • onSubmit: Callback when form is successfully submitted
  • mode: React Hook Form validation mode ('onChange', 'onBlur', 'onTouched', 'onSubmit', 'all')
  • children: Additional JSX elements (e.g., submit button, custom elements)

FormFields Type

Configuration for a single form field.

type FormFields = {
  name: string                                    // Unique field identifier
  type: FormFieldTypes                           // Input type
  validationScheme?: ZodTypeAny                 // Zod validation schema
  label?: string                                 // Display label
  placeholder?: string                          // Input placeholder
  options?: { label: string; value: string }[]  // For select/radio/checkbox
  min?: number                                  // Min value for number fields
  max?: number                                  // Max value for number fields
  required?: boolean                            // Is field required?
  width?: FormFieldWidth                        // Grid column span
  watch?: boolean                               // Watch for changes
  default?: string                              // Default value
  saveProgress?: boolean                        // Auto-save to localStorage on change
  _className?: string                           // Custom Tailwind classes
}

FormFieldTypes Enum

enum FormFieldTypes {
  TEXT = 'text'
  NUMBER = 'number'
  EMAIL = 'email'
  PASSWORD = 'password'
  SELECT = 'select'
  RADIO = 'radio'
  CHECKBOX = 'checkbox'
  TEXTAREA = 'textarea'
  DATE = 'date'
  FILE = 'file'
  SUBMIT = 'submit'
}

FormFieldWidth Enum

enum FormFieldWidth {
  FULL = 'col-span-4'    // 100% width
  HALF = 'col-span-2'    // 50% width
  THIRD = 'col-span-3'   // 75% width
  QUARTER = 'col-span-1' // 25% width
}

EDefaultFieldStyles Enum

enum EDefaultFieldStyles {
  CLEAN        // Minimal white inputs
  SOFT_GLASS   // Glassmorphism (default)
  DARK         // Dark mode inputs
  FLOATING_LABEL // Material Design floating labels
}

useN8xFormQuery Hook

const {
  data,      // Response data after successful request
  execute,   // Function to execute the query
  error,     // Error message string or null
  isLoading  // Boolean loading state
} = useN8xFormQuery(
  queryService: (payload: any, signal?: AbortSignal) => Promise<any>
)

Example:

const { data, execute, error, isLoading } = useN8xFormQuery(login)

// Later in form submission:
const handleSubmit = (formData: any) => {
  execute(formData)
}

useN8xQuery Hook

const {
  data,      // Response data after successful request
  error,     // Error message string or null
  isLoading  // Boolean loading state
} = useN8xQuery(
  queryService: (payload: any, signal?: AbortSignal) => Promise<any>,
  ...queryParams: any[]
)

Example:

const { data, error, isLoading } = useN8xQuery(fetchUserProfile, userId)

FormContextType Interface

interface FormContextType {
  formUtils: ReturnType<typeof useForm>        // React Hook Form utilities
  setValidationSchema: (props: ZodSchema) => void // Update validation schema
  setMode: (props: Mode) => void                // Update validation mode
}

Complete Real-World Example

Here's a complete registration flow combining everything:

// forms/registration.ts
import { z } from '@n8x/react-form-utils'
import { FormFieldTypes, FormFieldWidth, type FormFields } from '@n8x/react-form-utils'

export const registrationForm: FormFields[] = [
  {
    name: 'email',
    type: FormFieldTypes.EMAIL,
    label: 'Email Address',
    placeholder: '[email protected]',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().email('Invalid email address')
  },
  {
    name: 'password',
    type: FormFieldTypes.PASSWORD,
    label: 'Password',
    placeholder: 'Create a strong password',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string()
      .min(6, 'Password must be at least 6 characters')
      .max(36, 'Password too long')
  },
  {
    name: 'confirmPassword',
    type: FormFieldTypes.PASSWORD,
    label: 'Confirm Password',
    placeholder: 'Re-enter your password',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string()
      .min(6, 'Password must match')
  },
  {
    name: 'fullName',
    type: FormFieldTypes.TEXT,
    label: 'Full Name',
    placeholder: 'John Doe',
    required: true,
    width: FormFieldWidth.FULL,
    validationScheme: z.string().min(2, 'Name too short')
  },
  {
    name: 'accountType',
    type: FormFieldTypes.RADIO,
    label: 'Account Type',
    required: true,
    width: FormFieldWidth.FULL,
    options: [
      { label: 'Personal', value: 'personal' },
      { label: 'Business', value: 'business' }
    ],
    validationScheme: z.string()
  },
  {
    name: 'interests',
    type: FormFieldTypes.CHECKBOX,
    label: 'Select Your Interests',
    width: FormFieldWidth.FULL,
    options: [
      { label: 'Technology', value: 'tech' },
      { label: 'Finance', value: 'finance' },
      { label: 'Health', value: 'health' }
    ],
    validationScheme: z.array(z.string()).min(1, 'Select at least one interest')
  },
  {
    name: 'terms',
    type: FormFieldTypes.CHECKBOX,
    label: 'Agreement',
    required: true,
    width: FormFieldWidth.FULL,
    options: [
      { label: 'I agree to Terms and Conditions', value: 'agreed' }
    ],
    validationScheme: z.array(z.string()).min(1, 'You must agree to terms')
  }
]

// pages/auth/Register.tsx
import { useContext, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { N8xForm, useN8xFormQuery, EDefaultFieldStyles } from '@n8x/react-form-utils'
import { AuthContext, type IAuthContext } from '../../context/AuthContext/context'
import { registrationForm } from '../../forms/registration'
import { register } from '../../service/auth-service'

export default function RegisterPage() {
  const { user, setUser } = useContext(AuthContext) as IAuthContext
  const navigate = useNavigate()
  const [dismissError, setDismissError] = useState(false)
  const { data, execute, error, isLoading } = useN8xFormQuery(register)

  const handleRegisterSubmit = async (formData: any) => {
    // Validate passwords match
    if (formData.password !== formData.confirmPassword) {
      // You could set a custom error here or let Zod handle it
      return
    }
    setDismissError(false)
    execute(formData)
  }

  // Handle successful registration
  useEffect(() => {
    if (data) {
      setUser(data)
      navigate('/home')
    }
  }, [data, setUser, navigate])

  // Redirect if already logged in
  useEffect(() => {
    if (user) {
      navigate('/home')
    }
  }, [user, navigate])

  return (
    <div className="w-full min-h-screen flex justify-center items-center bg-gradient-to-br from-zinc-50 to-zinc-100 p-4">
      <div className="md:w-[600px] w-full">
        <div className="mb-8">
          <h1 className="text-4xl font-bold tracking-tight mb-2">Create Your Account</h1>
          <p className="text-zinc-600">Join us today and get started</p>
        </div>

        {error && !dismissError && (
          <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
            <p className="text-red-700 font-semibold">Registration failed</p>
            <p className="text-red-600 text-sm mt-1">{error}</p>
            <button 
              onClick={() => setDismissError(true)}
              className="mt-2 text-xs text-red-600 hover:text-red-700"
            >
              Dismiss
            </button>
          </div>
        )}

        <N8xForm 
          fields={registrationForm}
          name="registrationForm"
          onSubmit={handleRegisterSubmit}
          defaultFieldStyles={EDefaultFieldStyles.FLOATING_LABEL}
        >
          <button 
            disabled={isLoading}
            type="submit"
            className={`col-span-4 py-3 px-4 rounded-lg font-semibold text-white transition-all ${
              isLoading 
                ? 'bg-zinc-400 cursor-not-allowed' 
                : 'bg-blue-500 hover:bg-blue-600 active:bg-blue-700'
            }`}
          >
            {isLoading ? (
              <span className="inline-flex items-center gap-2">
                <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
                </svg>
                Creating account...
              </span>
            ) : (
              'Create Account'
            )}
          </button>
        </N8xForm>

        <p className="text-center text-sm text-zinc-600 mt-6">
          Already have an account?{' '}
          <Link to="/login" className="text-blue-500 hover:text-blue-600 font-semibold">
            Login here
          </Link>
        </p>
      </div>
    </div>
  )
}

Browser Support

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

License

ISC License - Created by Syed Shayan Ali

Contributing

This project is currently not open for external contributions. It will be made open source very soon in the future once it has matured and gained wider adoption.