ReduxFormとMaterial-UIでモダンなフォームを作る(MUI v3版)

2019/02/06追記：
新規の場合、Redux FormよりもReact Final Formを使うことをおすすめします。（作者は同じ）
React Final Formはreduxに依存しないのと作者の開発がReact Final Formをメインにしているからです。

# Reactでモダンなフォームを作る
ReactでのメジャーなUIライブラリである[Material-UI](https://material-ui.com/)とFormのステート管理ライブラリである[ReduxForm](https://redux-form.com/7.4.2/examples/)を組み合わせてモダンなフォームを作ります。
[ReduxForm公式のMUIサンプルが古すぎた(MUI v0から更新されていない・・・)](https://redux-form.com/7.4.2/examples/material-ui/)のと個人的に一通り整理したくなったので、MUI(v3)と組み合わせたサンプルを作りました。
→追記：[しれっとReduxForm v8.1.0のサンプルで追従していました](https://redux-form.com/8.1.0/examples/material-ui/)

ReduxFormの使い方を熟知するとフォームデータ用のstateが不要になります。

Reactがはじめての人はこちらにまとめたので参考にしてください。
[ReactJSで作る今時のSPA入門（基本編）](https://qiita.com/teradonburi/items/fb91e5feacab5071cfef)


# フォーム

今回のサンプル: https://github.com/teradonburi/muiReduxForm
yarnインストール前提で次のコマンドで起動できます。

```sh
$ yarn
$ yarn dev
```

一通りフォームで必要なコンポーネントを作成します。

- テキスト（1行）
- パスワード（1行）
- テキストエリア（複数行）
- セレクト
- ラジオボタン
- チェックボックス
- スイッチ
- ファイルアップロード（画像）
- 動的な項目追加できるグループ

![screencapture-localhost-7070-2018-11-12-00_48_42.png](https://qiita-image-store.s3.amazonaws.com/0/55077/7e2292b6-bff6-0f84-5434-718f30859946.png)

フォームの実装部分です。(App.js)

```App.jsx
import React from 'react'
import { connect } from 'react-redux'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import FormControl from '@material-ui/core/FormControl'
import FormHelperText from '@material-ui/core/FormHelperText'
import MenuItem from '@material-ui/core/MenuItem'
import Radio from '@material-ui/core/Radio'
import RadioGroup from '@material-ui/core/RadioGroup'
import FormControlLabel from '@material-ui/core/FormControlLabel'
import FormLabel from '@material-ui/core/FormLabel'
import Checkbox from '@material-ui/core/Checkbox'
import FormGroup from '@material-ui/core/FormGroup'
import Switch from '@material-ui/core/Switch'
import { reduxForm, Field, FieldArray } from 'redux-form'
import { withStyles } from '@material-ui/core'
import { create } from './modules/user'

// TextField
const renderTextField = ({
  input,
  label,
  meta: { touched, error },
  type='text',
  required = false,
  rootClass = '',
}) => (
  <TextField
    required={required}
    classes={{root: rootClass}}
    error={!!(touched && error)}
    label={label}
    type={type}
    variant='outlined'
    helperText={touched && error}
    {...input}
  />
)

// TextArea
const renderTextArea = ({
  input,
  label,
  meta: { touched, error },
  rows = 4,
  required = false,
  rootClass = '',
}) => (
  <TextField
    required={required}
    classes={{root: rootClass}}
    multiline
    rows={rows}
    error={!!(touched && error)}
    label={label}
    variant='outlined'
    helperText={touched && error}
    {...input}
  />
)

// Select
const renderSelect = ({
  input: { value, onChange },
  label,
  children,
  meta: { touched, error },
  onFieldChange,
  required = false,
  rootClass = '',
}) => (
  <TextField
    required={required}
    classes={{root: rootClass}}
    select
    label={label}
    variant='outlined'
    value={value}
    onChange={e => {
      onChange(e.target.value)
      onFieldChange && onFieldChange(e.target.value)
    }}
    helperText={touched && error}
  >
    {children}
  </TextField>
)


// RadioButton
const renderRadio = ({
  input: { value, onChange },
  label,
  children,
  meta: { touched, error },
  onFieldChange,
  row = true,
  required = false,
  rootClass = '',
}) => (
  <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(touched && error)}>
    <FormLabel component='legend'>{label}</FormLabel>
    <RadioGroup
      row={row}
      value={value}
      onChange={(e) => {
        onChange(e.target.value)
        onFieldChange && onFieldChange(e.target.value)
      }}
    >
      {children}
    </RadioGroup>
    {touched && error && <FormHelperText>{error}</FormHelperText>}
  </FormControl>
)

// CheckBox
const renderCheckBox = ({
  input: { value, onChange },
  label,
  children,
  meta: { touched, error },
  onFieldChange,
  row = true,
  required = false,
  rootClass = '',
}) => (
  <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(touched && error)}>
    <FormLabel component='legend'>{label}</FormLabel>
    <FormGroup
      row={row}
      value={value}
      onChange={(e) => {
        onChange(e.target.value)
        onFieldChange && onFieldChange(e.target.value)
      }}
    >
      {children}
    </FormGroup>
    {touched && error && <FormHelperText>{error}</FormHelperText>}
  </FormControl>
)

// Switch
const renderSwitch = ({
  input: { value, onChange },
  label,
  onFieldChange,
  rootClass = '',
}) => (
  <FormControlLabel
    classes={{root: rootClass}}
    control={
      <Switch
        checked={value}
        onChange={(e, bool) => {
          onChange(bool)
          onFieldChange && onFieldChange(bool)
        }}
      />
    }
    label={label}
  />
)

// File
const renderFile = withStyles(() => ({
  input: {
    display: 'none',
  },
  button: {
    marginTop: 10,
  },
}))(
  ({
    input: { value, name, onChange },
    label,
    meta: { touched, error },
    classes,
    onFieldChange,
    buttonLabel,
    accept = '*',
    required = false,
    rootClass = '',
  }) => (
    <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(touched && error)}>
      <FormLabel component='legend'>{label}</FormLabel>
      <input
        accept={accept}
        className={classes.input}
        id={name}
        type='file'
        onChange={e => {
          e.preventDefault()
          onChange(e.target.files[0])
          onFieldChange && onFieldChange(e.target.files[0])
        }}
        onBlur={() => {}}
      />
      <label htmlFor={name}>
        <Button classes={{root: classes.button}} variant='outlined' component='span'>
          {buttonLabel || 'ファイルを選択'}
        </Button>
      </label>
      <label>{value && value.name}</label>
      {touched && error && <FormHelperText>{error}</FormHelperText>}
    </FormControl>
  )
)

// Dynamic Items
const renderMembers = withStyles(theme => ({
  input: {
    display: 'flex',
  },
  space: {
    marginLeft: 10,
  },
  member: {
    marginTop: 10,
    padding: 10,
    border: `1px solid ${theme.palette.grey[400]}`,
    borderRadius: 10,
  },
  header: {
    marginTop: 0,
    marginBottom: 10,
  },
  addButton: {
    marginTop: 10,
  },
  deleteButton: {
    height: 30,
  },
}))(
  ({
    label,
    fields,
    meta: { error, submitFailed },
    classes,
    required = false,
    rootClass = '',
  }) => (
    <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(submitFailed && error)}>
      <FormLabel component='legend'>{label}</FormLabel>
      {fields.map((member, idx) => (
        <div key={idx} className={classes.member}>
          <h6 className={classes.header}>メンバー{idx + 1}</h6>
          <div className={classes.input}>
            <Field
              name={`${member}.lastName`}
              component={renderTextField}
              label='姓'
              rootClass={classes.space}
            />
            <Field
              name={`${member}.firstName`}
              component={renderTextField}
              label='名'
              rootClass={classes.space}
            />
            <Button classes={{root: [classes.deleteButton, classes.space].join(' ')}} size='small' variant='contained' color='secondary' onClick={() => fields.remove(idx)}>削除</Button>
          </div>
        </div>
      ))}
      <div>
        <Button classes={{root: classes.addButton}} size='medium' variant='contained' color='primary' onClick={() => fields.push({})}>追加</Button>
        {submitFailed && error && <FormHelperText>{error}</FormHelperText>}
      </div>
    </FormControl>
  )
)

@reduxForm({
  form: 'form',
  validate: values => {
    const errors = {}
    if (!values.text) {
      errors.text = '必須項目です'
    }
    if (!values.password) {
      errors.password = '必須項目です'
    }
    if (!values.textarea) {
      errors.textarea = '必須項目です'
    }
    if (!values.select) {
      errors.select = '必須項目です'
    }
    if (!values.radio) {
      errors.radio = '必須項目です'
    }
    if (!values.checkbox) {
      errors.checkbox = '必須項目です'
    }
    if (!values.image) {
      errors.image = '必須項目です'
    }
    if (!values.members || !values.members.length) {
      errors.members = { _error: '１人以上メンバーの追加が必要です' }
    } else {
      const membersArrayErrors = []
      values.members.forEach((member, memberIndex) => {
        const memberErrors = {}
        if (!member || !member.firstName) {
          memberErrors.firstName = '必須項目です'
          membersArrayErrors[memberIndex] = memberErrors
        }
        if (!member || !member.lastName) {
          memberErrors.lastName = '必須項目です'
          membersArrayErrors[memberIndex] = memberErrors
        }
      })
      if (membersArrayErrors.length) {
        errors.members = membersArrayErrors
      }
    }
    return errors
  },
})
@connect(
  state => ({
    send: state.user.user,
  }),
  { create }
)
@withStyles(() => ({
  formControl: {
    marginTop: 10,
    marginBottom: 10,
  },
}))
export default class MainPage extends React.Component {

  constructor(props) {
    super(props)
    this.props.initialize({text: 'てきすと'})
  }

  submit = (values) => {
    const params = new FormData()
    params.append('text', values.text)
    params.append('password', values.password)
    params.append('textarea', values.textarea)
    params.append('select', values.select)
    params.append('radio', values.radio)
    params.append('checkbox', values.checkbox)
    params.append('switch', !!values.switch)
    params.append('image', values.image)
    params.append('members', JSON.stringify(values.members))
    this.props.create(params)
  }

  render () {
    const { classes, handleSubmit, send } = this.props

    return (
      <form onSubmit={handleSubmit(this.submit)} encType='multipart/form-data'>
        <div style={{width: 400, display: 'flex', flexDirection: 'column'}} >
          <Field name='text' label='テキストフィールド' component={renderTextField} rootClass={classes.formControl} required />
          <Field name='password' type='password' label='パスワード' component={renderTextField} rootClass={classes.formControl} required />
          <Field name='textarea' label='テキストエリア' component={renderTextArea} rootClass={classes.formControl} required />
          <Field name='select' label='セレクト' component={renderSelect} rootClass={classes.formControl} required >
            <MenuItem value=''>未選択</MenuItem>
            <MenuItem value={10}>10円</MenuItem>
            <MenuItem value={20}>20円</MenuItem>
            <MenuItem value={30}>30円</MenuItem>
          </Field>
          <Field name='radio' label='ラジオボタン' component={renderRadio} rootClass={classes.formControl} required >
            <FormControlLabel value='female' control={<Radio />} label='女性' />
            <FormControlLabel value='male' control={<Radio />} label='男性' />
            <FormControlLabel value='other' control={<Radio />} label='その他' />
          </Field>
          <Field name='checkbox' label='チェックボックス' component={renderCheckBox} rootClass={classes.formControl} required >
            <FormControlLabel value='check1' control={<Checkbox />} label='オプション１'/>
            <FormControlLabel value='check2' control={<Checkbox />} label='オプション２'/>
            <FormControlLabel value='check3' control={<Checkbox />} label='オプション３'/>
          </Field>
          <Field name='switch' label='スイッチ' component={renderSwitch} />
          <Field name='image' label='画像' accept='image/*' component={renderFile} rootClass={classes.formControl} required />
          <FieldArray name='members' label='メンバー' component={renderMembers} rootClass={classes.formControl} required />
          <Button type='submit' size='medium' variant='contained' color='primary' >送信</Button>
        </div>
        <div>
          {send && JSON.stringify(send)}
        </div>
      </form>
    )
  }
}
```

renderMember以外のrender*は使いまわしできるように作成しました。
フォームのパラメータのチェックはvalidateで行います。
initializeでフォームの初期化処理を行います。
submitとvalidateで渡ってくるvaluesには、Fieldのname属性で指定した入力データがオブジェクト形式で入ってきます。
今回、画像送信するため、submitはFormData(multipart/form-data形式)での送信を行います。

```jsx
@reduxForm({
  form: 'form',
  validate: values => {
    ...
  })
})
export default class MainPage extends React.Component {

  submit = (values) => {
    const params = new FormData()
    params.append('text', values.text)
    params.append('password', values.password)
    params.append('textarea', values.textarea)
    params.append('select', values.select)
    params.append('radio', values.radio)
    params.append('checkbox', values.checkbox)
    params.append('switch', !!values.switch)
    params.append('image', values.image)
    params.append('members', JSON.stringify(values.members))
    this.props.create(params)
  }

  render () {
    const { classes, handleSubmit, send } = this.props

    return (
      <form onSubmit={handleSubmit(this.submit)} encType='multipart/form-data'>
        <div style={{width: 400, display: 'flex', flexDirection: 'column'}} >
          <Field name='text' label='テキストフィールド' component={renderTextField} rootClass={classes.formControl} required />
          ...
      </form>
   )
  }
}
```

# テキスト入力
MUIの[TextField Component](https://material-ui.com/demos/text-fields/)でテキスト入力できます。input, metaに関してはField Component経由で渡ってくるデータです。
touchedは一度でもフォーカスが当たった場合にtrueになります。
error時はerror属性がtrueになり、赤字になり、helperTextにてerror内容が表示されます。
必須欄の場合はrequiredをtrueにします。
rootClassは外部からレイアウト調整するために渡しているclass名です。

```jsx
// TextField
const renderTextField = ({
  input,
  label,
  meta: { touched, error },
  type='text',
  required = false,
  rootClass = '',
}) => (
  <TextField
    required={required}
    classes={{root: rootClass}}
    error={!!(touched && error)}
    label={label}
    type={type}
    variant='outlined'
    helperText={touched && error}
    {...input}
  />
)
```

Field Componentのcomponent属性にてwrapします。

```jsx
<Field name='text' label='テキストフィールド' component={renderTextField} rootClass={classes.formControl} required />
```

# パスワード入力
パスワードの場合はrenderTextFieldにtype='password'を渡せばパスワード入力欄になります。

# テキストエリア
renderTextFieldにrows（行数）とmultiline属性がついただけです。

```jsx
// TextArea
const renderTextArea = ({
  input,
  label,
  meta: { touched, error },
  rows = 4,
  required = false,
  rootClass = '',
}) => (
  <TextField
    required={required}
    classes={{root: rootClass}}
    multiline
    rows={rows}
    error={!!(touched && error)}
    label={label}
    variant='outlined'
    helperText={touched && error}
    {...input}
  />
)
```

# セレクト
選択欄を作ります。TextFieldにselect属性を指定します。
また、選択項目を受け取った場合にonChangeでReduxFormの値を更新します。
onFieldChangeはカスタムのハンドリングをしたい場合に渡しています（今回は使っていません）

```jsx
// Select
const renderSelect = ({
  input: { value, onChange },
  label,
  children,
  meta: { touched, error },
  onFieldChange,
  required = false,
  rootClass = '',
}) => (
  <TextField
    required={required}
    classes={{root: rootClass}}
    select
    label={label}
    variant='outlined'
    value={value}
    onChange={e => {
      onChange(e.target.value)
      onFieldChange && onFieldChange(e.target.value)
    }}
    helperText={touched && error}
  >
    {children}
  </TextField>
)
```

選択項目はMenuItem Componentで作成します。

```jsx
<Field name='select' label='セレクト' component={renderSelect} rootClass={classes.formControl} required >
  <MenuItem value=''>未選択</MenuItem>
  <MenuItem value={10}>10円</MenuItem>
  <MenuItem value={20}>20円</MenuItem>
  <MenuItem value={30}>30円</MenuItem>
</Field>
```

# ラジオボタン
ラジオボタンはFormControl、FormLabel、RadioGroup、FormHelperText等の[Selection Component](https://material-ui.com/demos/selection-controls/)を使います。

```jsx
// RadioButton
const renderRadio = ({
  input: { value, onChange },
  label,
  children,
  meta: { touched, error },
  onFieldChange,
  row = true,
  required = false,
  rootClass = '',
}) => (
  <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(touched && error)}>
    <FormLabel component='legend'>{label}</FormLabel>
    <RadioGroup
      row={row}
      value={value}
      onChange={(e) => {
        onChange(e.target.value)
        onFieldChange && onFieldChange(e.target.value)
      }}
    >
      {children}
    </RadioGroup>
    {touched && error && <FormHelperText>{error}</FormHelperText>}
  </FormControl>
)
```

選択項目はFormControlLabelを使います。

```jsx
<Field name='radio' label='ラジオボタン' component={renderRadio} rootClass={classes.formControl} required >
  <FormControlLabel value='female' control={<Radio />} label='女性' />
  <FormControlLabel value='male' control={<Radio />} label='男性' />
  <FormControlLabel value='other' control={<Radio />} label='その他' />
</Field>
```

# チェックボックス
チェックボックスはFormControl、FormLabel、FormGroup、FormHelperText等の[Selection Component](https://material-ui.com/demos/selection-controls/)を使います。

```jsx
// CheckBox
const renderCheckBox = ({
  input: { value, onChange },
  label,
  children,
  meta: { touched, error },
  onFieldChange,
  row = true,
  required = false,
  rootClass = '',
}) => (
  <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(touched && error)}>
    <FormLabel component='legend'>{label}</FormLabel>
    <FormGroup
      row={row}
      value={value}
      onChange={(e) => {
        onChange(e.target.value)
        onFieldChange && onFieldChange(e.target.value)
      }}
    >
      {children}
    </FormGroup>
    {touched && error && <FormHelperText>{error}</FormHelperText>}
  </FormControl>
)
```

選択項目はFormControlLabelを使います。

```jsx
<Field name='checkbox' label='チェックボックス' component={renderCheckBox} rootClass={classes.formControl} required >
  <FormControlLabel value='check1' control={<Checkbox />} label='オプション１'/>
  <FormControlLabel value='check2' control={<Checkbox />} label='オプション２'/>
  <FormControlLabel value='check3' control={<Checkbox />} label='オプション３'/>
</Field>
```

# スイッチ
スイッチはFormControlLabel、Switch等の[Selection Component](https://material-ui.com/demos/selection-controls/)を使います。

```jsx
// Switch
const renderSwitch = ({
  input: { value, onChange },
  label,
  onFieldChange,
  rootClass = '',
}) => (
  <FormControlLabel
    classes={{root: rootClass}}
    control={
      <Switch
        checked={value}
        onChange={(e, bool) => {
          onChange(bool)
          onFieldChange && onFieldChange(bool)
        }}
      />
    }
    label={label}
  />
)
```

# ファイル（画像）
[Button](https://material-ui.com/demos/buttons/)を押すとファイル選択させます。
実際にはlabelのhtmlFor属性でinputタグ（非表示）をクリックさせ、ファイルを選択させます。
onChangeのイベントハンドラでe.target.files[0]からFileオブジェクトを取得します。

```jsx
// File
const renderFile = withStyles(() => ({
  input: {
    display: 'none',
  },
  button: {
    marginTop: 10,
  },
}))(
  ({
    input: { value, name, onChange },
    label,
    meta: { touched, error },
    classes,
    onFieldChange,
    buttonLabel,
    accept = '*',
    required = false,
    rootClass = '',
  }) => (
    <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(touched && error)}>
      <FormLabel component='legend'>{label}</FormLabel>
      <input
        accept={accept}
        className={classes.input}
        id={name}
        type='file'
        onChange={e => {
          e.preventDefault()
          onChange(e.target.files[0])
          onFieldChange && onFieldChange(e.target.files[0])
        }}
        onBlur={() => {}}
      />
      <label htmlFor={name}>
        <Button classes={{root: classes.button}} variant='outlined' component='span'>
          {buttonLabel || 'ファイルを選択'}
        </Button>
      </label>
      <label>{value && value.name}</label>
      {touched && error && <FormHelperText>{error}</FormHelperText>}
    </FormControl>
  )
)
```

acceptに正しいMIMEを指定します。（今回は画像なのでimage/*）
ファイルの拡張子が偽装されていないかのチェックは今回省略しています。

```jsx
<Field name='image' label='画像' accept='image/*' component={renderFile} rootClass={classes.formControl} required />
```

# 動的項目
動的に項目を増やしたい場合はReduxFormのFieldArray Componentを使います。
fieldsに項目の数分のComponentの配列が渡ってきます。
項目を追加したい場合は`fields.push({})`を行います。
項目を削除したい場合は`fields.remove(idx)`で指定の番号の項目を削除します。

```jsx
// Dynamic Items
const renderMembers = withStyles(theme => ({
  input: {
    display: 'flex',
  },
  space: {
    marginLeft: 10,
  },
  member: {
    marginTop: 10,
    padding: 10,
    border: `1px solid ${theme.palette.grey[400]}`,
    borderRadius: 10,
  },
  header: {
    marginTop: 0,
    marginBottom: 10,
  },
  addButton: {
    marginTop: 10,
  },
  deleteButton: {
    height: 30,
  },
}))(
  ({
    label,
    fields,
    meta: { error, submitFailed },
    classes,
    required = false,
    rootClass = '',
  }) => (
    <FormControl classes={{root: rootClass}} required={required} component='fieldset' error={!!(submitFailed && error)}>
      <FormLabel component='legend'>{label}</FormLabel>
      {fields.map((member, idx) => (
        <div key={idx} className={classes.member}>
          <h6 className={classes.header}>メンバー{idx + 1}</h6>
          <div className={classes.input}>
            <Field
              name={`${member}.lastName`}
              component={renderTextField}
              label='姓'
              rootClass={classes.space}
            />
            <Field
              name={`${member}.firstName`}
              component={renderTextField}
              label='名'
              rootClass={classes.space}
            />
            <Button classes={{root: [classes.deleteButton, classes.space].join(' ')}} size='small' variant='contained' color='secondary' onClick={() => fields.remove(idx)}>削除</Button>
          </div>
        </div>
      ))}
      <div>
        <Button classes={{root: classes.addButton}} size='medium' variant='contained' color='primary' onClick={() => fields.push({})}>追加</Button>
        {submitFailed && error && <FormHelperText>{error}</FormHelperText>}
      </div>
    </FormControl>
  )
)
```

Field内部で使っているComponentはFieldArray Componentでwrapします。

```jsx
<FieldArray name='members' label='メンバー' component={renderMembers} rootClass={classes.formControl} required />
```

validateチェックも配列形式でvaluesが渡ってくるため、
配列内の各項目に対してチェックが必要です。

```jsx
  validate: values => {
    const errors = {}
    if (!values.members || !values.members.length) {
      errors.members = { _error: '１人以上メンバーの追加が必要です' }
    } else {
      const membersArrayErrors = []
      values.members.forEach((member, memberIndex) => {
        const memberErrors = {}
        if (!member || !member.firstName) {
          memberErrors.firstName = '必須項目です'
          membersArrayErrors[memberIndex] = memberErrors
        }
        if (!member || !member.lastName) {
          memberErrors.lastName = '必須項目です'
          membersArrayErrors[memberIndex] = memberErrors
        }
      })
      if (membersArrayErrors.length) {
        errors.members = membersArrayErrors
      }
    }
  }
```

# 送信
submitボタンが押されるとReduxFormのhandleSubmitが実行され、submitメソッドが呼ばれます。
ファイル送信するため、今回はFormDataでフォームのデータを格納します。

```jsx
  submit = (values) => {
    const params = new FormData()
    params.append('text', values.text)
    params.append('password', values.password)
    params.append('textarea', values.textarea)
    params.append('select', values.select)
    params.append('radio', values.radio)
    params.append('checkbox', values.checkbox)
    params.append('switch', !!values.switch)
    params.append('image', values.image)
    params.append('members', JSON.stringify(values.members))
    this.props.create(params)
  }

  render () {
    const { classes, handleSubmit, send } = this.props

    return (
      <form onSubmit={handleSubmit(this.submit)} encType='multipart/form-data'>
      ...
      <Button type='submit' size='medium' variant='contained' color='primary' >送信</Button>
      </form>
    )
  }
```


# サーバ側

正しく送信されるか確認するサーバ側はnodejs+expressで作っています。（楽なので）
multipart-formデータのファイル受信をするために[multer](https://github.com/expressjs/multer)を導入しています。
今回はuploadsフォルダに画像をアップロードするようにしています。

```server.js
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const multer  = require('multer')
const upload = multer({ dest: 'uploads/' })

process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))

app.use(express.static('.'))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())

app.post('/api/user', upload.single('image'), (req, res) => {
  console.log(req.body)
  console.log(req.file)
  res.json({image: req.file, ...req.body, members: JSON.parse(req.body.members)})
})

app.listen(9090, () => {
  console.log('Access to http://localhost:9090')
})
```

次のようなログが出力され、uploadsフォルダに画像がアップロードされます。

```
{ text: 'てきすと',
  password: 'password',
  textarea: 'えりあ',
  select: '20',
  radio: 'male',
  checkbox: 'check1',
  switch: 'true',
  members: '[{"lastName":"ほげ","firstName":"ふが"},{"lastName":"たろう","firstName":"はなこ"}]' }
{ fieldname: 'image',
  originalname: 'mitumo.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: 'uploads/',
  filename: '223805f032c88a5c41335a7901e5e945',
  path: 'uploads/223805f032c88a5c41335a7901e5e945',
  size: 62471 }
```