New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

react-smart-form-state

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-smart-form-state

A powerful React form library with declarative DSL for complex validation and conditional logic

latest
Source
npmnpm
Version
0.4.0
Version published
Maintainers
1
Created
Source

🧠 React Smart Form State

A powerful, declarative React form library with a fluent DSL for building complex conditional logic, validation, and computed fields.

✨ Features

  • 🎯 Declarative DSL - Express complex form logic in readable, chainable syntax
  • Built-in Validation - Email, age, regex patterns, and custom validators
  • 🔗 Conditional Logic - Complex AND/OR conditions with multiple field dependencies
  • 🎬 Multiple Actions - Chain multiple actions (disable, enable, setValue, setError)
  • 🧮 Computed Fields - Auto-calculate values based on other fields
  • 🔄 Form State Management - Built-in reset, validate, isDirty, isValid
  • 📊 Error Handling - Per-field error messages with automatic validation
  • 🎨 TypeScript - Full type safety and IntelliSense support
  • 🪶 Lightweight - Zero dependencies, small bundle size

📦 Installation

npm install react-smart-form-state
# or
pnpm add react-smart-form-state
# or
yarn add react-smart-form-state

🚀 Quick Start

import { useSmartForm, when } from 'react-smart-form-state'

function MyForm() {
  const form = useSmartForm({
    initialValues: {
      email: '',
      age: '',
      country: ''
    },
    // Debounce validation - improves UX and reduces unnecessary validation
    debounce: {
      email: 500,  // Wait 500ms after user stops typing
      age: 300
    },
    rules: [
      // Email validation
      when('email')
        .matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
        .clearError('email')
        .else()
        .setError('email', 'Invalid email address'),

      // Age validation with conditional logic
      when('age')
        .gte(18)
        .and('country')
        .eq('US')
        .enable('submit')
        .else()
        .disable('submit')
        .setError('submit', 'Must be 18+ and from US')
    ]
  })

  return (
    <form>
      <input {...form.field('email')} />
      {form.getError('email') && <p>{form.getError('email')}</p>}

      <input type="number" {...form.field('age')} />
      
      <select {...form.field('country')}>
        <option value="">Select</option>
        <option value="US">United States</option>
        <option value="VN">Vietnam</option>
      </select>

      <button disabled={!form.isValid()}>Submit</button>
      <button type="button" onClick={form.reset}>Reset</button>
    </form>
  )
}

📚 API Reference

useSmartForm(config)

Main hook for creating a smart form instance.

Config Options:

{
  initialValues: Record<string, any>  // Initial form values
  rules: Rule[]                       // Array of validation/logic rules
  debug?: boolean                     // Enable console logging
  debounce?: number | Record<string, number>  // Debounce validation (ms)
  onSubmit?: (values: Record<string, any>) => void | Promise<void>  // Form submission handler
  onSubmitError?: (error: any) => void  // Error handler for submission failures
}

Debounce Options:

// Global debounce - apply to all fields
debounce: 500

// Per-field debounce - granular control
debounce: {
  email: 500,      // Wait 500ms after user stops typing
  password: 300,   // Wait 300ms
  username: 1000   // Wait 1s (useful for async checks)
}

Returns:

{
  // Field binding
  field(name: string): { value, disabled, readOnly, onChange }
  
  // State
  values: Record<string, any>
  fields: Record<string, FieldState>
  errors: Record<string, string | null>
  touched: Record<string, boolean>
  dirty: Record<string, boolean>
  validating: Record<string, boolean>  // Async validation loading state
  isSubmitting: boolean                // Form submission in progress
  isSubmitted: boolean                 // Form was submitted successfully
  submitCount: number                  // Number of submission attempts
  
  // Methods
  isValid(): boolean
  isDirty(): boolean
  isDisabled(name: string): boolean
  isHidden(name: string): boolean
  isValidating(name?: string): boolean  // Check if field or form is validating
  getError(name: string): string | null
  validate(): boolean
  reset(): void
  handleSubmit(e?: React.FormEvent): void  // Form submission handler
  getValues(): Record<string, any>
  setValues(values: Record<string, any>): void
}

DSL - Condition Operators

Comparison Operators

when('field').lt(value)         // Less than
when('field').lte(value)        // Less than or equal
when('field').gt(value)         // Greater than
when('field').gte(value)        // Greater than or equal
when('field').eq(value)         // Equal
when('field').neq(value)        // Not equal

Array/String Operators

when('field').in(['a', 'b'])    // Value in array
when('field').notIn(['x', 'y']) // Value not in array
when('field').contains('text')  // String contains
when('field').matches(/regex/)  // Regex pattern match

Empty/Validation Operators

when('field').isEmpty()         // Field is empty
when('field').isNotEmpty()      // Field is not empty
when('field').require()         // Field is required (empty check)
when('field').changed()         // Field was just changed

Custom & Cross-Field Validators

// Custom validator - define your own validation logic
when('password').custom(
  (value, allValues) => {
    const hasLength = value.length >= 8
    const hasLetter = /[a-zA-Z]/.test(value)
    const hasNumber = /\d/.test(value)
    return hasLength && hasLetter && hasNumber
  }
)

// Cross-field validation - compare with another field
when('confirmPassword').equalsTo('password')      // Must equal
when('confirmPassword').notEqualsTo('password')   // Must not equal
when('endDate').gte('startDate')                  // Works with any operator

Async Validators

// Async validation - check via API
when('username').asyncValidate(async (value, allValues) => {
  if (!value || value.length < 3) return null
  
  // Call your API
  const response = await fetch(`/api/check-username?username=${value}`)
  const data = await response.json()
  
  // Return error message or null
  return data.exists ? 'Username already taken' : null
})

// Benefits:
// ✅ Automatic debouncing (uses debounce config)
// ✅ Loading state tracking with isValidating()
// ✅ Automatic cleanup on unmount

DSL - Logical Operators

// AND - all conditions must be true
when('age').gte(18)
  .and('country').eq('US')
  .and('email').isNotEmpty()
  .enable('submit')

// OR - at least one condition must be true
when('role').eq('admin')
  .or('permissions').contains('write')
  .enable('editButton')

DSL - Actions

// Enable/Disable
.enable('fieldName')
.disable('fieldName')

// Set Value
.setValue('fieldName', value)

// Error Management
.setError('fieldName', 'Error message')
.clearError('fieldName')

DSL - Action Chaining

Chain multiple actions in sequence:

when('vip').eq(true)
  .enable('premiumFeatures')
  .setValue('discount', 20)
  .clearError('payment')
  .else()
  .disable('premiumFeatures')
  .setValue('discount', 0)
  .setError('payment', 'VIP required')

DSL - Else Branch

when('age').gte(18)
  .enable('submit')
  .else()
  .disable('submit')
  .setError('submit', 'Must be 18+')

Computed Fields

Auto-calculate field values based on other fields:

import { compute } from 'react-smart-form-state'

compute(
  ['price', 'quantity'],           // Dependencies
  (ctx) => ctx.values.price * ctx.values.quantity,  // Calculator function
  'total'                           // Target field
)

🎯 Real-World Examples

Example 1: Registration Form

const form = useSmartForm({
  initialValues: {
    email: '',
    password: '',
    confirmPassword: '',
    age: '',
    terms: false
  },
  rules: [
    // Email validation
    when('email')
      .matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
      .clearError('email')
      .else()
      .setError('email', 'Please enter a valid email'),

    // Password strength with custom validator
    when('password')
      .custom((value) => {
        const hasLength = value.length >= 8
        const hasLetter = /[a-zA-Z]/.test(value)
        const hasNumber = /\d/.test(value)
        return hasLength && hasLetter && hasNumber
      })
      .clearError('password')
      .else()
      .setError('password', 'Password must be 8+ chars with letters and numbers'),

    // Cross-field validation: passwords must match
    when('confirmPassword')
      .equalsTo('password')
      .clearError('confirmPassword')
      .else()
      .setError('confirmPassword', 'Passwords do not match'),

    // Age verification
    when('age')
      .gte(18)
      .clearError('age')
      .else()
      .setError('age', 'Must be 18 or older'),

    // Enable submit only if valid
    when('email')
      .isNotEmpty()
      .and('password')
      .isNotEmpty()
      .and('age')
      .gte(18)
      .and('terms')
      .eq(true)
      .enable('submitButton')
      .else()
      .disable('submitButton')
  ]
})

Example 2: E-commerce Checkout

const form = useSmartForm({
  initialValues: {
    itemPrice: 100,
    quantity: 1,
    shippingMethod: 'standard',
    shippingCost: 0,
    vipMember: false,
    discount: 0,
    total: 0
  },
  rules: [
    // Shipping cost based on method
    when('shippingMethod')
      .eq('express')
      .setValue('shippingCost', 20)
      .else()
      .setValue('shippingCost', 5),

    // VIP discount
    when('vipMember')
      .eq(true)
      .setValue('discount', 15)
      .else()
      .setValue('discount', 0),

    // Calculate total
    compute(
      ['itemPrice', 'quantity', 'shippingCost', 'discount'],
      (ctx) => {
        const subtotal = ctx.values.itemPrice * ctx.values.quantity
        const shipping = ctx.values.shippingCost
        const discount = subtotal * (ctx.values.discount / 100)
        return subtotal + shipping - discount
      },
      'total'
    ),

    // Minimum order validation
    when('total')
      .lt(50)
      .setError('total', 'Minimum order is $50')
      .disable('checkout')
      .else()
      .clearError('total')
      .enable('checkout')
  ]
})

Example 3: Dynamic Form Fields

const form = useSmartForm({
  initialValues: {
    userType: 'individual',
    companyName: '',
    taxId: '',
    firstName: '',
    lastName: ''
  },
  rules: [
    // Show/require business fields for business users
    when('userType')
      .eq('business')
      .enable('companyName')
      .enable('taxId')
      .else()
      .disable('companyName')
      .disable('taxId'),

    // Validate business fields when enabled
    when('userType')
      .eq('business')
      .and('companyName')
      .isEmpty()
      .setError('companyName', 'Company name is required')
  ]
})

Example 4: Date Range Validation

const form = useSmartForm({
  initialValues: {
    startDate: '',
    endDate: '',
    duration: 0
  },
  rules: [
    // End date must be after start date
    when('startDate')
      .isNotEmpty()
      .and('endDate')
      .isNotEmpty()
      .and('endDate')
      .custom((endDate, values) => {
        return new Date(endDate) >= new Date(values.startDate)
      })
      .clearError('endDate')
      .else()
      .setError('endDate', 'End date must be after start date'),

    // Calculate duration in days
    compute(
      ['startDate', 'endDate'],
      (ctx) => {
        if (!ctx.values.startDate || !ctx.values.endDate) return 0
        const start = new Date(ctx.values.startDate)
        const end = new Date(ctx.values.endDate)
        const diff = end.getTime() - start.getTime()
        return Math.ceil(diff / (1000 * 60 * 60 * 24))
      },
      'duration'
    )
  ]
})

Example 5: Search with Debounce

const form = useSmartForm({
  initialValues: {
    searchQuery: '',
    minLength: 3
  },
  // Debounce search validation to avoid excessive checks
  debounce: {
    searchQuery: 500  // Wait 500ms after user stops typing
  },
  rules: [
    // Validate search query length
    when('searchQuery')
      .isNotEmpty()
      .and('searchQuery')
      .custom((value) => value.length >= 3)
      .clearError('searchQuery')
      .else()
      .setError('searchQuery', 'Search query must be at least 3 characters'),

    // In real app, you could trigger API search here
    when('searchQuery')
      .custom((value) => value.length >= 3)
      .setValue('resultsCount', 0)  // Would be from API
  ]
})

// Benefits of debounce:
// ✅ Better UX - no flickering errors while typing
// ✅ Performance - reduces validation calls
// ✅ API friendly - fewer requests to server

Example 6: Username Availability Check

const form = useSmartForm({
  initialValues: {
    username: ''
  },
  debounce: {
    username: 800  // Wait 800ms for async validation
  },
  rules: [
    // Async validation to check username availability
    when('username')
      .asyncValidate(async (value) => {
        if (!value || value.length < 3) return null
        
        try {
          // Call your API
          const response = await fetch(`/api/check-username?username=${value}`)
          const data = await response.json()
          
          // Return error message or null
          return data.exists ? 'Username already taken' : null
        } catch (err) {
          return 'Failed to check username'
        }
      })
  ]
})

// In your component:
<div>
  <label>
    Username {form.isValidating('username') && <span>⏳ Checking...</span>}
  </label>
  <input {...form.field('username')} />
  {form.getError('username') && <p>{form.getError('username')}</p>}
  {!form.getError('username') && form.touched.username && !form.isValidating('username') && (
    <p style={{ color: 'green' }}>✓ Username available</p>
  )}
</div>

// Benefits:
// ✅ Automatic debouncing
// ✅ Loading state tracking
// ✅ Error handling
// ✅ Cancellation on unmount
  .setError('companyName', 'Company name is required'),

when('userType')
  .eq('business')
  .and('taxId')
  .isEmpty()
  .setError('taxId', 'Tax ID is required')

] })


## 🎨 Form Methods

### `validate()`

Manually trigger validation for all fields:

```tsx
<button onClick={() => {
  if (form.validate()) {
    // Form is valid, proceed with submission
    submitToAPI(form.getValues())
  } else {
    // Form has errors, show them
    console.log(form.errors)
  }
}}>
  Submit
</button>

🎯 Form Submission

handleSubmit()

Built-in form submission handler with automatic validation:

const form = useSmartForm({
  initialValues: { email: '', password: '' },
  onSubmit: async (values) => {
    // Called only if form is valid
    await api.post('/login', values)
  },
  onSubmitError: (error) => {
    // Handle submission errors
    alert('Submission failed: ' + error.message)
  },
  rules: [...]
})

return (
  <form onSubmit={form.handleSubmit}>
    <input {...form.field('email')} />
    <input type="password" {...form.field('password')} />
    
    <button 
      type="submit" 
      disabled={form.isSubmitting || !form.isValid()}
    >
      {form.isSubmitting ? 'Submitting...' : 'Submit'}
    </button>
  </form>
)

Submission State

Track submission status:

// Check if form is currently submitting
form.isSubmitting  // true/false

// Check if form was submitted successfully
form.isSubmitted  // true/false

// Get number of submission attempts
form.submitCount  // number

// Example: Show success message
{form.isSubmitted && (
  <div>Form submitted successfully!</div>
)}

Async Submission

The onSubmit callback supports both sync and async functions:

const form = useSmartForm({
  initialValues: {...},
  onSubmit: async (values) => {
    // Async API call
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(values)
    })
    
    if (!response.ok) {
      throw new Error('Submission failed')
    }
    
    return response.json()
  },
  onSubmitError: (error) => {
    console.error('Error:', error)
  }
})

validate()

Manually trigger validation:

<button onClick={() => {
  if (form.validate()) {
    // Form is valid, proceed
  } else {
    // Form has errors, show them
    console.log(form.errors)
  }
}}>
  Submit
</button>

reset()

Reset form to initial values and clear submission state:

<button onClick={form.reset}>
  Reset Form
</button>

Field Arrays

Dynamic array operations for managing lists of items:

arrayPush(fieldName, value)

Add an item to the end of an array:

<button onClick={() => form.arrayPush('todos', 'New task')}>
  Add Todo
</button>

arrayPop(fieldName)

Remove the last item from an array:

<button onClick={() => form.arrayPop('todos')}>
  Remove Last
</button>

arrayRemove(fieldName, index)

Remove an item at a specific index:

{form.values.todos.map((todo, index) => (
  <button onClick={() => form.arrayRemove('todos', index)}>
    Remove
  </button>
))}

arrayInsert(fieldName, index, value)

Insert an item at a specific index:

<button onClick={() => form.arrayInsert('todos', 0, 'First task')}>
  Add to Top
</button>

arrayMove(fieldName, fromIndex, toIndex)

Move an item from one index to another:

<button onClick={() => form.arrayMove('todos', 2, 0)}>
  Move to Top
</button>

arraySwap(fieldName, indexA, indexB)

Swap two items:

<button onClick={() => form.arraySwap('todos', 0, 1)}>
  Swap First Two
</button>

arrayReplace(fieldName, index, value)

Replace an item at a specific index:

<button onClick={() => form.arrayReplace('todos', 0, 'Updated task')}>
  Update First
</button>

Complete Example:

const form = useSmartForm({
  initialValues: {
    todos: ['Task 1', 'Task 2']
  },
  rules: []
})

return (
  <div>
    {form.values.todos.map((todo, index) => (
      <div key={index}>
        <span>{todo}</span>
        <button onClick={() => form.arrayRemove('todos', index)}>×</button>
        <button onClick={() => form.arrayMove('todos', index, index - 1)} disabled={index === 0}>↑</button>
        <button onClick={() => form.arrayMove('todos', index, index + 1)} disabled={index === form.values.todos.length - 1}>↓</button>
      </div>
    ))}
    <button onClick={() => form.arrayPush('todos', `Task ${form.values.todos.length + 1}`)}>
      Add Todo
    </button>
  </div>
)

isValid()

Check if entire form is valid:

<button disabled={!form.isValid()}>
  Submit
</button>

isDirty()

Check if form has been modified:

{form.isDirty() && (
  <div>You have unsaved changes</div>
)}

getValues() / setValues()

// Get all values
const values = form.getValues()

// Set multiple values
form.setValues({
  email: 'new@email.com',
  age: 25
})

🔧 Advanced Patterns

Conditional Required Fields

// Only require phone when contact method is phone
when('contactMethod')
  .eq('phone')
  .and('phoneNumber')
  .isEmpty()
  .setError('phoneNumber', 'Phone number is required')
  .else()
  .clearError('phoneNumber')

Cross-Field Validation

// Password confirmation
when('password')
  .isNotEmpty()
  .and('confirmPassword')
  .neq(form.values.password)  // Access other field value
  .setError('confirmPassword', 'Passwords must match')

Multi-Step Forms

when('step')
  .eq(1)
  .enable('nextButton1')
  .disable('nextButton2')
  .else()
  .disable('nextButton1')
  .enable('nextButton2')

🐛 Debugging

Enable debug mode to see form state in console:

const form = useSmartForm({
  debug: true,  // 👈 Enable debug logs
  initialValues: { ... },
  rules: [ ... ]
})

📝 TypeScript Support

Full TypeScript support with type inference:

import { useSmartForm, when, compute, Rule } from 'react-smart-form-state'

interface MyFormValues {
  email: string
  age: number
  country: string
}

const form = useSmartForm<MyFormValues>({
  initialValues: {
    email: '',
    age: 0,
    country: ''
  },
  rules: [ ... ]
})

// Type-safe access
const email: string = form.values.email
const age: number = form.values.age

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

MIT

🙏 Credits

Built with ❤️ by the React Smart Form State team.

Keywords

react

FAQs

Package last updated on 22 Jan 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts