基于React Hooks实现表单双向数据绑定

5,033 阅读2分钟

在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的写法下,每次有propsstate的变化,匿名函数都会被重新创建,这样会有性能问题。而且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