Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt

Use this file to discover all available pages before exploring further.

Form Management

Reatom’s form system provides a comprehensive solution for building complex forms with validation, field management, and seamless schema integration.

Core Concepts

Reatom forms are built on three main primitives:
  • Fields - Individual form inputs with validation and state tracking
  • Field Arrays - Dynamic lists of fields
  • Forms - Complete form systems with submit handling and validation

Quick Start

Basic Form

import { reatomForm } from '@reatom/core/form'
import { wrap, noop } from '@reatom/core'

const loginForm = reatomForm(
  {
    email: '',
    password: '',
  },
  {
    onSubmit: async (state) => {
      const res = await wrap(fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(state),
      }))
      return await wrap(res.json())
    },
  }
)

// Access fields
loginForm.fields.email.change('user@example.com')
loginForm.fields.password.change('secret')

// Submit
await wrap(loginForm.submit()).catch(noop)

if (loginForm.submit.error()) {
  console.log('Login failed')
}

Fields

Creating Fields

Automatic field creation:
const form = reatomForm({
  name: '',
  age: 0,
  active: false,
})

// Fields are created automatically
form.fields.name.change('John')
form.fields.age.change(25)
form.fields.active.change(true)

Field State

Each field tracks multiple states:
const emailField = form.fields.email

// Value and state
emailField.value()    // Current value
emailField()          // Internal state (usually same as value)

// Focus state
emailField.focus()    // { active: boolean, dirty: boolean, touched: boolean }
emailField.focus.in() // Focus the field
emailField.focus.out() // Blur the field

// Validation state
emailField.validation() // { errors: [], triggered: boolean, validating?: Promise }

// Actions
emailField.change('new@email.com') // Change value
emailField.reset()                  // Reset to initial value

Value Transformation

Transform values between display and storage:
import { reatomField } from '@reatom/core/form'

const priceField = reatomField<number, string>(100.5, {
  // Convert internal state to display value
  fromState: (state) => state.toFixed(2),
  
  // Convert display value to internal state
  toState: (value) => {
    const parsed = parseFloat(value)
    return isNaN(parsed) ? 0 : parsed
  },
})

priceField()        // → 100.5 (internal state)
priceField.value()  // → "100.50" (display value)

priceField.change('150.75')
priceField()        // → 150.75
priceField.value()  // → "150.75"

Field Arrays

Manage dynamic lists of fields:
import { reatomFieldArray, reatomField } from '@reatom/core/form'

const todoForm = reatomForm({
  todos: reatomFieldArray({
    initState: ['Buy milk'],
    create: (value: string) => reatomField(value),
  }),
})

// Add items
todoForm.fields.todos.create('Write docs')
todoForm.fields.todos.create('Ship feature')

// Access items
const items = todoForm.fields.todos.array()
items[0]?.change('Buy organic milk')

// Get size
todoForm.fields.todos().size // → 3

// Remove items
todoForm.fields.todos.clear()

Nested Field Arrays

interface TodoItem {
  text: string
  subtasks: string[]
}

const form = reatomForm({
  todos: reatomFieldArray({
    initState: [],
    create: () => reatomFieldSet({
      text: '',
      subtasks: reatomFieldArray({
        initState: [],
        create: (text: string) => reatomField(text),
      }),
    }),
  }),
})

// Add a todo
const todo = form.fields.todos.create()
todo.fields.text.change('Main task')

// Add subtasks
todo.fields.subtasks.create('Subtask 1')
todo.fields.subtasks.create('Subtask 2')

Validation

Field Validation

1
Define Validation Rules
2
const passwordField = reatomField('', {
  validate: ({ value }) => {
    if (value.length < 8) {
      return 'Password must be at least 8 characters'
    }
    if (!/[A-Z]/.test(value)) {
      return 'Password must contain uppercase letter'
    }
    if (!/[0-9]/.test(value)) {
      return 'Password must contain number'
    }
  },
})
3
Access Validation State
4
const validation = passwordField.validation()

validation.errors     // Array of error objects
validation.triggered  // Has validation been triggered?
validation.validating // Promise if async validation running
5
Trigger Validation
6
// Manual trigger
await wrap(passwordField.validation.trigger())

// Auto-trigger on change
const field = reatomField('', {
  validate: ({ value }) => { /* ... */ },
  validateOnChange: true,
})

// Auto-trigger on blur
const field = reatomField('', {
  validate: ({ value }) => { /* ... */ },
  validateOnBlur: true,
})

Form-Level Validation

const form = reatomForm(
  {
    password: '',
    confirmPassword: '',
  },
  {
    validateBeforeSubmit: (state) => {
      if (state.password !== state.confirmPassword) {
        throw new Error('Passwords do not match')
      }
    },
  }
)

Schema Validation

Integrate with Zod, Valibot, or any Standard Schema library:
import { z } from 'zod'

const registerForm = reatomForm(
  {
    email: '',
    age: 0,
    terms: false,
  },
  {
    schema: z.object({
      email: z.string().email('Invalid email address'),
      age: z.number().min(18, 'Must be 18 or older'),
      terms: z.boolean().refine(v => v, 'Must accept terms'),
    }),
    onSubmit: async (state) => {
      // state is typed as validated output
      console.log(state.email) // ✓ Valid email
      console.log(state.age)   // ✓ Number >= 18
    },
  }
)

Submit Handling

Basic Submit

const form = reatomForm(
  { email: '', password: '' },
  {
    onSubmit: async (state) => {
      const res = await wrap(fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(state),
      }))
      return await wrap(res.json())
    },
  }
)

// Submit returns a promise
const result = await wrap(form.submit())

Submit with Parameters

const form = reatomForm(
  { email: '' },
  {
    onSubmit: async (state, skipDebounce: boolean) => {
      if (!skipDebounce) await wrap(sleep(300))
      return { success: true, email: state.email }
    },
  }
)

// Call with custom params
const result = await wrap(form.submit(true))
console.log(form.submit.data()) // { success: true, email: '...' }

Submit State

The submit action is extended with withAsyncData:
// Loading state
form.submit.pending()  // Number of pending submissions
form.submit.ready()    // Is submission complete?

// Results
form.submit.data()     // Last successful result
form.submit.error()    // Last error

// Status tracking
form.submit.status()   // Detailed status information

// Submitted flag
form.submitted()       // Has form been submitted?

Submit Flow

When submit() is called:
1
Field Validation
2
All field validations are triggered in parallel.
3
Schema Validation
4
If a schema is provided, it validates the entire form state.
5
Form Validation
6
The validateBeforeSubmit callback runs with validated state.
7
Submit Handler
8
The onSubmit callback runs with validated state.
9
State Update
10
The submitted atom is set to true.
11
Optional Reset
12
If resetOnSubmit: true, the form resets to initial state.

Form State Management

Accessing Form State

const form = reatomForm({
  name: '',
  email: '',
  age: 0,
})

// Get all field values
const state = form()
console.log(state) // { name: '', email: '', age: 0 }

// Access individual fields
const name = form.fields.name()
const email = form.fields.email.value()

Focus Management

// Get focus state for entire form
const focus = form.focus()
focus.active  // Is any field focused?
focus.dirty   // Has any field changed?
focus.touched // Has any field been blurred?

// Focus individual fields
form.fields.email.focus.in()
form.fields.email.focus.out()

Reset Form

// Reset entire form
form.reset()

// Reset individual field
form.fields.email.reset()

// Reset on successful submit
const form = reatomForm(
  { email: '' },
  { 
    resetOnSubmit: true,
    onSubmit: async (state) => { /* ... */ }
  }
)

Advanced Patterns

Multi-Step Forms

const currentStep = atom(1, 'currentStep')

const wizardForm = reatomForm({
  // Step 1
  email: '',
  
  // Step 2
  profile: reatomFieldSet({
    firstName: '',
    lastName: '',
  }),
  
  // Step 3
  preferences: reatomFieldSet({
    newsletter: false,
    notifications: true,
  }),
})

const nextStep = action(() => {
  const step = currentStep()
  
  // Validate current step
  if (step === 1) {
    wizardForm.fields.email.validation.trigger()
  } else if (step === 2) {
    wizardForm.fields.profile.validation.trigger()
  }
  
  // Move to next step
  currentStep.set(step + 1)
}, 'nextStep')

Dependent Fields

const form = reatomForm({
  country: 'US',
  state: '',
  city: '',
})

// Reset dependent fields when country changes
form.fields.country.extend(
  withCallHook(() => {
    form.fields.state.reset()
    form.fields.city.reset()
  })
)

Custom Field Components

import { useAtom } from '@reatom/npm-react'
import type { FieldAtom } from '@reatom/core/form'

function TextField({ field }: { field: FieldAtom<string> }) {
  const [value, setValue] = useAtom(field.value)
  const [focus] = useAtom(field.focus)
  const [validation] = useAtom(field.validation)
  
  return (
    <div>
      <input
        value={value}
        onChange={(e) => field.change(e.target.value)}
        onFocus={() => field.focus.in()}
        onBlur={() => field.focus.out()}
      />
      {focus.touched && validation.errors.map((error) => (
        <span key={error.message} className="error">
          {error.message}
        </span>
      ))}
    </div>
  )
}

Server-Side Validation

import { resolveFieldByPath } from '@reatom/core/form'

const form = reatomForm(
  { /* ... */ },
  {
    onSubmit: async (state) => {
      const res = await wrap(fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(state),
      }))
      
      if (!res.ok) {
        const errors = await wrap(res.json())
        
        // Map errors to fields
        for (const error of errors) {
          const field = resolveFieldByPath(error.path, form.fields)
          if (field) {
            field.validation.errors.unshift({
              source: 'submission',
              message: error.message,
            })
          }
        }
        
        throw new Error('Validation failed')
      }
      
      return await wrap(res.json())
    },
  }
)

Best Practices

1. Use Schema Validation

Prefer schema validation over manual validation:
// ✓ Good - type-safe, comprehensive
const form = reatomForm(
  { email: '', age: 0 },
  {
    schema: z.object({
      email: z.string().email(),
      age: z.number().min(18),
    }),
  }
)

// ✗ Less ideal - manual validation
const form = reatomForm(
  { email: '', age: 0 },
  {
    validateBeforeSubmit: (state) => {
      if (!state.email.includes('@')) throw new Error('Invalid email')
      if (state.age < 18) throw new Error('Too young')
    },
  }
)

2. Validate on Blur for Better UX

Avoid validating on every keystroke:
const form = reatomForm(
  { /* ... */ },
  {
    validateOnBlur: true,    // ✓ Good - validates when user leaves field
    validateOnChange: false, // ✓ Good - avoids annoying users
  }
)

3. Handle Submit Errors

Always handle submit errors gracefully:
await wrap(form.submit()).catch(noop)

if (form.submit.error()) {
  // Show error message
  console.error(form.submit.error())
}

4. Use Field Arrays for Dynamic Lists

Don’t manually manage array state:
// ✓ Good - automatic validation and state management
const form = reatomForm({
  items: reatomFieldArray({
    initState: [],
    create: (value) => reatomField(value),
  }),
})

// ✗ Bad - manual array management
const form = reatomForm({
  items: [] as string[],
})