npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@hookform/lenses

v0.6.0

Published

Type-safe lenses for React Hook Form that enable precise control over nested form state. Build reusable form components with composable operations, array handling, and full TypeScript support.

Downloads

4,078

Readme

npm downloads npm npm

React Hook Form Lenses

React Hook Form Lenses is a powerful TypeScript-first library that brings the elegance of functional lenses to React Hook Form. By providing type-safe manipulation of nested form state, it enables developers to precisely control and transform complex form data with ease. The library's composable lens operations make it simple to work with deeply nested structures while maintaining type safety, leading to more maintainable and reusable form components.

Installation

npm install @hookform/lenses

Features

  • Type-Safe Form State: Focus on specific parts of your form state with full TypeScript support and precise type inference
  • Functional Lenses: Build complex form state transformations through composable lens operations
  • Deep Structure Support: Handle deeply nested structures and arrays elegantly with specialized array 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 performance
  • Array Handling: Specialized support for array fields with type-safe mapping
  • Composable API: Build complex form state transformations through lens composition

Quickstart

import { useForm } 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} />
      ))}
    </>
  );
}

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> }) {
  return <input {...lens.interop((ctrl, name) => ctrl.register(name))} />;
}

API Reference

Core Types

Lens<T>

The main lens type that provides operations based on the field type

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

Hooks

useLens

Creates a new lens instance

const lens = useLens({
  control: form.control, // React Hook Form control
});

You can also pass dependencies to clear lenses with caches and re-create all of them

const lens = useLens(
  {
    control: form.control, // React Hook Form control
  },
  [dependencies], // optional dependency array if you need to clear caches
);

Lens Operations

focus

Creates a new lens focused on a specific path

// Type-safe path focusing
const profileLens = lens.focus('profile');
const emailLens = lens.focus('profile.contact.email');
const arrayItemLens = lens.focus('array.0');
reflect

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

The first argument is a dictionary of lenses. The second argument is the original lens.

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 }> }) {
  // ...
}
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 }> }) {
  // ...
}

Also, you can restructure array lens:

function ArrayComponent({ lens }: { lens: Lens<{ value: string }[]> }) {
  return <AnotherComponent lens={lens.reflect((_, l) => [{ data: l.focus('value') }])} />;
}

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

Pay attention that in case of array reflecting you have to pass an array with single item.

In addition you can use reflect to merge two lenses into one.

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

  // ...
}

Keep in mind that is such case the passed to reflect function is longer pure.

You can use spread in reflect if you want to leave other properties as is. In runtime the first argument is just a proxy that calls focus on the original lens.

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 (Array Lenses)

Maps over array fields with useFieldArray integration

import { useFieldArray } from '@hookform/lenses/rhf';

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

  return lens.map(fields, (value, l) => <ContactForm key={value.id} lens={l} />);
}
interop

The interop method provides 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.

The first variant involves calling interop() without arguments, which returns an object containing the control and name properties for react-hook-form.

const { control, name } = lens.interop();

return <input {...control.register(name)} />;

The second variant is passing 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.

return (
  <form onSubmit={handleSubmit(console.log)}>
    <input {...lens.interop((ctrl, name) => ctrl.register(name))} />
    <input type="submit" />
  </form>
);

The interop method's return value can be passed directly to the useController hook from react-hook-form, providing seamless integration

const { field, fieldState } = useController(lens.interop());

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

Caching System

All the lenses are cached to prevent component re-renders when utilizing React.memo. It means that focusing the same path multiple times will not create new lens instance.

assert(lens.focus('firstName') === lens.focus('firstName'));

However, there are some difficulties when you use functions, i.e. in reflect

lens.reflect((l) => l.focus('firstName')))

To make the caching work, you need to memoize the function you pass

lens.reflect(useCallback((l) => l.focus('firstName'), []));

Here is the case where React Compiler can be extremely helpful. Because the function you pass to reflect has no side effects, react compiler will hoist it to module scope and thus lens cache will work as expected.

Advanced Usage

Manual Lens Creation

You can create lenses manually without useLens hook by utilizing the LensCore class:

import { useMemo } from 'react';
import { useForm } 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]);

  lens.focus('firstName');
  lens.focus('lastName');
}