React.Hooks で 大量の input 更新ハンドラーをまとめる小技

Hooks で form 周りが大分楽になりましたね。さて、useState と useCallback を使って form を作るとき、大量の input 要素向けに callbackHandler と state を用意するのって、結構大変ですよね。たった 要素が4つでも、それなりにしんどいです。

```tsx
import * as React from 'react'
import { useState, useCallback } from 'react'
// ___________________________________
//
// @ Types

type Props = {
  className?: string
  firstName: string
  lastName: string
  comment: string
  address: string
  onChangeFirstName: (event: React.ChangeEvent<HTMLInputElement>) => void
  onChangeLastName: (event: React.ChangeEvent<HTMLInputElement>) => void
  onChangeComment: (event: React.ChangeEvent<HTMLInputElement>) => void
  onChangeAddress: (event: React.ChangeEvent<HTMLInputElement>) => void
}
// ___________________________________
//
// @ Component

const Component: React.FC<Props> = props => (
  <form className={props.className}>
    <input
      name="first_name"
      type="text"
      value={props.firstName}
      onChange={props.onChangeFirstName}
    />
    <input
      name="last_name"
      type="text"
      value={props.lastName}
      onChange={props.onChangeLastName}
    />
    <input
      name="comment"
      type="text"
      value={props.comment}
      onChange={props.onChangeComment}
    />
    <input
      name="address"
      type="text"
      value={props.address}
      onChange={props.onChangeAddress}
    />
  </form>
)
// ___________________________________
//
// @ Container

const Container: React.FC = () => {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')
  const [comment, setComment] = useState('')
  const [address, setAddress] = useState('')
  const onChangeFirstName = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setFirstName(event.target.value)
    },
    [firstName]
  )
  const onChangeLastName = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setLastName(event.target.value)
    },
    [lastName]
  )
  const onChangeComment = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setComment(event.target.value)
    },
    [comment]
  )
  const onChangeAddress = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setAddress(event.target.value)
    },
    [address]
  )
  return (
    <Component
      firstName={firstName}
      lastName={lastName}
      comment={comment}
      address={address}
      onChangeFirstName={onChangeFirstName}
      onChangeLastName={onChangeLastName}
      onChangeComment={onChangeComment}
      onChangeAddress={onChangeAddress}
    />
  )
}

export default Container

```

# 解決策

- state を一つの object にまとめる
- state の key は、name属性にあわせる

こうすることで、update関数での更新処理が `[event.target.name]: event.target.value` で済みます。

```tsx
import * as React from 'react'
import { useState, useCallback } from 'react'
// ___________________________________
//
// @ Types

type State = {
  first_name: string
  last_name: string
  comment: string
  address: string
}
type Props = {
  className?: string
  onChangeInputText: (event: React.ChangeEvent<HTMLInputElement>) => void
} & State
// ___________________________________
//
// @ Component

const Component: React.FC<Props> = props => (
  <form className={props.className}>
    {(['first_name', 'last_name', 'comment', 'address'] as const).map(name => (
      <input
        key={name}
        name={name}
        type="text"
        value={props[name]}
        onChange={props.onChangeInputText}
      />
    ))}
  </form>
)
// ___________________________________
//
// @ Container

const Container: React.FC = () => {
  const [state, update] = useState<State>({
    first_name: '',
    last_name: '',
    comment: '',
    address: ''
  })
  const onChangeInputText = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      event.persist()
      update(prev => ({
        ...prev,
        [event.target.name]: event.target.value
      }))
    },
    []
  )
  return <Component {...state} onChangeInputText={onChangeInputText} />
}

export default Container

```

# 余談

`value={props[name]}` が通用しているのは、このコードが TypeScript3.4 で書いているためです。
`as const` を使うことで String Literal Types になるので、props を参照することができています。