rilaykit
Forms

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 fields

useForm() 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.

HookReturns
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.

HookReturns
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.

HookPurpose
useFormValidationWithStoreWires field and form validation to the store
useFormSubmissionWithStoreHandles the submission lifecycle
useFormMonitoringTracks renders, validations, and submissions for profiling

Best Practices

  1. Prefer granular hooks over useForm() — a component that needs one value should use useFieldValue(fieldId).
  2. Use useFormErrors() for a form-level error banner (aria-live); field-targeted cross-field issues surface through useFieldErrors(id).

On this page