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.

Overview

reatomField creates a reactive atom representing a form field with built-in support for:
  • Value change handling with filtering
  • Focus and blur tracking
  • Validation (sync and async)
  • Dirty state management
  • State/value transformations

Type Signature

function reatomField<State, Value = State>(
  initState: State,
  options?: FieldOptions<State, Value>
): FieldAtom<State, Value>

Parameters

initState
State
required
The initial value of the field
options
FieldOptions<State, Value> | string
Field configuration options or debug name string

Return Value

FieldAtom
FieldAtom<State, Value>
Field atom with the following properties:

Examples

Basic Field

import { reatomField } from '@reatom/framework'

const emailField = reatomField('', 'emailField')

// Change value
emailField.change('user@example.com')

// Read value
console.log(emailField.value()) // 'user@example.com'
console.log(emailField()) // 'user@example.com' (state)

// Focus tracking
emailField.focus.in()
console.log(emailField.focus().active) // true

emailField.focus.out()
console.log(emailField.focus().touched) // true

Field with Validation

import { reatomField } from '@reatom/framework'

const usernameField = reatomField('', {
  name: 'username',
  validate: ({ value }) => {
    if (value.length < 3) return 'Username too short'
    if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Invalid characters'
  },
  validateOnChange: true,
})

usernameField.change('ab')
console.log(usernameField.validation().error) // 'Username too short'

usernameField.change('alice')
console.log(usernameField.validation().error) // undefined

Async Validation

import { reatomField } from '@reatom/framework'
import { wrap } from '@reatom/framework'

const emailField = reatomField('', {
  name: 'email',
  validate: async ({ value }) => {
    if (!value.includes('@')) return 'Invalid email'
    
    // Check if email is available
    const response = await wrap(
      fetch(`/api/check-email?email=${value}`)
    )
    const { available } = await response.json()
    
    if (!available) throw new Error('Email already taken')
  },
  validateOnBlur: true,
})

emailField.change('test@example.com')
emailField.focus.out()

// Check validation state
console.log(emailField.validation().validating) // Promise
await wrap(emailField.validation().validating)
console.log(emailField.validation().error) // 'Email already taken' or undefined

Standard Schema Validation

import { reatomField } from '@reatom/framework'
import { z } from 'zod'

const ageField = reatomField(0, {
  name: 'age',
  validate: z.number().min(18, 'Must be 18 or older'),
  validateOnChange: true,
})

ageField.change(15)
console.log(ageField.validation().error) // 'Must be 18 or older'

ageField.change(25)
console.log(ageField.validation().error) // undefined

State/Value Transformation

import { reatomField } from '@reatom/framework'

// Store as number, display as formatted string
const priceField = reatomField<number, string>(100.5, {
  name: 'price',
  fromState: (state) => state.toFixed(2), // number -> string
  toState: (value) => parseFloat(value),  // string -> number
})

console.log(priceField()) // 100.5 (state)
console.log(priceField.value()) // '100.50' (value)

priceField.change('99.99')
console.log(priceField()) // 99.99
console.log(priceField.value()) // '99.99'

Reactive Transformations

import { atom, reatomField } from '@reatom/framework'

const decimalPlaces = atom(2, 'decimalPlaces')

const priceField = reatomField<number, string>(100.5, {
  name: 'price',
  fromState: (state) => state.toFixed(decimalPlaces()),
  toState: (value) => {
    if (!value) return 0
    const parsed = parseFloat(value)
    const multiplier = Math.pow(10, decimalPlaces())
    return Math.round(parsed * multiplier) / multiplier
  },
})

priceField.change('100.12345')
console.log(priceField.value()) // '100.12'

decimalPlaces.set(4)
console.log(priceField.value()) // '100.1200'

Aborting Invalid Updates

import { reatomField, throwAbort } from '@reatom/framework'

const numberField = reatomField(0, {
  name: 'number',
  fromState: (state) => state.toString(),
  toState: (value: string) => {
    const parsed = Number(value)
    // Abort if invalid, keeping previous value
    return isNaN(parsed) ? throwAbort() : parsed
  },
})

numberField.change('123')
console.log(numberField()) // 123

numberField.change('invalid')
console.log(numberField()) // 123 (unchanged)

Custom Filter

import { reatomField } from '@reatom/framework'

const uppercaseField = reatomField('', {
  name: 'uppercase',
  filter: (newValue, prevValue) => {
    // Only allow uppercase changes
    return newValue === newValue.toUpperCase()
  },
})

uppercaseField.change('HELLO')
console.log(uppercaseField.value()) // 'HELLO'

uppercaseField.change('hello')
console.log(uppercaseField.value()) // 'HELLO' (unchanged)

Dynamic Validation

import { reatomField } from '@reatom/framework'

const passwordField = reatomField('', 'password')

const confirmField = reatomField('', {
  name: 'confirm',
  validateOnChange: true,
  validate: () => {
    if (
      !passwordField.validation().error &&
      passwordField.value() !== confirmField.value()
    ) {
      return 'Passwords do not match'
    }
  },
})

passwordField.change('secret123')
confirmField.change('secret456')
console.log(confirmField.validation().error) // 'Passwords do not match'

confirmField.change('secret123')
console.log(confirmField.validation().error) // undefined

Conditional Validation

import { reatomField } from '@reatom/framework'
import { z } from 'zod'

const field = reatomField(90, {
  name: 'field',
  validateOnChange: true,
  validate: ({ focus }) => {
    // Only validate if field has been modified
    if (focus.dirty) {
      return z.number().min(100, 'Must be at least 100')
    }
  },
})

// No validation on first render
console.log(field.validation().error) // undefined

// Validation triggers after change
field.change(91)
console.log(field.validation().error) // 'Must be at least 100'

Disabled State

import { reatomField } from '@reatom/framework'

const field = reatomField('', {
  validate: () => 'Error',
  validateOnChange: true,
})

field.change('value')
console.log(field.validation().error) // 'Error'
console.log(field.focus().dirty) // true

// Disable field
field.disabled.set(true)
console.log(field.validation().error) // undefined
console.log(field.focus().dirty) // false

// Re-enable
field.disabled.set(false)
console.log(field.validation().error) // 'Error'

Element Reference

import { reatomField } from '@reatom/framework'

const field = reatomField('', 'field')

// In your component
const inputElement = document.querySelector('input')
field.elementRef.set(inputElement)

// Later, focus programmatically
field.elementRef()?.focus()

Reset with Custom State

import { reatomField } from '@reatom/framework'

const field = reatomField(100, 'field')

field.change(200)
console.log(field()) // 200

// Reset to initial
field.reset()
console.log(field()) // 100

// Reset to custom value
field.reset(300)
console.log(field()) // 300
console.log(field.initState()) // 300 (updated)