Performant, flexible and extensible forms with easy to use validation.
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.
npm install @hookform/lenses
- 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
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))} />;
}
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>;
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
);
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');
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 }> }) {
// ...
}
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'),
}))}
/>
);
}
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} />);
}
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>
);
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.
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');
}