Form Hooks
Granular Zustand-powered hooks for optimal re-render performance and fine-grained state access.
RilayKit forms expose granular Zustand selector hooks: subscribe to exactly the state slice you need instead of re-rendering the whole form on every change. All hooks require a FormProvider ancestor (the <Form> component includes one) and import from the /react entry:
import { useFieldValue, useFieldActions, useFormSubmitState } from '@rilaykit/forms/react';Context Hook
useForm() returns the full form context: configuration, condition helpers, validation, and submission.
interface FormConfigContextValue {
formConfig: FormConfiguration;
formInstanceKey: string; // opaque identity of the mounted form — compare by equality only; changes on form swap
conditionsHelpers: {
hasConditionalFields: boolean;
getFieldCondition(fieldId: string): ConditionEvaluationResult | undefined;
isFieldVisible(fieldId: string): boolean;
isFieldDisabled(fieldId: string): boolean;
isFieldRequired(fieldId: string): boolean;
isFieldReadonly(fieldId: string): boolean;
};
validateField(fieldId: string, value?: unknown): Promise<ValidationResult>;
validateForm(): Promise<ValidationResult>;
validateFormLevel(): Promise<unknown>; // re-run cross-field rules, route issues into the error map
submit(eventOrOptions?: React.FormEvent | SubmitOptions): Promise<boolean>;
}submit accepts a React.FormEvent (for <form onSubmit>) or options:
const { submit } = useForm();
await submit(); // Validate, then submit
await submit({ force: true }); // Skip validation entirely
await submit({ skipInvalid: true }); // Validate, exclude invalid fieldsuseForm() re-renders whenever any part of the context changes. Prefer the granular hooks below for performance-sensitive components.
Field State Hooks
Read-only selectors scoped to one field — each re-renders only when its slice changes.
| Hook | Returns |
|---|---|
useFieldValue<T>(fieldId) | T |
useFieldErrors(fieldId) | FieldError[] (includes cross-field issues routed to this field) |
useFieldTouched(fieldId) | boolean |
useFieldValidationState(fieldId) | ValidationState |
useFieldConditions(fieldId) | FieldConditions |
useFieldProps(fieldId) | Record<string, unknown> — dynamic props set by effects |
useFieldState(fieldId) | FieldState (value, errors, validationState, touched, dirty) |
useRepeatableKeys(repeatableId) | string[] — stable row keys of a repeatable |
A typical field component pairs a read hook with useFieldActions (write):
import { useFieldValue, useFieldErrors, useFieldActions } from '@rilaykit/forms/react';
function CustomInput({ fieldId }: { fieldId: string }) {
const value = useFieldValue<string>(fieldId);
const errors = useFieldErrors(fieldId);
const { setValue, setTouched } = useFieldActions(fieldId);
return (
<div>
<input
value={value ?? ''}
onChange={(e) => setValue(e.target.value)}
onBlur={() => setTouched()}
/>
{errors.map((err) => (
<p key={err.message} className="text-red-500">{err.message}</p>
))}
</div>
);
}Types
interface FieldError {
readonly message: string;
readonly code?: string;
readonly path?: string;
}
type ValidationState = 'idle' | 'validating' | 'valid' | 'invalid';
interface FieldConditions {
readonly visible: boolean;
readonly disabled: boolean;
readonly required: boolean;
readonly readonly: boolean;
}
interface FieldState {
readonly value: unknown;
readonly errors: FieldError[];
readonly validationState: ValidationState;
readonly touched: boolean;
readonly dirty: boolean;
}Form State Hooks
Form-level selectors, same granularity rules.
| Hook | Returns |
|---|---|
useFormSubmitting() | boolean |
useFormValid() | boolean — counts the whole error map, __form__ included |
useFormDirty() | boolean |
useFormValues() | Record<string, unknown> — re-renders on any value change |
useFormErrors() | FieldError[] — the __form__ bucket (cross-field issues targeting no field) |
useFormSubmitState() | { isSubmitting, isValid, isDirty } |
import { useFormSubmitState, useFormErrors } from '@rilaykit/forms/react';
function SubmitBar() {
const { isSubmitting, isValid, isDirty } = useFormSubmitState();
const formErrors = useFormErrors();
return (
<>
{formErrors.length > 0 && (
<div role="alert">{formErrors.map((e) => e.message).join(' ')}</div>
)}
<button type="submit" disabled={isSubmitting || !isDirty || !isValid}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</>
);
}Action Hooks
Action hooks return stable function references that call directly into the store. They never subscribe to state, so they never cause re-renders — safe to pass as props or use in event handlers.
Field Actions
import { useFieldActions } from '@rilaykit/forms/react';
const { setValue, setTouched, setErrors, clearErrors, setValidationState } =
useFieldActions('email');
interface UseFieldActionsResult {
setValue: (value: unknown) => void;
setTouched: () => void;
setErrors: (errors: FieldError[]) => void;
clearErrors: () => void;
setValidationState: (state: ValidationState) => void;
}Form Actions
import { useFormActions } from '@rilaykit/forms/react';
const { setValue, setTouched, setErrors, setSubmitting, reset, setFieldConditions } =
useFormActions();
setValue('firstName', 'Ada');
reset(); // Back to default values
reset({ firstName: 'Ada' }); // Explicit values
reset(values, { items: ['k2', 'k0'] }); // Also restore repeatable row order
interface UseFormActionsResult {
setValue: (fieldId: string, value: unknown) => void;
setTouched: (fieldId: string) => void;
setErrors: (fieldId: string, errors: FieldError[]) => void;
setSubmitting: (isSubmitting: boolean) => void;
reset: (values?: Record<string, unknown>, repeatableOrder?: Record<string, string[]>) => void;
setFieldConditions: (fieldId: string, conditions: FieldConditions) => void;
}useFormStoreApi() returns the raw Zustand store (no subscription) — useful for reading store.getState() imperatively, e.g. in tests.
Condition Hooks
Evaluate conditional behaviors (visible, disabled, required, readonly) against form data.
useConditionEvaluation
Evaluates one ConditionalBehavior against provided data. Memoized on conditions and formData.
import { useConditionEvaluation } from '@rilaykit/forms/react';
const { visible, disabled, required, readonly } = useConditionEvaluation(
fieldConfig.conditions, // ConditionalBehavior | undefined
formData, // Record<string, unknown>
{ visible: true }, // optional default state overrides
);useFormConditions
Evaluates all field conditions for a form configuration at once — what FormProvider uses internally.
import { useFormConditions } from '@rilaykit/forms/react';
const { fieldConditions, hasConditionalFields, isFieldVisible, isFieldDisabled,
isFieldRequired, isFieldReadonly, getFieldCondition } =
useFormConditions({ formConfig, formValues });useFieldConditionsLazy
Reads conditions from the store and re-evaluates only when form values actually change (values hash).
import { useFieldConditionsLazy } from '@rilaykit/forms/react';
const conditions = useFieldConditionsLazy('myField', {
conditions: fieldConfig.conditions,
skip: false,
});
if (!conditions.visible) return null;useConditionEvaluator
Returns a memoized evaluator to call imperatively for any field, without subscribing.
import { useConditionEvaluator } from '@rilaykit/forms/react';
const evaluate = useConditionEvaluator();
const nameConditions = evaluate('name', nameFieldConfig.conditions);Internal Hooks
Used internally by RilayKit components; exported for advanced cases.
| Hook | Purpose |
|---|---|
useFormValidationWithStore | Wires field and form validation to the store |
useFormSubmissionWithStore | Handles the submission lifecycle |
useFormMonitoring | Tracks renders, validations, and submissions for profiling |
Best Practices
- Prefer granular hooks over
useForm()— a component that needs one value should useuseFieldValue(fieldId). - Use
useFormErrors()for a form-level error banner (aria-live); field-targeted cross-field issues surface throughuseFieldErrors(id).