React uses controlled forms, usually written like this:

const Page = () = > {
  const [value, setValue] = useState(null)

  const handleChange = useCallback((val) = > {
    setValue(val)
  }, [])

  return <Input value={value} onChange={handleChange} />
}
Copy the code

The code looks simple, but suppose the form contains three input fields (normal forms contain 3-8 input fields) :

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} />
    </>)}Copy the code

As you can see, the code multiplies, and for each additional input, you need to write one more value and one more callback function. As you can see, the purpose of the callback function is to update the value of the corresponding form item (most forms use scenarios). We can write it this way:

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')} />
    </>)}Copy the code

The code is much simpler, but each onChange defines an anonymous function, and with React Hooks, the anonymous function is recreated every time there is a change in props or state, which can cause performance problems. And the onChange value is meant to update the value, so writing every form feels a bit redundant. However, we can see that the array structure returned by useState is very similar to the principle of the Vue instruction V-model. The V-model is split at compile time into a model-value value and an UPDATE :model-value event callback. The return value of useState is an array of two values, a state value and a setState function. So we can use useState to achieve the v-model effect.

Write a HOC to handle two-way data binding:

// 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
Copy the code

WithModel simply enables two-way data binding for components without affecting any of the original behavior. Then just create an Input component and wrap it around a 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)
Copy the code

This allows bidirectional data binding like Vue when using Input:

import React, { useState } from 'react'

import Input from './Input.jsx'

const Component = () = > {
  const model = useState(' ')

  return (
    <Input model={model} />)}Copy the code

With this solution, we can extend more functionality to implement a complete set of form components, such as form validation.

Coming soon:

  • CodePen