Validation
Overview
Section titled “Overview”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
Section titled “Import”import { mapValidationErrors, getFieldError, extractValidationErrors, handleValidationError, isValidationError,} from "@ic-reactor/react"Utilities
Section titled “Utilities”mapValidationErrors
Section titled “mapValidationErrors”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 }}handleValidationError
Section titled “handleValidationError”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) => voidUsage:
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 }}getFieldError
Section titled “getFieldError”Extracts the first error message for a specific field. Pass "" for an issue about the whole argument.
function getFieldError( error: ValidationError, field: string): string | undefinedextractValidationErrors
Section titled “extractValidationErrors”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> | nullintegration Examples
Section titled “integration Examples”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> )}import { useForm } from "react-hook-form"import { useReactorMutation, mapValidationErrors, isValidationError, type ReactorArgsOf,} from "@ic-reactor/react"import { backend } from "./reactor"
type TransferFields = ReactorArgsOf<typeof backend, "transfer">[0]
function TransferForm() { const { register, handleSubmit, setError } = useForm<TransferFields>({ defaultValues: { to: "", amount: "" }, })
const { mutate } = useReactorMutation({ reactor: backend, functionName: "transfer", onError: (error) => { if (isValidationError(error)) { const fieldErrors = mapValidationErrors(error) Object.entries(fieldErrors).forEach(([field, message]) => { setError(field as keyof TransferFields, { type: "server", message }) }) } }, })
const onSubmit = (data: TransferFields) => mutate([data])
return ( <form onSubmit={handleSubmit(onSubmit)}> <input placeholder="To" {...register("to")} /> <input placeholder="Amount" {...register("amount")} /> <button>Submit</button> </form> )}