</> useLens
React Hook Form Lenses 是一个强大的 TypeScript 优先库,它将函数式镜头的优雅引入表单开发。它提供类型安全的嵌套结构操作,使开发者能够轻松地精确控制和转换复杂数据。
¥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 precisely control and transform complex data with ease.
useLens
是一个自定义钩子,它会创建一个连接到 React Hook Form 控件的镜头实例,通过函数式编程概念,实现类型安全的聚焦、转换和对深度嵌套表单数据结构的操作。
¥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
npm install @hookform/lenses
特性
¥Features
-
类型安全表单状态:通过完整的 TypeScript 支持和精确的类型推断,专注于数据的特定部分。
¥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
-
无缝集成:与 React Hook Form 的 Control API 和现有功能顺畅协作
¥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
-
可组合 API:通过优雅的镜头组合构建复杂的转换
¥Composable API: Build complex transformations through elegant lens composition
属性
¥Props
useLens
hook 接受以下配置:
¥The useLens
hook accepts the following configuration:
control
:Control<TFieldValues>
必需。来自 React Hook Form 的 useForm
hook 的控制对象。这将你的镜头连接到表单管理系统。
¥Required. The control object from React Hook Form's useForm
hook. This connects your lens to the form management system.
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:
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>
- 根据你正在使用的字段类型提供不同操作的主要镜头类型:
¥Lens<T>
- The main lens type that provides different operations based on the field type you're working with:
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:
方法 | 描述 | 返回 |
---|---|---|
focus | 专注于特定的字段路径。 | Lens<PathValue> |
reflect | 转换和重塑镜头结构 | Lens<NewStructure> |
map | 遍历数组字段(使用 useFieldArray) | R[] |
interop | 连接到 React Hook Form 的控制系统 | { control, name } |
focus
创建一个专注于特定路径的新镜头。这是深入研究数据结构的主要方法。
¥Creates a new lens focused on a specific path. This is the primary method for drilling down into your data structure.
// Type-safe path focusingconst profileLens = lens.focus("profile")const emailLens = lens.focus("profile.email")const arrayItemLens = lens.focus("users.0.name")
数组聚焦:
¥Array focusing:
function ContactsList({ lens }: { lens: Lens<Contact[]> }) {// Focus on specific array indexconst firstContact = lens.focus("0")const secondContactName = lens.focus("1.name")return (<div><ContactForm lens={firstContact} /><input{...secondContactName.interop((ctrl, name) => ctrl.register(name))}/></div>)}
focus
方法提供完整的 TypeScript 支持,并具有自动补全和类型检查功能:
¥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 on property access. The second argument is the original lens.
对象反射
¥Object Reflection
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><input{...lens.focus("name").interop((ctrl, name) => ctrl.register(name))}/><input{...lens.focus("phoneNumber").interop((ctrl, name) => ctrl.register(name))}/></div>)}
使用镜头参数的替代语法:
¥Alternative syntax using the lens parameter:
你也可以直接使用第二个参数(原始镜头):
¥You can also use the second parameter (the original lens) directly:
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:
function ArrayComponent({ lens }: { lens: Lens<{ value: string }[]> }) {return (<AnotherComponent lens={lens.reflect(({ value }) => [{ data: value }])} />)}function AnotherComponent({ lens }: { lens: Lens<{ data: string }[]> }) {// ...}
请注意,对于数组反射,必须传递一个包含单个项的数组作为模板。
¥Note that for array reflection, you must pass an array with a single item as the template.
合并镜头
¥Merging Lenses
你可以使用 reflect
将两个镜头合并为一个:
¥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"),}))return <PersonForm lens={combined} />}
请记住,在这种情况下,传递给 reflect
的函数不再是纯函数。
¥Keep in mind that in such cases, the function passed to reflect
is no longer pure.
扩展运算符支持
¥Spread Operator Support
如果你想保留其他属性,可以在 Reflect 中使用 Spread。在运行时,第一个参数只是一个代理,它会在原始镜头上调用 focus
。当你只需要更改几个字段的属性名称而其余字段保持不变时,这对于正确的输入非常有用:
¥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:
function Component({lens,}: {lens: Lens<{ firstName: string; lastName: string; age: number }>}) {return (<PersonFormlens={lens.reflect(({ firstName, lastName, ...rest }) => ({...rest,name: firstName,surname: lastName,}))}/>)}
map
通过 useFieldArray
集成映射数组字段。此方法需要 useFieldArray
中的 fields
属性。
¥Maps over array fields with useFieldArray
integration. This method requires the fields
property from useFieldArray
.
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><input{...lens.focus("name").interop((ctrl, name) => ctrl.register(name))}/><input{...lens.focus("email").interop((ctrl, name) => ctrl.register(name))}/></div>)}
Map 回调参数:
¥Map callback parameters:
参数 | 类型 | 描述 |
---|---|---|
value | T | id 的当前字段值 |
lens | Lens<T> | 镜头聚焦于当前数组项 |
index | number | 当前数组索引 |
array | T[] | 完整数组 |
originLens | Lens<T[]> | 原始数组镜头 |
interop
interop
方法通过暴露底层 control
和 name
属性,与 React Hook Form 无缝集成。这允许你将镜头连接到 React Hook Form 的控制 API。
¥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
第一种变体涉及不带参数调用 interop()
,它返回一个包含 React Hook Form 的 control
和 name
属性的对象:
¥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)} />
第二种变体:回调函数
¥Second Variant: Callback Function
第二种变体将一个回调函数传递给 interop
,该函数接收 control
和 name
属性作为参数。这允许你直接在回调范围内使用这些属性:
¥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:
return (<form onSubmit={handleSubmit(console.log)}><input {...lens.interop((ctrl, name) => ctrl.register(name))} /><input type="submit" /></form>)
与 useController 集成
¥Integration with useController
interop
方法的返回值可以从 React Hook Form 直接传递给 useController
hook,从而实现无缝集成:
¥The interop
method's return value can be passed directly to the useController
hook from React Hook Form, providing seamless integration:
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>)}
useFieldArray
从 @hookform/lenses/rhf
导入增强型 useFieldArray
,以便使用镜头无缝处理数组。
¥Import the enhanced useFieldArray
from @hookform/lenses/rhf
for seamless array handling with lenses.
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}><input{...itemLens.focus("name").interop((ctrl, name) => ctrl.register(name))}/><inputtype="number"{...itemLens.focus("value").interop((ctrl, name) =>ctrl.register(name, { valueAsNumber: true }))}/><button onClick={() => remove(index)}>Remove</button>{index > 0 && (<button onClick={() => move(index, index - 1)}>Move Up</button>)}</div>))}</div>)}
-
control
参数是必需的,并且必须来自 React Hook Form 的useForm
hook¥The
control
parameter is required and must be from React Hook Form'suseForm
hook -
每个镜头都会被缓存并复用,以实现最佳性能。 - 多次聚焦同一条路径将返回相同的镜头实例
¥Each lens is cached and reused for optimal performance - focusing on the same path multiple times returns the identical lens instance
-
当使用带有类似
reflect
的方法的函数时,请记住该函数以保持缓存优势¥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
-
所有镜头操作均保持完整的 TypeScript 类型安全和推断
¥All lens operations maintain full TypeScript type safety and inference
示例
¥Examples
基本用法
¥Basic Usage
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: stringlastName: stringchildren: {name: stringsurname: string}[]}>({})const lens = useLens({ control })return (<form onSubmit={handleSubmit(console.log)}><PersonFormlens={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 sourcesfunction 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))} />}
动机
¥Motivation
在 React Hook Form 中使用复杂、深度嵌套的表单很快就会变得具有挑战性。传统方法经常会导致一些常见问题,使开发更加困难且容易出错:
¥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. 类型安全的 Name Props 几乎无法实现
¥ Type-Safe Name Props Are Nearly Impossible
创建可复用的表单组件需要接受 name
属性来指定要控制的字段。然而,在 TypeScript 中使其类型安全极具挑战性:
¥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:
// ❌ Loses type safety - no way to ensure name matches the form schemainterface InputProps<T> {name: string // Could be any string, even invalid field pathscontrol: Control<T>}// ❌ Attempting proper typing leads to complex, unmaintainable genericsinterface InputProps<T, TName extends Path<T>> {name: TNamecontrol: Control<T>}// This becomes unwieldy and breaks down with nested objects
2. useFormContext()
造成紧耦合
¥ useFormContext()
Creates Tight Coupling
在可重用组件中使用 useFormContext()
会将它们与特定的表单模式紧密耦合,从而降低可移植性并更难共享:
¥Using useFormContext()
in reusable components tightly couples them to specific form schemas, making them less portable and harder to share:
// ❌ Tightly coupled to parent form structurefunction AddressForm() {const { control } = useFormContext<UserForm>() // Locked to UserForm typereturn (<div><input {...control.register("address.street")} />{" "}{/* Fixed field paths */}<input {...control.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:
// ❌ String concatenation is error-prone and hard to refactorfunction 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
使用 React.memo
时,镜头会自动缓存,以防止不必要的组件重新渲染。这意味着多次聚焦于同一条路径将返回相同的镜头实例:
¥Lenses are automatically cached to prevent unnecessary component re-renders when using React.memo
. This means that focusing on the same path multiple times will return the identical lens instance:
assert(lens.focus("firstName") === lens.focus("firstName"))
函数记忆化
¥Function Memoization
当使用带有类似 reflect
的方法的函数时,需要注意函数标识以保持缓存优势:
¥When using functions with methods like reflect
, you need to be careful about function identity to maintain caching benefits:
// ❌ Creates a new function on every render, breaking the cachelens.reflect((l) => l.focus("firstName"))
为了保持缓存,请记住传递的函数:
¥To maintain caching, memoize the function you pass:
// ✅ Memoized function preserves the cachelens.reflect(useCallback((l) => l.focus("firstName"), []))
React 编译器 可以自动为你优化这些功能!由于传递给 reflect
的函数没有副作用,React Compiler 会自动将它们提升到模块作用域,确保镜头缓存无需手动记忆即可完美运行。
¥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
对于高级用例或需要更多控制时,你可以使用 LensCore
类手动创建镜头,而无需使用 useLens
钩子:
¥For advanced use cases or when you need more control, you can create lenses manually without the useLens
hook using 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])return (<div><input{...lens.focus("firstName").interop((ctrl, name) => ctrl.register(name))}/><input{...lens.focus("lastName").interop((ctrl, name) => ctrl.register(name))}/></div>)}
发现了 bug 或有功能请求?查看 GitHub 存储库 以报告问题或为项目做出贡献。
¥Found a bug or have a feature request? Check out the GitHub repository to report issues or contribute to the project.