rilaykit
Forms

Form Hooks

Granular Zustand-powered hooks for optimal re-render performance and fine-grained state access.

RilayKit forms are powered by Zustand stores, exposing granular selector hooks that minimize re-renders. Instead of one monolithic hook that causes the entire form to re-render on every change, you pick exactly the state slices you need.

All hooks must be used inside a component wrapped by FormProvider (or the <Form> component, which includes it).

import {
  useFieldValue,
  useFieldActions,
  useFormSubmitState,
} from '@rilaykit/forms';

Context Hook

useFormConfigContext() returns the full form context, including the form configuration, condition helpers, validation, and submission methods. Use it when you need access to multiple concerns at once, but be aware that it subscribes to the entire context object.

import { useFormConfigContext } from '@rilaykit/forms';

const { formConfig, conditionsHelpers, validateField, validateForm, submit } =
  useFormConfigContext();

FormConfigContextValue

interface FormConfigContextValue {
  formConfig: FormConfiguration;
  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>;
  submit(event?: React.FormEvent): Promise<boolean>;
}

useFormConfigContext() re-renders whenever any part of the context changes. Prefer the granular hooks below for performance-sensitive components.

Field State Hooks

These hooks are read-only selectors that subscribe to a single field slice in the Zustand store. Each hook only triggers a re-render when its specific slice changes.

HookReturnsRe-renders when
useFieldValue<T>(fieldId)TField value changes
useFieldErrors(fieldId)ValidationError[]Field errors change
useFieldTouched(fieldId)booleanField touch state changes
useFieldValidationState(fieldId)ValidationStateValidation state changes
useFieldConditions(fieldId)FieldConditionsField conditions change
useFieldState(fieldId)FieldStateAny field state changes

Usage

import { useFieldValue, useFieldErrors } from '@rilaykit/forms';

function PriceDisplay() {
  const price = useFieldValue<number>('price');
  const errors = useFieldErrors('price');

  return (
    <div>
      <span>Current price: {price ?? 'N/A'}</span>
      {errors.map((err) => (
        <p key={err.message} className="text-red-500">{err.message}</p>
      ))}
    </div>
  );
}
import { useFieldState } from '@rilaykit/forms';

function FieldDebug({ fieldId }: { fieldId: string }) {
  const { value, errors, validationState, touched, dirty } =
    useFieldState(fieldId);

  return (
    <pre>{JSON.stringify({ value, errors, validationState, touched, dirty }, null, 2)}</pre>
  );
}

Types

type ValidationState = 'idle' | 'validating' | 'valid' | 'invalid';

interface FieldConditions {
  visible: boolean;
  disabled: boolean;
  required: boolean;
  readonly: boolean;
}

interface FieldState {
  value: unknown;
  errors: ValidationError[];
  validationState: ValidationState;
  touched: boolean;
  dirty: boolean;
}

Form State Hooks

These hooks subscribe to form-level slices. Like field hooks, they only re-render when their specific slice changes.

HookReturnsRe-renders when
useFormSubmitting()booleanSubmit state changes
useFormValid()booleanValidity changes
useFormDirty()booleanDirty state changes
useFormValues()Record<string, unknown>Any value changes
useFormSubmitState(){ isSubmitting, isValid, isDirty }Submit-related state changes

Usage

import { useFormSubmitState } from '@rilaykit/forms';

function SubmitButton() {
  const { isSubmitting, isValid, isDirty } = useFormSubmitState();

  return (
    <button type="submit" disabled={isSubmitting || !isDirty || !isValid}>
      {isSubmitting ? 'Saving...' : 'Save'}
    </button>
  );
}

useFormValues() re-renders whenever any field value changes. If you only need a single field, prefer useFieldValue(fieldId) instead.

Action Hooks

Action hooks return stable function references that call directly into the Zustand store. They never subscribe to state, so they never cause re-renders.

Action hooks return stable references and never trigger re-renders. You can safely pass them as props or use them in event handlers without worrying about performance.

Field Actions

useFieldActions(fieldId) returns actions scoped to a single field.

import { useFieldActions } from '@rilaykit/forms';

const { setValue, setTouched, setErrors, clearErrors, setValidationState } =
  useFieldActions('email');

// Update the field value programmatically
setValue('user@example.com');

// Mark the field as touched (e.g., on blur)
setTouched();

// Set validation errors manually
setErrors([{ message: 'Invalid email', code: 'INVALID_EMAIL' }]);

// Clear all errors for this field
clearErrors();

// Set validation state directly
setValidationState('validating');

UseFieldActionsResult

interface UseFieldActionsResult {
  setValue: (value: unknown) => void;
  setTouched: () => void;
  setErrors: (errors: ValidationError[]) => void;
  clearErrors: () => void;
  setValidationState: (state: ValidationState) => void;
}

Form Actions

useFormActions() returns actions that operate at the form level.

import { useFormActions } from '@rilaykit/forms';

const { setValue, setTouched, setErrors, setSubmitting, reset, setFieldConditions } =
  useFormActions();

// Set any field's value
setValue('firstName', 'Ada');

// Reset the entire form to default values
reset();

// Reset with specific values
reset({ firstName: 'Ada', lastName: 'Lovelace' });

// Programmatically set field conditions
setFieldConditions('phoneNumber', {
  visible: true,
  disabled: false,
  required: true,
  readonly: false,
});

UseFormActionsResult

interface UseFormActionsResult {
  setValue: (fieldId: string, value: unknown) => void;
  setTouched: (fieldId: string) => void;
  setErrors: (fieldId: string, errors: ValidationError[]) => void;
  setSubmitting: (isSubmitting: boolean) => void;
  reset: (values?: Record<string, unknown>) => void;
  setFieldConditions: (fieldId: string, conditions: FieldConditions) => void;
}

Condition Hooks

These hooks evaluate conditional behaviors (visibility, disabled state, required state, readonly state) for fields based on form data.

useConditionEvaluation

Evaluates a ConditionalBehavior configuration against the provided form data and returns the resolved states.

import { useConditionEvaluation } from '@rilaykit/forms';

const { visible, disabled, required, readonly } = useConditionEvaluation(
  fieldConfig.conditions, // ConditionalBehavior | undefined
  formData,               // Record<string, unknown>
  { visible: true },      // optional default state overrides
);

The result is memoized and only recomputed when conditions or formData change.

useFormConditions

Evaluates all field conditions for an entire form configuration at once. This is what FormProvider uses internally.

import { useFormConditions } from '@rilaykit/forms';

const {
  fieldConditions,      // Record<string, ConditionEvaluationResult>
  hasConditionalFields, // boolean
  getFieldCondition,    // (fieldId: string) => ConditionEvaluationResult | undefined
  isFieldVisible,       // (fieldId: string) => boolean
  isFieldDisabled,      // (fieldId: string) => boolean
  isFieldRequired,      // (fieldId: string) => boolean
  isFieldReadonly,      // (fieldId: string) => boolean
} = useFormConditions({ formConfig, formValues });

useFieldConditionsLazy

Lazy evaluation with caching. Reads conditions from the Zustand store and only evaluates when form values actually change (based on a values hash).

import { useFieldConditionsLazy } from '@rilaykit/forms';

const conditions = useFieldConditionsLazy('myField', {
  conditions: fieldConfig.conditions, // ConditionalBehavior | undefined
  skip: false,                        // skip evaluation entirely
});

if (!conditions.visible) return null;

useConditionEvaluator

Returns a memoized evaluator function that you can call imperatively for any field. Useful when you need to evaluate conditions for multiple fields on-demand without triggering re-renders.

import { useConditionEvaluator } from '@rilaykit/forms';

const evaluate = useConditionEvaluator();

// Call for any field, at any time
const nameConditions = evaluate('name', nameFieldConfig.conditions);
const emailConditions = evaluate('email', emailFieldConfig.conditions);

Internal Hooks

These hooks are used internally by RilayKit components. You rarely need them directly, but they are exported for advanced use cases.

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

Best Practices

  1. Prefer granular hooks over useFormConfigContext(). A component that only needs a field value should use useFieldValue(fieldId) rather than pulling the entire context.

  2. Use useFieldActions for programmatic field changes. Since actions are stable references, they are safe to pass as props without causing unnecessary re-renders in child components.

  3. Use useFormSubmitState() for submit buttons. It bundles exactly the three values a submit button needs (isSubmitting, isValid, isDirty) into a single hook with minimal subscriptions.

  4. Combine read and action hooks in the same component. A typical field component pairs useFieldValue (read) with useFieldActions (write) to achieve optimal re-render boundaries.

import { useFieldValue, useFieldActions, useFieldErrors } from '@rilaykit/forms';

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>
  );
}

On this page