Create Form Control
Apply this skill any time the user is writing a new input component intended to live inside a <Form> or <NativeForm>. Every input in this library is a thin UI wrapper around useFormControl.
The contract
useFormControl(props, options?) registers the input with the surrounding form (via FormContext) and returns a stable hook object:
{ id: string; // generated or echoed back from props.id name: string; // echoed back, dot-notation normalized type: string; // echoed back (default "text") value: any; // current value changeValue: (value, options?) => void; // call from onChange / onChangeText error: ReactNode; // current validation error (or null) errorsList: { [ruleName]: ReactNode }; // per-rule errors when validateAll is on setError: (error) => void; checked: boolean; // for checkbox / radio setChecked: (checked: boolean) => void; inputRef: RefObject; // attach to the host input — enables focus()/blur() visibleElementRef: RefObject; // attach to the wrapper — enables isVisible() / validateVisible() formControl: FormControl; // the underlying registration object — escape hatch disabled: boolean; disable: () => void; enable: () => void; isInvalid: boolean; // true once touched AND validation has failed otherProps: object; // every prop NOT consumed by the hook or by rules}Pattern 1 — Text input (Web)
import { useFormControl, type FormControlProps } from "@mongez/react-form";
export default function TextInput(props: FormControlProps) { const { value, changeValue, id, error, inputRef, otherProps } = useFormControl(props);
return ( <> <input id={id} ref={inputRef} value={value} onChange={(e) => changeValue(e.target.value)} {...otherProps} /> {error && <span className="error">{error}</span>} </> );}Critical points:
- Spread
otherProps(NOT rawprops) onto the host<input>.otherPropsexcludes hook-internal props (name,rules,errors,onChange,value,defaultValue, etc.) and any props the active rules declared aspreservedProps(e.g.minLength,pattern). - Always wire
inputRefif you wantformControl.focus()/formControl.blur()to work. - The hook is controlled internally —
valuefrom the hook is always the source of truth, regardless of whether the user passed avalueprop or not.
Pattern 2 — Checkbox
import { useFormControl, type FormControlProps } from "@mongez/react-form";
export default function Checkbox(props: FormControlProps) { const { checked, setChecked, id, error } = useFormControl({ ...props, type: "checkbox", // MUST be explicit });
return ( <> <input id={id} type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)} /> {error && <span className="error">{error}</span>} </> );}For checkboxes, use checked / setChecked, not value / changeValue. The type: "checkbox" setting changes how the value is collected:
- Checked →
valueprop is emitted (ortrueif novalueprop). - Unchecked → emits
uncheckedValue(from the second argument ofuseFormControl) ifcollectUncheckedis true, otherwise the field is omitted entirely.
const { checked, setChecked } = useFormControl(props, { uncheckedValue: 0, // emit 0 when unchecked collectUnchecked: true,});Pattern 3 — Multi-value control (multi-select, tag input)
Pass multiple: true in the options to declare a multi-value control:
const { value, changeValue } = useFormControl(props, { multiple: true });// value is always an arrayMulti-value controls are collected as arrays in form.values() even when they hold a single item.
Pattern 4 — Radio (use useRadioInput instead)
For radios, don’t use useFormControl per radio button. Build a RadioGroup (one useFormControl call) that provides RadioGroupContext, and have each RadioInput consume it via useRadioInput(value). See the react-native-usage and validation-rules skills for the full pattern.
Pattern 5 — Custom per-instance validation
Pass a validate callback in props (NOT in rules) for one-off validation that should only apply to this instance:
<TextInput name="username" validate={({ value }) => { if (!/^[a-zA-Z0-9]+$/.test(value)) return "Username must be alphanumeric"; }}/>The callback receives the full InputRuleOptions object and may return a ReactNode (error) or a Promise<ReactNode> (async — blocks other rules until resolved). Returning nothing means valid.
Pattern 6 — Showing per-rule errors
When useFormControl(props, { validateAll: true }) is set, error becomes an array of all failing-rule messages and errorsList[ruleName] exposes each individually:
const { errorsList } = useFormControl({ ...props, rules: [requiredRule, minLengthRule],}, { validateAll: true });
return ( <> {errorsList.required && <p>{errorsList.required}</p>} {errorsList.minLength && <p>{errorsList.minLength}</p>} </>);Pattern 7 — Hidden input (no UI)
Use the built-in HiddenInput component for values that should be collected but not rendered:
import { HiddenInput } from "@mongez/react-form";
<HiddenInput name="csrfToken" value={token} />It calls useFormControl and returns null. The value still appears in form.values().
Default id generation
If no id prop is provided, the hook derives one from name: input-<sanitized-name> (dots become dashes, non-alphanumerics stripped). This is deliberate — it makes the input’s id predictable for <label htmlFor> association.
Recap checklist before committing a new input component
-
useFormControlcalled with props (andtypeexplicitly set when not “text”). -
otherPropsspread onto the host element (not rawprops). -
inputRefwired to the host input. -
errorrendered conditionally. - For checkbox/radio:
checked/setCheckedused, notvalue/changeValue. -
nameprop is required and must be provided by the consumer.