在React里使用可控表单,通常的写法是:
const Page = () => {
const [value, setValue] = useState(null)
const handleChange = useCallback((val) => {
setValue(val)
}, [])
return <Input value={value} onChange={handleChange} />
}
看起来代码很简单,但假如表单包含三个输入框(正常表单包含3-8个输入项):
const Page = () => {
const [name, setName] = useState(null)
const [age, setAge] = useState(null)
const [gender, setGender] = useState(null)
const handleNameChange = useCallback((val) => {
setName(val)
}, [])
const handleAgeChange = useCallback((val) => {
setAge(val)
}, [])
const handleGenderChange = useCallback((val) => {
setGender(val)
}, [])
return (
<>
<Input value={name} onChange={handleNameChange} />
<Input value={age} onChange={handleAgeChange} />
<Input value={gender} onChange={handleGenderChange} />
</>
)
}
可以发现,代码成倍数增加,每多一个输入项,就需要多写一个值和一个回调函数。通过观察我们可以发现,回调函数的作用都是为了更新对应表单项的值(大部分表单的使用场景)。我们可以换个写法:
const Page = () => {
const [form, setForm] = useState({})
const handleChange = useCallback((val, name) => {
setForm(preVal => {...preVal, [name]: val})
}, [])
return (
<>
<Input value={form.name} onChange={(val) => handleChange(val, 'name')} />
<Input value={form.age} onChange={(val) => handleChange(val, 'age')} />
<Input value={form.gender} onChange={(val) => handleChange(val, 'gender')} />
</>
)
}
以上方式,代码简单多了,但每个onChange
都定义了一个匿名函数,而且在React Hooks的写法下,每次有props
或state
的变化,匿名函数都会被重新创建,这样会有性能问题。而且onChange
的值就是为了更新value
,每个表单都写还是感觉有点多余。但我们可以发现,useState返回的数组结构和Vue指令v-model的原理非常相似。v-model在编译时会被拆分成model-value
值和update:model-value
事件回调,useState的返回值是一个数组,包含两个值,一个是state
值,一个是setState
函数。所以我们可以使用useState来实现v-model的效果。
写一个HOC来处理双向数据绑定:
// withModel.jsx
import React, { forwardRef, useMemo, useCallback, useEffect } from 'react'
const withModel = (Component) => forwardRef(({
model = [],
name,
value,
onChange,
...other
}, outerRef) => {
const [modelValue, setModelValue] = useMemo(() => model, [model])
const handleChange = useCallback((e) => {
if (setModelValue) {
setModelValue(e.target.value)
}
onChange(e)
}, [onChange])
return (
<Component
{...other}
ref={outerRef}
name={name}
value={modelValue !== undefined ? modelValue : value}
onChange={handleChange}
/>
)
})
export default withModel
withModel
只是对组件赋能了双向数据绑定的能力,不影响原来的任何行为。然后只需要创建一个Input组件并用withModel
包起来:
// Input.jsx
import React, { forwardRef } from 'react'
import withModel from './withModel.jsx'
const Component = forwardRef((props, outerRef) => {
return (
<input ref={outerRef} {...props} />
)
})
Component.displayName = 'Input'
export default withModel(Component)
这样使用Input的时候就可以像Vue一样实现双向数据绑定:
import React, { useState } from 'react'
import Input from './Input.jsx'
const Component = () => {
const model = useState('')
return (
<Input model={model} />
)
}
基于这个解决方案,我们可以扩展更多的功能来实现一套完整的表单组件,如表单校验等。
即将更新:
- CodePen