type
FieldValues
export type FieldValues = { readonly [string]: mixed, ... };A form's values: a plain object addressed by [FieldPath].
API reference
Forms that do not re-render: uncontrolled fields, narrow subscriptions and schema validation, part of the Unified Toolchain for Flow.
Written from the source by uf doc when this site was built: the signature and the comment above each export, grouped by the specifier a program imports it from.
@uniflowed/formtype
FieldValuesexport type FieldValues = { readonly [string]: mixed, ... };A form's values: a plain object addressed by [FieldPath].
type
FieldPathexport type FieldPath = string;A dotted path from the root of the values, e.g. "items.2.quantity".
A string, and deliberately not a type derived from the shape of the values. Flow has no template-literal types, so there is no honest way to spell "a dotted path that exists in TValues" — and a type that only *looks* like it checks paths would be worse than one that admits it does not.
This stays the storage key and the wire format: it is what register gives an input as its name, what an error is keyed by, and what a field array rewrites. What it is *not* any more is the only way to address a field — see [FieldSegment], and index.js for what each form is checked for.
type
FieldSegmentsexport type FieldSegments = $ReadOnlyArray<string | number>;A path given as its segments, rather than as one dotted string.
"items.2.quantity" cannot be checked against the shape of the values, and that is a fact about template literal types. ["items", 2, "quantity"] is a different question, and Flow answers it: a generic bounded by the keys of the object it indexes resolves, and composes to the next segment. The whole of the typed half of this package's API is that observation, applied at a depth.
type
FieldSegmentexport type FieldSegment<TNode> = TNode extends $ReadOnlyArray<mixed> ? number : $Keys<TNode>;What may follow TNode in a path: an index if it is a list, a key if not.
The conditional is load-bearing rather than decorative. $Keys<Array<Row>> is not "a number" — it reports *an index signature declaring the expected key / value type is missing in array type*, because an array's keys are not what $Keys is about. One line tells the two cases apart and every array index in a path works from there.
type
ValueAtPathexport type ValueAtPath<TValues, TPath> = TPath extends [infer K1]
? StepInto<TValues, K1>
: TPath extends [infer K1, infer K2]
? StepInto<StepInto<TValues, K1>, K2>
: TPath extends [infer K1, infer K2, infer K3]
? StepInto<StepInto<StepInto<TValues, K1>, K2>, K3>
: TPath extends [infer K1, infer K2, infer K3, infer K4]
? StepInto<StepInto<StepInto<StepInto<TValues, K1>, K2>, K3>, K4>
: mixed;The type held at TPath within TValues, to a depth of four.
Written out per length rather than recursively, and the reason is a defect rather than a preference: a recursive conditional over a tuple — TPath extends [infer K, ...infer Rest] — binds K as unknown and Rest as the whole array widened, which is ubugeeei-prod/uf#300. So the variadic version is blocked, not impossible in principle, and this is what can be written until it is fixed.
Four segments reaches items.0.tags.0. A fifth is mixed, which is what the dotted form gives everywhere and is the floor this degrades to rather than an error. That is why the last branch exists: a path past the cap has to keep *working*.
type
Modeexport type Mode = "onSubmit" | "onBlur" | "onChange" | "onTouched" | "all";When a field is validated, before the form has ever been submitted.
onSubmit is the default because it is the only one that never tells someone their email address is invalid while they are halfway through typing it. onTouched waits for the first blur and validates on every change after — a field has to have been visited before it is allowed to complain.
type
ReValidateModeexport type ReValidateMode = "onChange" | "onBlur" | "onSubmit";When a field is re-validated once the form has been submitted at least once.
type
FieldErrorsexport type FieldErrors = { readonly [string]: FieldError, ... };Errors, dirty flags and touched flags, all keyed by field path.
type
FormStateexport type FormState<TValues extends FieldValues = FieldValues> = {|
readonly errors: FieldErrors,
readonly isDirty: boolean,
readonly dirtyFields: FieldFlags,
readonly touchedFields: FieldFlags,
readonly isSubmitting: boolean,
readonly isSubmitted: boolean,
readonly isSubmitSuccessful: boolean,
readonly isValidating: boolean,
readonly isValid: boolean,
readonly submitCount: number,
/**
* Whether the whole form is switched off — `useForm({ disabled })`.
*
* The form's own flag, not a summary of its fields: a form with one disabled
* field is not a disabled form. It follows the option by one commit, because
* a snapshot is only rebuilt when the store is told, and the store is told
* from an effect. The `disabled` attribute on a control does *not* lag, and
* that is deliberate — `register` is handed the current render's flag. Use
* this for what a form-level flag is for, disabling a submit button while a
* save is in flight, and `formState.isSubmitting` where the timing has to be
* exact.
*/
readonly disabled: boolean,
/**
* Whether an asynchronous `defaultValues` is still resolving.
*
* True from the first render of a form built with a thunk, false for every
* other form from its first render — which is the property that matters, and
* is why this is not a flag the store learns about in an effect. A loading
* form is usable meanwhile: empty, not dirty, and not lying about either.
*
* There is deliberately no `isReady` beside it. React Hook Form has one
* because its form finishes setting itself up after the first render; this
* store is built in a `useState` initialiser, so its first snapshot is
* already the real one and an `isReady` here would be either `!isLoading`
* under a second name or a flag that flipped in an effect — which would cost
* every form in the package one render to learn something that was already
* true.
*/
readonly isLoading: boolean,
/**
* What `reset()` with no argument would go back to.
*
* The form's own copy, not the object that was passed: `useForm` deep-copies
* `defaultValues` so that a caller mutating theirs cannot change what the
* form resets to, and this is that copy. It moves when `reset(values)`, a
* `values` re-seed or a resolved asynchronous default moves it, and its
* identity is what the snapshot comparison uses — so reading it costs a form
* that never resets nothing at all.
*/
readonly defaultValues: TValues,
|};Everything a form knows about itself that is not a value.
Generic in the values only because of [defaultValues], and with a default so that FormState still means something written on its own — an annotation that does not care which form it came from gets FieldValues, and reads the defaults as mixed, exactly as it would have before.
type
SetValueOptionsexport type SetValueOptions = {|
readonly shouldValidate?: boolean,
readonly shouldDirty?: boolean,
readonly shouldTouch?: boolean,
|};What setValue does beyond writing the value.
All three default to false, which is React Hook Form's choice and the right one: a value the application put there is not a value the user changed, so it does not make the form dirty, does not mark the field visited, and does not make it start complaining. useController passes shouldDirty, because there the write *is* the user typing.
type
ResetOptionsexport type ResetOptions = {|
readonly keepValues?: boolean,
/**
* Replace the values, except at the paths the user has already edited.
*
* The option that makes `useForm({ values })` usable on a form somebody is
* typing into: a record the server re-sent lands everywhere except the three
* fields being worked on. A field kept this way keeps its dirty flag too —
* unless the incoming values happen to agree with what was typed, in which
* case it is not dirty any more and saying it was would be a form that can
* never be clean again.
*
* A field the new values do *not* contain still keeps the user's edit, for
* the same reason: it was theirs, and the point of this flag is that their
* work is not what a re-seed is for.
*/
readonly keepDirtyValues?: boolean,
readonly keepDefaultValues?: boolean,
readonly keepErrors?: boolean,
readonly keepDirty?: boolean,
readonly keepTouched?: boolean,
readonly keepSubmitCount?: boolean,
readonly keepIsSubmitted?: boolean,
/**
* Keep `isSubmitSuccessful`.
*
* Separate from `keepIsSubmitted` because the two answer different questions
* — "has this form been submitted" and "did the last submit work" — and the
* form that wants the second is the one that resets itself in `onValid` and
* then renders a confirmation. Without this flag that reset is what erases
* the thing the confirmation is reading.
*/
readonly keepIsSubmitSuccessful?: boolean,
/**
* Keep `isValidating`.
*
* Without it a reset clears the flag, on the grounds that a form which has
* just been replaced is not validating the thing it no longer holds. A pass
* already in flight still lands — dropping it is [`publish`]'s job and it
* drops by sequence number, not by whether a reset happened.
*/
readonly keepIsValidating?: boolean,
/**
* Keep `isValid` and the per-field verdicts behind it.
*
* Without it a reset forgets every verdict, which leaves `isValid` false
* until something checks again — the same state a form is in before anything
* has looked at it. With it, a reset to values that are known good does not
* disable a submit button until the next keystroke.
*/
readonly keepIsValid?: boolean,
|};What reset keeps rather than throwing away.
Also what a values re-seed keeps, and that is the same list on purpose: useForm({ values }) is a reset the form performs for you when the object it was given changes identity, so there is one set of rules about what survives one rather than two.
type
WatchInfoexport type WatchInfo = {|
/** The path that changed, or `""` when the whole form was replaced. */
readonly name: FieldPath,
readonly type: "change" | "set" | "reset" | "array",
|};What an imperative watch listener is told.
type
Controlexport type Control<TValues extends FieldValues, TOutput = TValues> = {|
/** Phantom, never called: carries `TValues` so two forms are not one type. */
readonly __values: () => TValues,
readonly __output: () => TOutput,
readonly getValues: () => TValues,
readonly valueAt: (name: FieldPath) => mixed,
readonly setValue: (name: FieldPath, value: mixed, options?: SetValueOptions) => void,
readonly reset: (values?: TValues, options?: ResetOptions) => void,
readonly rulesFor: (name: FieldPath, rules: ValidationRules) => void,
/**
* Tell the store what this render says about `useForm({ disabled })`.
*
* Called from `useForm`'s render body, before anything below it renders, so
* that a field asking [`isDisabled`] during the same pass is answered with
* the form the user is looking at rather than the one the last effect saw.
*/
readonly noteDisabled: (off: boolean) => void,
/** Whether this field is switched off, by its own flag or the form's. */
readonly isDisabled: (name: FieldPath) => boolean,
readonly attach: (name: FieldPath, element: mixed) => void,
readonly detach: (name: FieldPath, element: mixed) => void,
readonly unregister: (names?: FieldPath | $ReadOnlyArray<FieldPath>) => void,
readonly handleChange: (name: FieldPath) => void,
readonly handleControlledChange: (name: FieldPath) => void,
readonly handleBlur: (name: FieldPath) => void,
readonly focus: (name: FieldPath, select?: boolean) => void,
readonly errorAt: (name: FieldPath) => FieldError | void,
readonly setError: (
name: FieldPath,
error: FieldError,
options?: {| readonly shouldFocus?: boolean |},
) => void,
readonly clearErrors: (names?: FieldPath | $ReadOnlyArray<FieldPath>) => void,
readonly trigger: (names?: FieldPath | $ReadOnlyArray<FieldPath>) => Promise<boolean>,
readonly primeValidity: () => void,
readonly submitWith: (
onValid: (values: TOutput, event?: mixed) => mixed,
onInvalid?: (errors: FieldErrors, event?: mixed) => mixed,
) => (event?: mixed) => Promise<void>,
readonly configure: (options: CreateStoreOptions<TValues, TOutput>) => void,
/**
* Run an asynchronous `defaultValues`, once.
*
* Called from an effect rather than from the store's constructor, because
* starting a fetch is a side effect and a `useState` initialiser runs during
* a render React is allowed to throw away. The store guards it so that Strict
* Mode's mount–unmount–mount does not fetch twice.
*/
readonly loadDefaults: () => void,
readonly subscribeFormState: (listener: () => void) => () => void,
readonly formState: () => FormState<TValues>,
readonly fieldStateSnapshot: (
key: string,
names: $ReadOnlyArray<FieldPath> | null,
) => FormState<TValues>,
readonly subscribeWatch: (
key: string,
paths: $ReadOnlyArray<FieldPath>,
listener: () => void,
) => () => void,
readonly watchSnapshot: (key: string, paths: $ReadOnlyArray<FieldPath>) => mixed,
readonly observe: (name: FieldPath) => void,
readonly subscribeObserved: (listener: () => void) => () => void,
readonly observedVersion: () => number,
readonly listen: (
name: FieldPath | null,
listener: (values: TValues, info: WatchInfo) => void,
) => () => void,
readonly arrayRows: (name: FieldPath) => $ReadOnlyArray<FieldArrayRow>,
readonly spliceArray: (
name: FieldPath,
start: number,
remove: number,
inserted: $ReadOnlyArray<mixed>,
) => void,
readonly moveArray: (name: FieldPath, from: number, to: number) => void,
readonly swapArray: (name: FieldPath, left: number, right: number) => void,
readonly updateArray: (name: FieldPath, index: number, value: mixed) => void,
readonly replaceArray: (name: FieldPath, items: $ReadOnlyArray<mixed>) => void,
|};The handle every hook in this package takes.
Its members are this package's business, not an application's: pass it to useFieldArray, useWatch, useFormState and useController, and read nothing off it. It is a plain type rather than an opaque one because Flow's opaque types are opaque to the rest of *this package* as well, and the way around that — routing every operation through free functions here so the other modules can see through it — would drag useFieldArray's implementation into this file in order to hide a type. The comment is the weaker guarantee and it is the honest one.
type
FieldPropsexport type FieldProps = {|
readonly name: string,
readonly ref: (element: mixed) => (() => void) | void,
readonly onChange: (event: mixed) => void,
readonly onBlur: (event: mixed) => void,
/** Present only while the field has an error. */
readonly "aria-invalid": "true" | void,
/** The id of the message element, and only while that element is rendered. */
readonly "aria-describedby": string | void,
/**
* Present when the field is required and the native attribute is not.
*
* Exactly one of the two is emitted, never both. `required` on the element
* already announces the field as required, so adding the ARIA attribute
* beside it is a second copy of the same fact; without `required` — which is
* every form that has not asked for `progressive` — the ARIA attribute is the
* only thing that announces it before the user gets there. `aria-invalid`
* says a field is wrong after it has been checked; this says it is required
* before, which is the announcement that prevents the error rather than
* reporting it.
*/
readonly "aria-required": "true" | void,
/** The five constraint attributes, and only under `progressive`. */
readonly required: boolean | void,
readonly min: number | string | void,
readonly max: number | string | void,
readonly minLength: number | void,
readonly maxLength: number | void,
readonly pattern: string | void,
/** Present while the field, or the whole form, is switched off. */
readonly disabled: boolean | void,
|};What register returns: spread it onto an input, select or textarea.
Exact, and every member is declared even when it is absent, because that is how an exact object type says "this may not be here": the value is void, and React drops an attribute whose value is undefined. A caller cannot spread something extra in to make up for a member this type does not have, which is why the constraint attributes had to be added rather than left to the application.
type
RegisterContextexport type RegisterContext = {|
/**
* Emit the constraint attributes, so the browser enforces them before the
* JavaScript arrives.
*/
readonly progressive: boolean,
/** The whole form is switched off — `useForm({ disabled })`. */
readonly disabled: boolean,
|};What the current render says about the form as a whole.
type
ErrorPropsexport type ErrorProps = {|
readonly id: string,
readonly role: "alert",
|};What errorProps returns: spread it onto the element holding the message.
@uniflowed/form/controllertype
ControlledFieldexport type ControlledField = {|
readonly name: string,
readonly value: mixed,
readonly onChange: (value: mixed) => void,
readonly onBlur: () => void,
readonly ref: (element: mixed) => void,
/**
* Whether the field is switched off, by its own option or by the form's.
*
* Spread onto whatever the caller is wrapping, the way `register` puts it on
* an `input`. It is here rather than left to the caller because the field
* does not know about `useForm({ disabled })` and this does.
*
* Both halves are current: this field's own option because it is recorded
* during this render, and the form's because `useForm` leaves it in the store
* during its own — which is earlier in the same pass. So a form switched off
* while it saves reaches a controlled field in the commit that switched it
* off, exactly as it reaches `register`, and neither has to be told twice.
*/
readonly disabled: boolean,
|};The props a controlled component is handed.
type
ControlledFieldStateexport type ControlledFieldState = {|
readonly invalid: boolean,
readonly isDirty: boolean,
readonly isTouched: boolean,
readonly error: mixed,
|};What is currently true of the field, for rendering its state.
hook
useControllerexport hook useController<TValues extends FieldValues, TOutput>(
options: UseControllerOptions<TValues, TOutput>,
): UseControllerReturn { ... }Bind one field to a component that owns its own value.
onChange takes the value, not an event — because the components this exists for hand back a Date, an option object or a number, and unwrapping event.target.value is exactly the thing they are not doing. An event is still accepted, because a caller who wraps a plain <input> with this should not have to think about it.
component
Controllerexport component Controller<TValues extends FieldValues, TOutput = TValues>(
control: Control<TValues, TOutput>,
name: FieldPath,
rules?: ValidationRules,
defaultValue?: mixed,
disabled?: boolean,
render: (bound: UseControllerReturn) => React.Node,
) { ... }useController as a component, for a render prop.
The same hook, for the places where a hook cannot go: a list of fields built from a configuration object, where each entry needs its own subscription and a loop cannot call a hook.
@uniflowed/form/fieldhook
useFieldSourceexport hook useFieldSource<
TValues extends FieldValues,
TOutput,
TName extends FieldSegment<TValues>,
>(form: UseFormReturn<TValues, TOutput>, name: TName, rules?: ValidationRules): FieldSource { ... }Everything a Field.Root needs to know about one of this form's fields.
rules are recorded the same way register(name, rules) records them, so a field declared through a Field is validated like any other; the store documents why that write during a render is safe.
@uniflowed/form/field-arrayhook
useFieldArrayexport hook useFieldArray<TValues extends FieldValues, TOutput>(
options: UseFieldArrayOptions<TValues, TOutput>,
): UseFieldArrayReturn { ... }Rows for the array at name.
The list re-renders when the array's *shape* changes — a row added, removed, moved — and when a value inside it is written through setValue or update. It does not re-render when somebody types into one of the rows, because the text is in the DOM and nothing in this list is showing it.
@uniflowed/form/resolvertype
ResolverErrorsexport type ResolverErrors = { readonly [string]: FieldError, ... };Errors by field path — the same strings register was given.
type
ResolverResultexport type ResolverResult<TOut> =
| {| readonly values: TOut, readonly errors?: void |}
| {| readonly values?: void, readonly errors: ResolverErrors |};What a resolver answers with.
values is present only when there were no errors, and a resolver that reports errors need not produce a value at all. The union is exact so that checking errors narrows values for the caller.
type
Resolverexport type Resolver<TIn extends FieldValues, TOut = TIn> = (
values: TIn,
context: mixed,
) => ResolverResult<TOut> | Promise<ResolverResult<TOut>>;Validate a whole form.
Given every value the form holds, plus whatever useForm({ context }) was passed — a locale, a tenant, a set of already-taken names — so that a schema that depends on something outside the form does not have to close over it at module scope.
May be synchronous or return a promise. Both are supported because a schema check is usually the former and a uniqueness check is always the latter, and forcing the sync case through a promise costs a microtask on every keystroke in onChange mode.
function
runResolverexport function runResolver<TIn extends FieldValues, TOut>(
resolver: Resolver<TIn, TOut>,
values: TIn,
context: mixed,
): ResolverResult<TOut> | Promise<ResolverResult<TOut>> { ... }Run a resolver and normalise its answer.
Awaiting a value that is not a promise is a microtask a form in onChange mode pays on every keystroke, so a synchronous resolver is returned synchronously and the caller decides whether it has to wait. That is the only reason this is not a one-line await.
function
errorsOfexport function errorsOf<TOut>(result: ResolverResult<TOut>): ResolverErrors { ... }The errors a result carries, as a plain map.
function
collectErrorsexport function collectErrors(
issues: $ReadOnlyArray<{|
readonly path: string,
readonly type: string,
readonly message: string,
|}>,
): ResolverErrors { ... }Build resolver errors from path -> message, keeping the first per path.
An adapter's job is nearly always this: a schema reports a list of issues, several of which can land on the same field, and a form shows one message per field. Keeping the *first* rather than the last matches how schema libraries order their issues — outermost check first — so "expected string" wins over a length complaint about a value that is not a string.
@uniflowed/form/rulestype
Ruleexport type Rule<TLimit> = TLimit | {| readonly value: TLimit, readonly message: string |};A rule given either bare or with the message it should report.
type
Validateexport type Validate = (
value: mixed,
values: FieldValues,
) => boolean | string | void | Promise<boolean | string | void>;A caller's own check.
Returning true (or nothing) accepts; returning false rejects with the default message; returning a string rejects with that string. A promise is awaited, which is what makes "is this username taken" an ordinary rule rather than a reason to leave the library.
type
ValidationRulesexport type ValidationRules = {|
readonly required?: boolean | string | {| readonly value: boolean, readonly message: string |},
readonly min?: Rule<number | string>,
readonly max?: Rule<number | string>,
readonly minLength?: Rule<number>,
readonly maxLength?: Rule<number>,
readonly pattern?: Rule<RegExp>,
/** One check, or several keyed by name so the error says which one failed. */
readonly validate?: Validate | { readonly [string]: Validate, ... },
/** Read the control's text as a number. */
readonly valueAsNumber?: boolean,
/** Read the control's text as a `Date`. */
readonly valueAsDate?: boolean,
/** Convert the raw value however the caller likes; runs last. */
readonly setValueAs?: (value: mixed) => mixed,
/**
* Other fields whose errors are re-checked when this one changes.
*
* For the rules that are about a pair: a confirmation that must match a
* password, an end date that must follow a start date. Without it the second
* field keeps the error it earned before the first one was corrected.
*/
readonly deps?: string | $ReadOnlyArray<string>,
/**
* Switch the field off: not validated, not dirtied, and absent from the
* values a submit hands over.
*
* Not a validation rule, and it is in a type named for them because this is
* `register`'s second argument and React Hook Form puts it here too. The
* alternative — a third parameter, or a second options type the caller has to
* know the difference between — costs every call site more than the misnomer
* costs this one.
*/
readonly disabled?: boolean,
|};What a field is checked against, and how its raw value is read.
type
FieldConstraintsexport type FieldConstraints = {|
readonly required: boolean | void,
readonly min: number | string | void,
readonly max: number | string | void,
readonly minLength: number | void,
readonly maxLength: number | void,
readonly pattern: string | void,
|};The constraint attributes a rule set corresponds to.
void for a rule that was not given, because these are spread onto an element and React drops an attribute whose value is undefined.
function
isRequiredexport function isRequired(rules: ValidationRules): boolean { ... }Whether required was asked for, in any of the three shapes it takes.
function
constraintsOfexport function constraintsOf(rules: ValidationRules): FieldConstraints { ... }What register puts on the element, given the rules it was handed.
The translation is exact for five of the six and approximate for pattern, which is the reason this is opt-in. ValidationRules.pattern is a RegExp and the HTML attribute is a string that the browser anchors implicitly and matches with the v flag's dialect. re.source is right for the patterns people write — character classes, \d, alternation — and wrong for some: a leading ^ and a trailing $ become redundant rather than harmful, but a u-flag escape the v dialect reads differently is a pattern that means one thing to runRules and another to the browser. Emitting it verbatim and saying so here is honest; claiming the two are equivalent is not.
Flags are dropped, because the attribute has nowhere to put them. A case-insensitive pattern therefore becomes case-sensitive in the browser's check and stays case-insensitive in this package's — write the insensitivity into the pattern ([aA]) if both have to agree.
type
FieldErrorexport type FieldError = {|
/** The rule that rejected the value: `"required"`, `"pattern"`, a
* `validate` key, or whatever a resolver reported. */
readonly type: string,
readonly message: string,
|};One field's error: what failed, and what to show for it.
function
whenSettledexport function whenSettled<TValue, TNext>(
value: TValue | Promise<TValue>,
next: (value: TValue) => TNext,
): TNext | Promise<TNext> { ... }Continue with next, whether the answer arrived or was promised.
Every check in a form is one of two shapes: a comparison that answers now, and a request that answers later. Awaiting both would be simpler to write and would put a microtask between every keystroke and the error that keystroke cleared — which is a render the form did not need, in the mode where renders are the thing being avoided. So the synchronous case stays synchronous all the way up to the store, and this is the joint it turns on.
It lives here rather than in a module of loose helpers because this is where the two shapes first meet: validate is the first thing in the package that a caller may write either way.
function
runRulesexport function runRules(
rules: ValidationRules,
value: mixed,
values: FieldValues,
): FieldError | null | Promise<FieldError | null> { ... }Check value against rules, in the order a reader would.
required first, because "this is empty" outranks every other complaint about an empty value. Then the limits, then the pattern, then the caller's own checks — so a field that is both too short and malformed reports being too short, which is the problem the user has to fix first.
Returns the first failure. A form that reported every failing rule for one field would have to choose which to show anyway, and choosing here means the choice is documented rather than implicit in a render.
Synchronous unless the caller's own validate is not — see [whenSettled].
function
dependenciesOfexport function dependenciesOf(rules: ValidationRules): $ReadOnlyArray<string> { ... }The fields whose errors should be re-checked when name changes.
function
transformOfexport function transformOf(rules: ValidationRules): {|
readonly valueAsNumber?: boolean,
readonly valueAsDate?: boolean,
readonly setValueAs?: (value: mixed) => mixed,
|} { ... }The subset of a rule set that says how to read the control's raw value.
@uniflowed/form/use-formtype
WatchListenerexport type WatchListener<TValues> = (values: TValues, info: WatchInfo) => void;What an imperative watch callback is given.
type
FieldStateexport type FieldState = {|
readonly invalid: boolean,
readonly isDirty: boolean,
readonly isTouched: boolean,
readonly error: mixed,
|};What getFieldState answers about one field.
type
GetValuesexport type GetValues<TValues> = (<K1 extends FieldSegment<TValues>>(k1: K1) => TValues[K1]) &
(<K1 extends FieldSegment<TValues>, K2 extends FieldSegment<TValues[K1]>>(
k1: K1,
k2: K2,
) => TValues[K1][K2]) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
>(
k1: K1,
k2: K2,
k3: K3,
) => TValues[K1][K2][K3]) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
K4 extends FieldSegment<TValues[K1][K2][K3]>,
>(
k1: K1,
k2: K2,
k3: K3,
k4: K4,
) => TValues[K1][K2][K3][K4]) &
(() => TValues) &
((name: FieldPath) => mixed);getValues, in its six shapes.
The four typed arms, then the whole-form read, then the dotted string. The order is the resolution order and the last two are unchanged by any of this: getValues() is TValues and getValues("items.0.price") is mixed, exactly as before.
type
SetValueexport type SetValue<TValues> = (<K1 extends FieldSegment<TValues>>(
path: [K1],
value: TValues[K1],
options?: SetValueOptions,
) => void) &
(<K1 extends FieldSegment<TValues>, K2 extends FieldSegment<TValues[K1]>>(
path: [K1, K2],
value: TValues[K1][K2],
options?: SetValueOptions,
) => void) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
>(
path: [K1, K2, K3],
value: TValues[K1][K2][K3],
options?: SetValueOptions,
) => void) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
K4 extends FieldSegment<TValues[K1][K2][K3]>,
>(
path: [K1, K2, K3, K4],
value: TValues[K1][K2][K3][K4],
options?: SetValueOptions,
) => void) &
((name: FieldPath, value: mixed, options?: SetValueOptions) => void);setValue, in its five shapes.
The path is an array here rather than a list of arguments, for the reason above it: the value follows the path, and three string arguments would be ambiguous at run time. What the typed arms buy is the *value* as well as the path — setValue(["items", 0, "price"], "cheap") is refused, which is the half of this that a wrong read cannot tell you about.
type
GetFieldStateexport type GetFieldState<TValues> = (<K1 extends FieldSegment<TValues>>(k1: K1) => FieldState) &
(<K1 extends FieldSegment<TValues>, K2 extends FieldSegment<TValues[K1]>>(
k1: K1,
k2: K2,
) => FieldState) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
>(
k1: K1,
k2: K2,
k3: K3,
) => FieldState) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
K4 extends FieldSegment<TValues[K1][K2][K3]>,
>(
k1: K1,
k2: K2,
k3: K3,
k4: K4,
) => FieldState) &
((name: FieldPath) => FieldState);getFieldState, in its five shapes.
No value type is involved — a FieldState is the same shape whatever the field holds — so what the typed arms check is the path and nothing else. That is still the answer to "how do I read the error for a nested field with the checker's help": the errors stay one flat map keyed by the dotted path, for the reason resolver.js gives, and this is the accessor that will not let you misspell one.
type
Watchexport type Watch<TValues> = (<K1 extends FieldSegment<TValues>>(k1: K1) => TValues[K1]) &
(<K1 extends FieldSegment<TValues>, K2 extends FieldSegment<TValues[K1]>>(
k1: K1,
k2: K2,
) => TValues[K1][K2]) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
>(
k1: K1,
k2: K2,
k3: K3,
) => TValues[K1][K2][K3]) &
(<
K1 extends FieldSegment<TValues>,
K2 extends FieldSegment<TValues[K1]>,
K3 extends FieldSegment<TValues[K1][K2]>,
K4 extends FieldSegment<TValues[K1][K2][K3]>,
>(
k1: K1,
k2: K2,
k3: K3,
k4: K4,
) => TValues[K1][K2][K3][K4]) &
(() => TValues) &
((name: FieldPath) => mixed) &
((names: $ReadOnlyArray<FieldPath>) => $ReadOnlyArray<mixed>) &
((listener: WatchListener<TValues>) => () => void) &
((name: FieldPath, listener: WatchListener<TValues>) => () => void);watch, in its nine shapes.
An intersection rather than one signature, so the reactive reads and the imperative subscription are told apart by the checker instead of by a comment — const email = watch("email") and const stop = watch("email", save) are different enough that inferring mixed for both would be no help at all.
The four typed arms come first, and the two-argument one sits above (name, listener) without disturbing it: a listener is a function and a segment is not, so the arms are told apart by the checker for the same reason the run time tells them apart with typeof.
watch(["a", "b"]) is *not* a path and never becomes one. It means the two fields a and b, which is the meaning it has here and in React Hook Form, and a library that quietly changed it into a.b would change what a working form watched. That is why the readers take segments as arguments: the array slot in this signature was already spoken for.
hook
useFormexport hook useForm<TValues extends FieldValues, TOutput = TValues>(
options?: UseFormOptions<TValues, TOutput>,
): UseFormReturn<TValues, TOutput> { ... }Create a form.
The store is built in a useState initialiser, which is React's supported way to make something exactly once: an initialiser runs on the first render of the component and is not re-run, and — unlike a useRef filled in during render — it does not have to be guarded against Strict Mode's second pass.
component
FormProviderexport component FormProvider<TValues extends FieldValues, TOutput = TValues>(
form: UseFormReturn<TValues, TOutput>,
children: React.Node,
) { ... }Hand the whole form to everything below it.
The alternative is threading control through every layer, which is fine for two levels and not for five. What is passed is the entire useForm return value, so a field component can call useFormContext() where it would otherwise have called useForm().
hook
useFormContextexport hook useFormContext<TValues extends FieldValues, TOutput = TValues>(): UseFormReturn<
TValues,
TOutput,
> { ... }The form a FormProvider above put there.
Raises rather than returning null: a field that renders without a form would render a control wired to nothing, and would look entirely correct.
Note that watch(name)'s *reactive* form belongs to the component that called useForm. Read a value from down here with useWatch({ control }), which subscribes this component and leaves the form alone.
@uniflowed/form/validatorfunction
errorsFromIssuesexport function errorsFromIssues(issues: $ReadOnlyArray<Issue>): ResolverErrors { ... }Field errors for a list of validator issues.
Exported because the same translation is what a Server Action needs when it validates the same schema again on the server and sends the failures back: feed the issues through this and the keys line up with the fields the form already has, so setError can put each message where it belongs.
function
validatorResolverexport function validatorResolver<TValues extends FieldValues, TOutput>(
schema: Schema<TOutput>,
): Resolver<TValues, TOutput> { ... }A resolver that validates the form against schema.
Synchronous when the schema is, which is the case a form in onChange mode runs on every keystroke: a promise there would cost a microtask and a pair of isValidating renders for an answer that was already available. The Resolver contract allows either, and runResolver returns a synchronous answer synchronously, so nothing downstream pays for the choice.
A schema with a checkAsync in it — "is this handle taken" — is asynchronous from that step up, and @uniflowed/validator says so through isAsync when the schema is built rather than when it is run. So the branch is taken once, here, and not per keystroke; and safeParse never gets a schema it would refuse. The form already handles an asynchronous resolver, including discarding a slower answer that a newer one has overtaken.
@uniflowed/form/watchhook
useWatchexport hook useWatch<TValues extends FieldValues, TOutput, TPath extends FieldSegments = []>(
options: UseWatchOptions<TValues, TOutput, TPath>,
): ValueAtPath<TValues, TPath> { ... }The value at a path, re-rendering this component when it changes.
One name answers with the value; several answer with a frozen tuple in the order they were asked for; none answers with the whole values object, which changes identity on every write and so re-renders on every one. A path answers with the value *at its type* — see [ValueAtPath].
# Why path is typed differently from getValues's segments
getValues is a field on an object, so its type can be an intersection of one signature per path length, and each of those bounds its segments by the keys of what the segment before it landed on. A hook is a declaration, and a declaration has one signature: there is nowhere to put the other three.
So path is one generic tuple and the value is computed from it by [ValueAtPath], which is a conditional type rather than a bound. The type it produces is the same. What differs is *when* a misspelt segment is reported: a bound is checked at the call whatever the result is used for, and a conditional is evaluated against the type the result is wanted at — so const city: string = useWatch({ control, path: ["address", "cty"] }) is an error and const city: mixed = ... is not. Both are better than mixed everywhere, and the difference is written down here rather than found.
hook
useFormStateexport hook useFormState<TValues extends FieldValues, TOutput>(
options: UseFormStateOptions<TValues, TOutput>,
): FormState<TValues> { ... }A form's state, subscribed from wherever it is rendered.
The same object useForm().formState gives, but read in *this* component, so a change re-renders this component instead of the whole form. That is the whole reason to reach for it: a form with forty message components pays only for the one whose error changed.