Capítulo 37 de 39

Chapter 37: useLens

Core Idea

useLens (from the separate @hookform/lenses package) creates a type-safe functional "lens" connected to a form's control, letting you drill into, reshape, and pass around nested slices of form data without hand-writing Path<T> strings or losing type inference across component boundaries.

Key Concepts

  • Installation: npm install @hookform/lenses — not part of core react-hook-form.
  • useLens({ control }, deps?): builds a root lens from a form's control; an optional dependency array clears the lens cache and rebuilds when external state changes.
  • focus(path): the primary drill-down operation — lens.focus("profile.email") or lens.focus("users.0.name") — fully typed, autocompletes valid paths and rejects invalid ones.
  • reflect(fn): reshapes a lens into a new structure (rename fields, merge two lenses, restructure an array) for passing into a shared component that expects a different shape than the source data.
  • map(fields, callback): iterates an array lens together with useFieldArray's fields, yielding (value, itemLens, index, array, originLens) per row.
  • interop(): the bridge back to plain React Hook Form — called with no arguments returns { control, name }; called with a callback passes (control, name) directly; its return value can be spread onto register or passed straight into useController/useFieldArray.
  • narrow / assert / defined / cast: type-only escape hatches for union types — narrow<T>() or narrow(discriminant, value) produces a differently-typed lens without a runtime check; assert does the same but mutates the current lens's inferred type in place; defined() is shorthand for narrowing away null/undefined; cast<T>() forces an arbitrary type change with no safety at all. None of these validate anything at runtime — pair them with your own runtime check.

Code Examples

const { control } = useForm<MyFormData>()
const lens = useLens({ control })

function ContactsList({ lens }: { lens: Lens<Contact[]> }) {
  const { fields, append, remove } = useFieldArray(lens.interop())

  return (
    <div>
      <button onClick={() => append({ name: "", email: "" })}>Add</button>
      {lens.map(fields, (value, l, index) => (
        <div key={value.id}>
          <button onClick={() => remove(index)}>Remove</button>
          <input {...l.focus("name").interop((ctrl, name) => ctrl.register(name))} />
        </div>
      ))}
    </div>
  )
}
  • What it demonstrates: focus + interop + useFieldArray combined to build a typed, reusable row component for a dynamic array, without manually templating field-path strings.

Reference Tables

MethodPurposeReturns
focusdrill into a field pathLens<PathValue>
reflectreshape/rename/merge lens structureLens<NewStructure>
mapiterate an array lens with useFieldArrayR[]
interopbridge to RHF's { control, name }object or callback result
narrowtype-only union narrowingLens<SubType>
asserttype-only assertion, mutates current lens's typevoid
definednarrow away null/undefinedLens<NonNullable<T>>
castunchecked forced type changeLens<NewType>

Anti-patterns

  • Using cast without a prior runtime check: the type system will trust it completely while the actual value can be anything — reserve it for genuinely untyped boundaries (API responses) after validating shape.
  • Array reflect with more than one template item: array reflection expects exactly one item as the shape template.
  • Treating narrow/assert as runtime validation: both are compile-time only; a wrong assumption produces a type-safe-looking bug, not a caught error.

Key Takeaways

  1. useLens trades hand-written Path<T> strings for composable, type-checked focus/reflect calls — most useful once forms get deeply nested or components need to accept "some slice of the form" generically.
  2. interop() is always the exit point back to vanilla RHF (register, useController, useFieldArray).
  3. narrow/assert/defined/cast only affect what TypeScript believes — always back them with your own runtime check.

Connects To

  • usefieldarray: lens.map is built directly on top of it.
  • usecontroller: lens.interop()'s return value plugs straight into it.
  • useform-control: the control object every lens ultimately wraps.