◆ input に value 渡して onChange 設定して を一つ一つするのが面倒
◆ 特に value と変更先設定の onChange で同じ名前を 2 回書かないといけないし
◆ 基本は input の value をそのまま取り込むだけなので楽にできるようにする

React のフォームを使うときは毎回 value にプロパティ指定とその更新のリスナを設定があって面倒です
DOM 操作風に親にリスナを設定しても イベント発生源がどのプロパティと対応するかわからないので name 属性をつけることになります
結局 value と name で同じ名前を指定することになりますし そこまで変わりません

一応こういうのを作ってみたりはしてたのですが 見やすさ的にそこまで優れてるとは言えないです

const createFormInput = (form_data, update) => (name, type) =>
<input type={type} value={form_data[name]} onChange={(event) => update(name, event.target.value)} />

const App = () => {
const [form_data, setFormData] = useState({})

const update = (name, value) => {
setFormData({ ...form_data, [name]: value })
}

const createInput = createFormInput(form_data, update)

const show = () => {
const str = JSON.stringify(form_data, null, " ")
alert(str)
}

return (
<div>
{createInput("message", "text")}
{createInput("count", "number")}
{createInput("date", "date")}
<button onClick={show}>OK</button>
</div>
)
}

UI ライブラリで見かけるような Form や Input をコンポーネントにする感じで やると少しはマシかなと思って試してみました

const FormDataContext = React.createContext()
const FormDataProvider = FormDataContext.Provider
const useFormData = () => useContext(FormDataContext)

const Form = ({ children, data, onChange }) => {
const updateFormData = (name, value) => {
onChange({ ...data, [name]: value })
}

const context_value = useMemo(() => {
return [data, updateFormData]
}, [data, onChange])

return (
<FormDataProvider value={context_value}>
{children}
</FormDataProvider>
)
}

const Input = ({ component, children, ...props }) => {
const [form_data, updateFormData] = useFormData()
const Component = component || "input"

const onChange = (event) => {
updateFormData(props.name, event.target.value)
}

return (
<Component {...props} value={form_data[props.name]} onChange={onChange}>{children || null}</Component>
)
}

const App = () => {
const [form_data, setFormData] = useState({ text: "abc", select: "a" })
const onChange = (value) => {
setFormData(value)
}
const show = () => {
const str = JSON.stringify(form_data, null, " ")
alert(str)
}

return (
<Form data={form_data} onChange={onChange}>
<div>
<Input component="input" type="text" name="text" />
</div>
<div>
<Input component="select" name="select">
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</Input>
</div>
<button onClick={show}>OK</button>
</Form>
)
}

export default App

Form や Input の定義が長くなりましたが App の中をみると 割といい感じのように思います
コンポーネントにして children も扱えるので select タグにも対応しています

Form 内で使うデータは Form の data として渡します
Input の name を元に value を設定と onChange を使った更新を行います

更新は単純に value を data で渡したオブジェクトのプロパティに代入するだけです
許可してない文字の入力は無視するとか複雑な制御が必要な input は対応してません
そういうのが必要なときもありますが 9 割くらいは特殊な処理のないただの input ですし

一応 Form や Input で特殊な場合に対応するための関数を受け取れるようにしようかとも思ったのですが 関数渡すよりも自分で input タグを用意したほうが自由度高いのでこのコンポーネント内では扱わないことにしました