🧠 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
pnpm add react-smart-form-state
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: {
email: 500,
age: 300
},
rules: [
when('email')
.matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
.clearError('email')
.else()
.setError('email', 'Invalid email address'),
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>
rules: Rule[]
debug?: boolean
debounce?: number | Record<string, number>
onSubmit?: (values: Record<string, any>) => void | Promise<void>
onSubmitError?: (error: any) => void
}
Debounce Options:
debounce: 500
debounce: {
email: 500,
password: 300,
username: 1000
}
Returns:
{
field(name: string): { value, disabled, readOnly, onChange }
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>
isSubmitting: boolean
isSubmitted: boolean
submitCount: number
isValid(): boolean
isDirty(): boolean
isDisabled(name: string): boolean
isHidden(name: string): boolean
isValidating(name?: string): boolean
getError(name: string): string | null
validate(): boolean
reset(): void
handleSubmit(e?: React.FormEvent): void
getValues(): Record<string, any>
setValues(values: Record<string, any>): void
}
DSL - Condition Operators
Comparison Operators
when('field').lt(value)
when('field').lte(value)
when('field').gt(value)
when('field').gte(value)
when('field').eq(value)
when('field').neq(value)
Array/String Operators
when('field').in(['a', 'b'])
when('field').notIn(['x', 'y'])
when('field').contains('text')
when('field').matches(/regex/)
Empty/Validation Operators
when('field').isEmpty()
when('field').isNotEmpty()
when('field').require()
when('field').changed()
Custom & Cross-Field Validators
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
}
)
when('confirmPassword').equalsTo('password')
when('confirmPassword').notEqualsTo('password')
when('endDate').gte('startDate')
Async Validators
when('username').asyncValidate(async (value, allValues) => {
if (!value || value.length < 3) return null
const response = await fetch(`/api/check-username?username=${value}`)
const data = await response.json()
return data.exists ? 'Username already taken' : null
})
DSL - Logical Operators
when('age').gte(18)
.and('country').eq('US')
.and('email').isNotEmpty()
.enable('submit')
when('role').eq('admin')
.or('permissions').contains('write')
.enable('editButton')
DSL - Actions
.enable('fieldName')
.disable('fieldName')
.setValue('fieldName', value)
.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'],
(ctx) => ctx.values.price * ctx.values.quantity,
'total'
)
🎯 Real-World Examples
Example 1: Registration Form
const form = useSmartForm({
initialValues: {
email: '',
password: '',
confirmPassword: '',
age: '',
terms: false
},
rules: [
when('email')
.matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
.clearError('email')
.else()
.setError('email', 'Please enter a valid email'),
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'),
when('confirmPassword')
.equalsTo('password')
.clearError('confirmPassword')
.else()
.setError('confirmPassword', 'Passwords do not match'),
when('age')
.gte(18)
.clearError('age')
.else()
.setError('age', 'Must be 18 or older'),
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: [
when('shippingMethod')
.eq('express')
.setValue('shippingCost', 20)
.else()
.setValue('shippingCost', 5),
when('vipMember')
.eq(true)
.setValue('discount', 15)
.else()
.setValue('discount', 0),
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'
),
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: [
when('userType')
.eq('business')
.enable('companyName')
.enable('taxId')
.else()
.disable('companyName')
.disable('taxId'),
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: [
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'),
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: {
searchQuery: 500
},
rules: [
when('searchQuery')
.isNotEmpty()
.and('searchQuery')
.custom((value) => value.length >= 3)
.clearError('searchQuery')
.else()
.setError('searchQuery', 'Search query must be at least 3 characters'),
when('searchQuery')
.custom((value) => value.length >= 3)
.setValue('resultsCount', 0)
]
})
Example 6: Username Availability Check
const form = useSmartForm({
initialValues: {
username: ''
},
debounce: {
username: 800
},
rules: [
when('username')
.asyncValidate(async (value) => {
if (!value || value.length < 3) return null
try {
const response = await fetch(`/api/check-username?username=${value}`)
const data = await response.json()
return data.exists ? 'Username already taken' : null
} catch (err) {
return 'Failed to check username'
}
})
]
})
<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>
.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) => {
await api.post('/login', values)
},
onSubmitError: (error) => {
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:
form.isSubmitting
form.isSubmitted
form.submitCount
{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) => {
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()) {
} else {
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()
const values = form.getValues()
form.setValues({
email: 'new@email.com',
age: 25
})
🔧 Advanced Patterns
Conditional Required Fields
when('contactMethod')
.eq('phone')
.and('phoneNumber')
.isEmpty()
.setError('phoneNumber', 'Phone number is required')
.else()
.clearError('phoneNumber')
Cross-Field Validation
when('password')
.isNotEmpty()
.and('confirmPassword')
.neq(form.values.password)
.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,
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: [ ... ]
})
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.