Skip to content

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.

License

Notifications You must be signed in to change notification settings

react-hook-form/lenses

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Performant, flexible and extensible forms with easy to use validation.

npm downloads npm npm

CodeSandbox | Examples

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 { useFieldArray, useForm } from 'react-hook-form';
import { Lens, useLens } from '@hookform/lenses';

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

  const lens = useLens({ control });
  const children = lens.focus('children');
  const { fields: childrenFields } = useFieldArray(children.interop());

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <PersonForm
        lens={lens.reflect((l) => ({
          name: l.focus('firstName'),
          surname: l.focus('lastName'),
        }))}
      />

      {children.map(childrenFields, (l, key) => (
        <PersonForm key={key} lens={l} />
      ))}

      <input type="submit" />
    </form>
  );
}

function PersonForm({ lens }: { lens: Lens<{ name: string; surname: string }> }) {
  return (
    <>
      <StringInput lens={lens.focus('name')} />
      <StringInput lens={lens.focus('surname')} />
    </>
  );
}

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.

const contactLens = lens.reflect((l) => ({
  firstName: l.focus('profile.contact.firstName'),
  phoneNumber: l.focus('profile.contact.phone'),
}));

<SharedComponent lens={contactLens} />;

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

Combines two lenses into one. You have to provide a merger function because in runtime it is not clear to which prop path of two lenses subsequent lens operations will be applied.

function Card(props: { person: Lens<{ name: string }>; contact: Lens<{ phoneNumber: string }> }) {
  return (
    <SharedComponent
      lens={props.person.join(props.contact, (person, contact) => ({
        name: person.focus('name'),
        phoneNumber: contact.focus('phoneNumber'),
      }))}
    />
  );
}
map (Array Lenses)

Maps over array fields with useFieldArray integration

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

  return lens.map(fields, (l, key) => <ContactForm key={key} 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 { createLensesCache, LensCore } from '@hookform/lenses';

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

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

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

About

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.

Resources

License

Stars

Watchers

Forks

Packages

No packages published