Skip to content
IC Reactor

Validation

The @ic-reactor/react package provides utilities to easily handle validation errors thrown by DisplayReactor. These helpers make it simple to integrate with form libraries like React Hook Form or manage state manually.

These helpers read the ValidationError a validator’s verdict produces. A validator that throws or rejects instead is reported as a CallError whose cause is what it threw, so extractValidationErrors returns null for it and handleValidationError hands it to onOtherError. The hooks and factories call callMethod(), which runs validators synchronously, so they refuse every call to a method whose validator returns a promise, such as fromZodSchema(schema, { async: true }), with a CallError. Run async validation with the reactor’s validate() or callMethodWithValidation().

import {
mapValidationErrors,
getFieldError,
extractValidationErrors,
handleValidationError,
isValidationError,
} from "@ic-reactor/react"

Maps a ValidationError to a simple object where keys are field names and values are error messages.

function mapValidationErrors(
error: ValidationError,
options?: { multiple?: false }
): Record<string, string>
function mapValidationErrors(
error: ValidationError,
options: { multiple: true }
): Record<string, string[]>
function mapValidationErrors(
error: ValidationError,
options?: { multiple?: boolean }
): Record<string, string> | Record<string, string[]>

By default each field maps to its first message; pass { multiple: true } to get every message for each field as an array.

An issue about the whole argument rather than one field (an empty path, as zod reports for an object-level .refine()) is filed under the key "".

Usage:

Call mutateAsync here: it returns a promise that rejects with the error. mutate returns nothing and reports errors only through onError, so a catch around it never runs.

import { useState } from "react"
import {
isValidationError,
mapValidationErrors,
type ReactorArgsOf,
} from "@ic-reactor/react"
import { backend, useActorMutation } from "./reactor"
function useTransfer() {
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
const { mutateAsync } = useActorMutation({ functionName: "transfer" })
const transfer = async (
data: ReactorArgsOf<typeof backend, "transfer">[0]
) => {
try {
await mutateAsync([data])
} catch (error) {
if (isValidationError(error)) {
const errors = mapValidationErrors(error)
console.log(errors) // { to: "Required", amount: "Must be positive" }
setFormErrors(errors)
}
}
}
return { transfer, formErrors }
}

Creates an error handler callback that automatically checks for ValidationError and extracts field errors. Perfect for onError callbacks.

function handleValidationError(
setFieldErrors: (errors: Record<string, string>) => void,
onOtherError?: (error: Error) => void
): (error: Error) => void

Usage:

import { useState } from "react"
import { handleValidationError, useReactorMutation } from "@ic-reactor/react"
import { backend } from "./reactor"
// `toast` is your notification helper, such as sonner's
function useTransferMutation() {
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
const { mutate } = useReactorMutation({
reactor: backend,
functionName: "transfer",
onError: handleValidationError(
(errors) => setFormErrors(errors), // Called if validation error
(error) => toast.error(error.message) // Called for other errors
),
})
return { mutate, formErrors }
}

Extracts the first error message for a specific field. Pass "" for an issue about the whole argument.

function getFieldError(
error: ValidationError,
field: string
): string | undefined

A type-safe helper that returns the mapped errors if the error is a ValidationError, or null otherwise.

function extractValidationErrors(error: unknown): Record<string, string> | null
import { useState } from "react"
import {
useReactorMutation,
mapValidationErrors,
isValidationError,
} from "@ic-reactor/react"
import { backend } from "./reactor"
function TransferForm() {
const [errors, setErrors] = useState<Record<string, string>>({})
const { mutate, isPending } = useReactorMutation({
reactor: backend,
functionName: "transfer",
onError: (error) => {
if (isValidationError(error)) {
setErrors(mapValidationErrors(error))
}
},
})
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutate([
{
to: formData.get("to") as string,
amount: formData.get("amount") as string,
},
])
}
return (
<form onSubmit={handleSubmit}>
<div>
<input name="to" placeholder="To" />
{errors.to && <span className="error">{errors.to}</span>}
</div>
<div>
<input name="amount" placeholder="Amount" />
{errors.amount && <span className="error">{errors.amount}</span>}
</div>
<button disabled={isPending}>Submit</button>
</form>
)
}