Forms
QuickDapp uses a custom hook-based form system rather than external libraries like React Hook Form. The useForm and useField hooks provide validation, error handling, and field state management.
The useField Hook
Each form field gets its own useField instance that tracks value, validation state, and touched status:
const nameField = useField({
initialValue: "",
validate: (value) => {
if (!value.trim()) return "Name is required"
if (value.length < 2) return "Name must be at least 2 characters"
}
})
The hook returns:
name— Field name from optionsvalue— Current field valuevalid— Whether the field passes validationerror— Validation error message (if any)version— Increments on each changeisSet— True if the field has a value or is optionalisValidating— True during async validationhandleChange(value)— Update the field valueunset()— Reset to initial state
Async Validation
Fields support async validation with debouncing. This is useful for checking availability or validating against a server:
const addressField = useField({
initialValue: "",
validateAsync: async (value) => {
const isValid = await checkAddressOnChain(value)
if (!isValid) return "Invalid address"
},
validateAsyncDebounceMs: 300
})
The isValidating flag indicates when async validation is in progress, so you can show a loading indicator.
The useForm Hook
For forms with multiple fields, useForm coordinates validation across all fields:
const form = useForm({
onSubmit: async (values) => {
await createToken(values)
}
})
The form tracks overall validity and handles submission. Fields register themselves with the form and validation runs on submit.
Form Components
The form UI components in Form.tsx integrate with the field hooks:
Input and Textarea accept an error prop to display validation messages below the field. They show a red border when invalid.
Label wraps Radix UI's label with optional required indicator styling.
FormField combines a label, input, and error message into a single component.
TextInput and NumberInput are pre-integrated with useField for common use cases.
FieldSuffix shows a spinning indicator during async validation.
Validation Patterns
Validate on change for immediate feedback:
const field = useField({
initialValue: "",
validate: (value) => {
if (!value) return "Required"
}
})
// Error updates as user types
Validate on blur for less intrusive feedback:
// Check field.isSet before showing errors
{field.isSet && field.error && <span>{field.error}</span>}
Combine sync and async validation:
const field = useField({
initialValue: "",
validate: (value) => {
// Sync validation runs first
if (!isAddress(value)) return "Invalid address format"
},
validateAsync: async (value) => {
// Async only runs if sync passes
const exists = await checkAddressExists(value)
if (!exists) return "Address not found"
}
})
Sanitization
Fields can sanitize values before validation:
const field = useField({
initialValue: "",
sanitize: (value) => value.toLowerCase().trim()
})
The sanitized value is what gets validated and submitted.