# useLens

> Type-safe functional lenses that scope form context to a nested field path, reducing boilerplate in deeply nested React Hook Forms.

Source: https://react-hook-form.com/docs/uselens

## `useLens`

React Hook Form Lenses is a powerful TypeScript-first library that brings the elegance of functional lenses to form development. It provides type-safe manipulation of nested structures, enabling developers to control and transform complex data precisely and with ease.

`useLens` is a custom hook that creates a lens instance connected to your React Hook Form control, enabling type-safe focusing, transformation, and manipulation of deeply nested form data structures through functional programming concepts.

### Installation

---

```bash
npm install @hookform/lenses
```

### Features

---

- **Type-Safe Form State**: Focus on specific parts of your data with full TypeScript support and precise type inference
- **Functional Lenses**: Build complex transformations through composable lens operations
- **Deep Structure Support**: Handle deeply nested structures and arrays elegantly with specialized operations
- **Seamless Integration**: Work smoothly with React Hook Form's Control API and existing functionality
- **Optimized Performance**: Each lens is cached and reused for optimal efficiency
- **Array Handling**: Specialized support for dynamic fields with type-safe mapping
- **Composable API**: Build complex transformations through elegant lens composition

### Props

---

The `useLens` hook accepts the following configuration:

#### `control`: [Control](https://react-hook-form.com/ts#Control)\<TFieldValues>

**Required.** The control object from React Hook Form's `useForm` hook. This connects your lens to the form management system.

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

#### Dependencies Array (Optional)

You can optionally pass a dependency array as the second parameter to clear the lens cache and re-create all lenses when dependencies change:

```tsx
const lens = useLens({ control }, [dependencies])
```

This is useful when you need to reset the entire lens cache based on external state changes.

### Return

---

The following table contains information about the main types and operations available on lens instances:

**Core Types:**

`Lens<T>` - The main lens type that provides different operations based on the field type you're working with:

```ts
type LensWithArray = Lens<string[]>
type LensWithObject = Lens<{ name: string; age: number }>
type LensWithPrimitive = Lens<string>
```

**Main Operations:**

These are the core methods available on every lens instance:

| Method                | Description                                    | Returns              |
| --------------------- | ---------------------------------------------- | -------------------- |
| [`focus`](#focus)     | Focuses on a specific field path               | `Lens<PathValue>`    |
| [`reflect`](#reflect) | Transform and reshape lens structure           | `Lens<NewStructure>` |
| [`map`](#map)         | Iterate over array fields (with useFieldArray) | `R[]`                |
| [`interop`](#interop) | Connect to React Hook Form's control system    | `{ control, name }`  |
| [`narrow`](#narrow)   | Type-safe narrowing of union types             | `Lens<SubType>`      |
| [`assert`](#assert)   | Runtime type assertion for type narrowing      | `void`               |
| [`defined`](#defined) | Exclude null and undefined from lens type      | `Lens<NonNullable>`  |
| [`cast`](#cast)       | Force type change (unsafe)                     | `Lens<NewType>`      |

---

### focus

---

Creates a new lens focused on a specific path. This is the primary method for drilling down into your data structure.

```tsx
// Type-safe path focusing
const profileLens = lens.focus("profile")
const emailLens = lens.focus("profile.email")
const arrayItemLens = lens.focus("users.0.name")
```

**Array focusing:**

```tsx
function ContactsList({ lens }: { lens: Lens<Contact[]> }) {
  // Focus on specific array index
  const firstContact = lens.focus("0")
  const secondContactName = lens.focus("1.name")

  return (
    <div>
      <ContactForm lens={firstContact} />
      <Controller
        {...secondContactName.interop()}
        render={({ field }) => <input {...field} />}
      />
    </div>
  )
}
```

> **Important: TypeScript Support**
>
> The `focus` method provides full TypeScript support with autocompletion and type checking:
>
> - Autocomplete available field paths
> - Type errors for non-existent paths
> - Inferred return types based on focused field
>

### reflect

---

Transforms the lens structure with complete type inference. This is useful when you want to create a new lens from an existing one with a different shape to pass to a shared component.

The first argument is a proxy with a dictionary of lenses. Note that lens instantiation happens only upon property access. The second argument is the original lens.

#### Object Reflection

```tsx
const contactLens = lens.reflect(({ profile }) => ({
  name: profile.focus("contact.firstName"),
  phoneNumber: profile.focus("contact.phone"),
}))

<SharedComponent lens={contactLens} />

function SharedComponent({
  lens,
}: {
  lens: Lens<{ name: string; phoneNumber: string }>
}) {
  return (
    <div>
      <Controller
        {...lens.focus("name").interop()}
        render={({ field }) => <input {...field} />}
      />
      <Controller
        {...lens.focus("phoneNumber").interop()}
        render={({ field }) => <input {...field} />}
      />
    </div>
  )
}
```

**Alternative syntax using the lens parameter:**

You can also use the second parameter (the original lens) directly:

```tsx
const contactLens = lens.reflect((_, l) => ({
  name: l.focus("profile.contact.firstName"),
  phoneNumber: l.focus("profile.contact.phone"),
}))

<SharedComponent lens={contactLens} />

function SharedComponent({
  lens,
}: {
  lens: Lens<{ name: string; phoneNumber: string }>
}) {
  // ...
}
```

#### Array Reflection

You can restructure array lenses:

```tsx
function ArrayComponent({ lens }: { lens: Lens<{ value: string }[]> }) {
  return (
    <AnotherComponent lens={lens.reflect(({ value }) => [{ data: value }])} />
  )
}

function AnotherComponent({ lens }: { lens: Lens<{ data: string }[]> }) {
  // ...
}
```

> **Important**
>
> Note that for array reflection, you must pass an array with a single item as the template.
>

#### Merging Lenses

You can use `reflect` to merge two lenses into one:

```tsx
function Component({
  lensA,
  lensB,
}: {
  lensA: Lens<{ firstName: string }>
  lensB: Lens<{ lastName: string }>
}) {
  const combined = lensA.reflect((_, l) => ({
    firstName: l.focus("firstName"),
    lastName: lensB.focus("lastName"),
  }))

  return <PersonForm lens={combined} />
}
```

Keep in mind that in such cases, the function passed to `reflect` is no longer pure.

#### Spread Operator Support

You can use spread in reflect if you want to leave other properties as is. At runtime, the first argument is just a proxy that calls `focus` on the original lens. This is useful for proper typing when you need to change the property names for only a few fields and leave the rest unchanged:

```tsx
function Component({
  lens,
}: {
  lens: Lens<{ firstName: string; lastName: string; age: number }>
}) {
  return (
    <PersonForm
      lens={lens.reflect(({ firstName, lastName, ...rest }) => ({
        ...rest,
        name: firstName,
        surname: lastName,
      }))}
    />
  )
}
```

### map

---

Maps over array fields with `useFieldArray` integration. This method requires the `fields` property from `useFieldArray`.

```tsx
import { Controller } from "react-hook-form"
import { useFieldArray } from "@hookform/lenses/rhf"

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

  return (
    <div>
      <button onClick={() => append({ name: "", email: "" })}>
        Add Contact
      </button>

      {lens.map(fields, (value, l, index) => (
        <div key={value.id}>
          <button onClick={() => remove(index)}>Remove</button>
          <ContactForm lens={l} />
        </div>
      ))}
    </div>
  )
}

function ContactForm({
  lens,
}: {
  lens: Lens<{ name: string; email: string }>
}) {
  return (
    <div>
      <Controller
        {...lens.focus("name").interop()}
        render={({ field }) => <input {...field} />}
      />
      <Controller
        {...lens.focus("email").interop()}
        render={({ field }) => <input {...field} />}
      />
    </div>
  )
}
```

**Map callback parameters:**

| Parameter    | Type        | Description                            |
| ------------ | ----------- | -------------------------------------- |
| `value`      | `T`         | The current field value with `id`      |
| `lens`       | `Lens<T>`   | Lens focused on the current array item |
| `index`      | `number`    | Current array index                    |
| `array`      | `T[]`       | The complete array                     |
| `originLens` | `Lens<T[]>` | The original array lens                |

### interop

---

The `interop` method provides seamless integration with React Hook Form by exposing the underlying `control` and `name` properties. This allows you to connect your lens to React Hook Form's control API.

#### First Variant: Object Return

The first variant involves calling `interop()` without arguments, which returns an object containing the `control` and `name` properties for React Hook Form:

```tsx
const { control, name } = lens.interop()
const { field } = useController({ control, name })

return <input {...field} />
```

#### Second Variant: Callback Function

The second variant passes a callback function to `interop` which receives the `control` and `name` properties as arguments. This allows you to work with these properties directly within the callback scope:

```tsx
return (
  <form onSubmit={handleSubmit(console.log)}>
    {lens.interop((control, name) => (
      <Controller
        control={control}
        name={name}
        render={({ field }) => <input {...field} />}
      />
    ))}
    <input type="submit" />
  </form>
)
```

#### Integration with useController

The `interop` method's return value can be passed directly to the `useController` hook from React Hook Form, providing seamless integration:

```tsx
import { useController } from "react-hook-form"

function ControlledInput({ lens }: { lens: Lens<string> }) {
  const { field, fieldState } = useController(lens.interop())

  return (
    <div>
      <input {...field} />
      {fieldState.error && <p>{fieldState.error.message}</p>}
    </div>
  )
}
```

> **Info: Type System Escape Hatches**
>
> The `narrow`, `assert`, `defined`, and `cast` methods serve as escape hatches for current TypeScript limitations with lens type compatibility. These methods address scenarios where you need to pass lenses with wider types to components expecting narrower types.
>
> These workarounds will become less necessary once [issue #38](https://github.com/react-hook-form/lenses/issues/38) is resolved, which aims to improve lens type variance to allow more natural type narrowing and component composition.
>

### narrow

---

The `narrow` method provides type-safe narrowing of union types, allowing you to tell the type system which branch of a union you want to work with. This is particularly useful when working with discriminated unions or optional values.

#### Manual Type Narrowing

Use the single generic parameter to manually narrow the type when you know (by external logic) what the value should be:

```tsx
// Lens<string | number>
const unionLens = lens.focus("optionalField")

// Narrow to string when you know it's a string
const stringLens = unionLens.narrow<string>()
// Now: Lens<string>
```

#### Discriminated Union Narrowing

Use the discriminant overload to narrow based on a specific property value:

```tsx
type Animal = { type: "dog"; breed: string } | { type: "cat"; indoor: boolean }

const animalLens: Lens<Animal> = lens.focus("pet")

// Narrow to Dog type using discriminant
const dogLens = animalLens.narrow("type", "dog")
// Now: Lens<{ type: 'dog'; breed: string }>

const breedLens = dogLens.focus("breed")
// Type-safe access to dog-specific properties
```

> **Important: Type Safety**
>
> The `narrow` method performs type-level operations only. It doesn't validate the runtime value - use it when you have external guarantees about the value's type (e.g., from validation, conditional rendering, or runtime checks).
>

### assert

---

The `assert` method provides runtime type assertions that convince TypeScript the current lens is already the desired subtype. Unlike `narrow`, this is a type assertion that modifies the current lens instance.

#### Manual Type Assertion

Use the generic parameter to assert the lens is already the desired type:

```tsx
function processString(lens: Lens<string>) {
  // Work with string lens
}

const maybeLens: Lens<string | undefined> = lens.focus("optional")

// After your runtime check
if (value !== undefined) {
  maybeLens.assert<string>()
  processString(maybeLens) // Now TypeScript knows it's Lens<string>
}
```

#### Discriminant-Based Assertion

Use the discriminant overload when you're in a conditional branch:

```tsx
type Status =
  | { type: "loading" }
  | { type: "success"; data: string }
  | { type: "error"; message: string }

const statusLens: Lens<Status> = lens.focus("status")

// In a conditional branch
if (selected.type === "success") {
  statusLens.assert("type", "success")
  // Within this block, statusLens is Lens<{ type: 'success'; data: string }>
  const dataLens = statusLens.focus("data") // Type-safe access
}
```

> **Warning: Runtime Safety**
>
> `assert` is a type-only operation that doesn't perform runtime validation. Ensure your assertions are backed by proper runtime checks to avoid type safety violations.
>

### defined

---

The `defined` method is a convenience function that narrows the lens type to exclude `null` and `undefined` values. This is equivalent to using `narrow<NonNullable<T>>()` but provides a more expressive API.

```tsx
const optionalLens: Lens<string | null | undefined> = lens.focus("optional")

// Remove null and undefined from the type
const definedLens = optionalLens.defined()
// Now: Lens<string>

// Use after validation
if (value != null) {
  const safeLens = optionalLens.defined()
  // Work with guaranteed non-null value
}
```

**Common use cases:**

```tsx
// Form validation
const emailLens = lens.focus("email") // Lens<string | undefined>

function validateEmail(email: string) {
  // validation logic
}

// After confirming value exists
if (formState.isValid) {
  const validEmailLens = emailLens.defined()
  // Pass to functions expecting non-null values (getValues from useForm)
  validateEmail(getValues(validEmailLens.interop().name))
}
```

### cast

---

The `cast` method forcefully changes the lens type to a new type, regardless of compatibility with the original type. This is a powerful but potentially **unsafe** operation that should be used with extreme caution.

```tsx
// Cast from unknown/any to specific type
const unknownLens: Lens<unknown> = lens.focus("dynamicData")
const stringLens = unknownLens.cast<string>()
// Now: Lens<string>

// Cast between incompatible types (dangerous!)
const numberLens: Lens<number> = lens.focus("count")
const stringLens = numberLens.cast<string>()
// Type system now thinks it's Lens<string>, but runtime value is still number
```

**Safe usage patterns:**

```tsx
// Working with external APIs returning 'any'
function processApiData(data: any) {
  const apiLens = LensCore.create(data)

  // Cast after runtime validation
  if (typeof data.user === "object" && data.user !== null) {
    const userLens = apiLens.focus("user").cast<User>()
    return <UserProfile lens={userLens} />
  }
}

// Type narrowing when you have more information
interface BaseConfig {
  type: string
}

interface DatabaseConfig extends BaseConfig {
  type: "database"
  connectionString: string
}

const configLens: Lens<BaseConfig> = lens.focus("config")

// After checking the type at runtime
if (config.type === "database") {
  const dbConfigLens = configLens.cast<DatabaseConfig>()
  // Now can access database-specific properties
}
```

> **Danger: Use with Extreme Caution**
>
> `cast` bypasses TypeScript's type system entirely. It can lead to runtime errors if the underlying data doesn't match the asserted type. Always validate data at runtime before using `cast`, or prefer safer alternatives like `narrow` when possible.
>

### useFieldArray

---

Import the enhanced `useFieldArray` from `@hookform/lenses/rhf` for seamless array handling with lenses.

```tsx
import { Controller } from "react-hook-form"
import { useFieldArray } from "@hookform/lenses/rhf"

function DynamicForm({
  lens,
}: {
  lens: Lens<{ items: { name: string; value: number }[] }>
}) {
  const itemsLens = lens.focus("items")
  const { fields, append, remove, move } = useFieldArray(itemsLens.interop())

  return (
    <div>
      <button onClick={() => append({ name: "", value: 0 })}>Add Item</button>

      {itemsLens.map(fields, (field, itemLens, index) => (
        <div key={field.id}>
          <Controller
            {...itemLens.focus("name").interop()}
            render={({ field }) => <input {...field} />}
          />
          <Controller
            {...itemLens.focus("value").interop()}
            render={({ field }) => (
              <input
                type="number"
                {...field}
                onChange={(e) => field.onChange(e.target.valueAsNumber)}
              />
            )}
          />
          <button onClick={() => remove(index)}>Remove</button>
          {index > 0 && (
            <button onClick={() => move(index, index - 1)}>Move Up</button>
          )}
        </div>
      ))}
    </div>
  )
}
```

> **Important: Rules**
>
> - The `control` parameter is required and must be from React Hook Form's `useForm` hook
> - Each lens is cached and reused for optimal performance - focusing on the same path multiple times returns the identical lens instance
> - When using functions with methods like `reflect`, memoize the function to maintain caching benefits
> - Dependencies array is optional but useful for clearing lens cache based on external state changes
> - All lens operations maintain full TypeScript type safety and inference
>

### Examples

---

#### Basic Usage

[Open in CodeSandbox](https://codesandbox.io/p/sandbox/keen-herschel-y5h2ft)

```tsx
import { useForm, useController } from "react-hook-form"
import { Lens, useLens } from "@hookform/lenses"
import { useFieldArray } from "@hookform/lenses/rhf"

function FormComponent() {
  const { handleSubmit, control } = useForm<{
    firstName: string
    lastName: string
    children: {
      name: string
      surname: string
    }[]
  }>({})

  const lens = useLens({ control })

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <PersonForm
        lens={lens.reflect(({ firstName, lastName }) => ({
          name: firstName,
          surname: lastName,
        }))}
      />
      <ChildForm lens={lens.focus("children")} />
      <input type="submit" />
    </form>
  )
}

function ChildForm({
  lens,
}: {
  lens: Lens<{ name: string; surname: string }[]>
}) {
  const { fields, append } = useFieldArray(lens.interop())

  return (
    <>
      <button type="button" onClick={() => append({ name: "", surname: "" })}>
        Add child
      </button>
      {lens.map(fields, (value, l) => (
        <PersonForm key={value.id} lens={l} />
      ))}
    </>
  )
}

// PersonForm is used twice with different sources
function PersonForm({
  lens,
}: {
  lens: Lens<{ name: string; surname: string }>
}) {
  return (
    <div>
      <StringInput lens={lens.focus("name")} />
      <StringInput lens={lens.focus("surname")} />
    </div>
  )
}

function StringInput({ lens }: { lens: Lens<string> }) {
  const { field } = useController(lens.interop())

  return <input {...field} />
}
```

### Motivation

---

Working with complex, deeply nested forms in React Hook Form can quickly become challenging. Traditional approaches often lead to common problems that make development more difficult and error-prone:

#### 1. Type-Safe Name Props Are Nearly Impossible

Creating reusable form components requires accepting a `name` prop to specify which field to control. However, making this type-safe in TypeScript is extremely challenging:

```tsx
// ❌ Loses type safety - no way to ensure name matches the form schema
interface InputProps<T> {
  name: string // Could be any string, even invalid field paths
  control: Control<T>
}

// ❌ Attempting proper typing leads to complex, unmaintainable generics
interface InputProps<T, TName extends Path<T>> {
  name: TName
  control: Control<T>
}
// This becomes unwieldy and breaks down with nested objects
```

#### 2. `useFormContext()` Creates Tight Coupling

Using `useFormContext()` in reusable components tightly couples them to specific form schemas, making them less portable and harder to share:

```tsx
// ❌ Tightly coupled to parent form structure
function AddressForm() {
  const { register } = useFormContext<UserForm>() // Locked to UserForm type

  return (
    <div>
      <input {...register("address.street")} /> {/* Fixed field paths */}
      <input {...register("address.city")} />
    </div>
  )
}
// Can't reuse this component with different form schemas
```

#### 3. String-Based Field Paths Are Error-Prone

Building reusable components with string concatenation for field paths is fragile and difficult to maintain:

```tsx
// ❌ String concatenation is error-prone and hard to refactor
function PersonForm({ basePath }: { basePath: string }) {
  const { register } = useForm();

  return (
    <div>
      {/* No type safety, prone to typos */}
      <input {...register(`${basePath}.firstName`)} />
      <input {...register(`${basePath}.lastName`)} />
      <input {...register(`${basePath}.email`)} />
    </div>
  );
}

// Usage becomes unwieldy and error-prone
<PersonForm basePath="user.profile.owner" />
<PersonForm basePath="user.profile.emergency_contact" />
```

### Performance Optimization

---

#### Built-in Caching System

Lenses are automatically cached to prevent unnecessary component re-renders when using `React.memo`. This means that focusing on the same path multiple times returns the identical lens instance:

```tsx
assert(lens.focus("firstName") === lens.focus("firstName"))
```

#### Function Memoization

When using functions with methods like `reflect`, you need to be careful about function identity to maintain caching benefits:

```tsx
// ❌ Creates a new function on every render, breaking the cache
lens.reflect((proxy) => proxy.focus("firstName"))
```

To maintain caching, memoize the function you pass:

```tsx
// ✅ Memoized function preserves the cache
lens.reflect(useCallback((proxy) => proxy.focus("firstName"), []))
```

> **Tip: React Compiler Optimization**
>
> [React Compiler](https://react.dev/learn/react-compiler) can automatically optimize these functions for you! Since functions passed to `reflect` have no side effects, React Compiler will automatically hoist them to module scope, ensuring lens caching works perfectly without manual memoization.
>

### Advanced Usage

---

#### Manual Lens Creation

For advanced use cases or when you need more control, you can create lenses manually without the `useLens` hook using the `LensCore` class:

```tsx
import { useMemo } from "react"
import { useForm, Controller } from "react-hook-form"
import { LensCore, LensesStorage } from "@hookform/lenses"

function App() {
  const { control } = useForm<{ firstName: string; lastName: string }>()

  const lens = useMemo(() => {
    const cache = new LensesStorage(control)
    return LensCore.create(control, cache)
  }, [control])

  return (
    <div>
      <Controller
        {...lens.focus("firstName").interop()}
        render={({ field }) => <input {...field} />}
      />
      <Controller
        {...lens.focus("lastName").interop()}
        render={({ field }) => <input {...field} />}
      />
    </div>
  )
}
```

#### Extending lenses

You can extend the basic lens functionality by adding custom methods to the `LensBase` interface. This is useful when you need additional methods that aren't available in the default lens API.

For example, let's add a `useValue` method to the lens that allows you to subscribe to the current value at the lens path easily.

**Step 1: Create the type declarations file**

Create a `lenses.d.ts` file to extend the basic interface with the methods you want:

```typescript
declare module "@hookform/lenses" {
  interface LensBase<T> {
    useValue(): T
  }
}

export {}
```

**Step 2: Create the custom lens core implementation**

Create a `MyLensCore.ts` file with the actual runtime implementation:

```typescript
import { useWatch, type FieldValues } from "react-hook-form"
import { LensCore } from "@hookform/lenses"

export class MyLensCore<T extends FieldValues> extends LensCore<T> {
  // Calls a hook, so only call it during render
  public useValue() {
    return useWatch({ control: this.control, name: this.path })
  }
}
```

**Step 3: Create the custom hook**

Create a `useMyLens.ts` file that accepts control and returns the lens as usual:

```typescript
import { type DependencyList, useMemo } from "react"
import type { FieldValues } from "react-hook-form"

import { LensesStorage, type Lens, type UseLensProps } from "@hookform/lenses"
import { MyLensCore } from "./MyLensCore"

export function useMyLens<TFieldValues extends FieldValues = FieldValues>(
  props: UseLensProps<TFieldValues>,
  deps: DependencyList = []
): Lens<TFieldValues> {
  return useMemo(() => {
    const cache = new LensesStorage(props.control)
    const lens = new MyLensCore<TFieldValues>(
      props.control,
      "",
      cache
    ) as unknown as Lens<TFieldValues>

    return lens
  }, [props.control, ...deps])
}
```

**Step 4: Use your extended lens**

Now you can use this hook as usual and you have the new method with correct TypeScript support:

```typescript
const { control } = useForm()
const lens = useMyLens({ control })
lens.useValue() // Your custom method is now available with full type support
```

This pattern allows you to add any custom functionality to lenses while maintaining full type safety and compatibility with the existing lens API.

> **Tip: Questions or Feedback?**
>
> Found a bug or have a feature request? Check out the [GitHub repository](https://github.com/react-hook-form/lenses) to report issues or contribute to the project.
>
