Formikを使う
jaredpalmer/formik: Build forms in React, without the tears 😭
Formikはいくつかの点でRedux-Form
より優れている。
readmeによると、
Redux-Form
でないのかというのと、フォームの状態は本質的に一過性で局所的であるためにreduxでそれを追跡することは不要ため。
さらにredux-fromの場合必ずreducerを通るためアプリケーションの成長につれて描画コストが上がる。
またサイズも半分程度である。
ということがreadme
に書いてある。
まあだいたい同意で、ただのReactコンポーネントでしかのでテストも書きやすい。
render prop
HoCよりrender propの方がたいていの場合好ましいので、render propで書いていく。
function MyForm() {
return (
<div>
<h1>My Form</h1>
<Formik
initialValues={{
email: '',
password: ''
}}
validate={values => {
let errors = {}
if (!values.email) {
errors.email = 'Required'
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
) {
errors.email = 'Invalid email address'
}
return errors
}}
onSubmit={(values, { setSubmitting, setErrors }) => {
LoginToMyApp(values).then(
user => {
setSubmitting(false)
},
errors => {
setSubmitting(false)
setErrors(transformMyApiErrors(errors))
}
)
}}
render={({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
}) => (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
/>
{touched.email && errors.email && <div>{errors.email}</div>}
<input
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
/>
{touched.password &&
errors.password && <div>{errors.password}</div>}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
/>
</div>
)
}
ただ、これだと冗長な部分が多い。
Fieldコンポーネントを使うと冗長性をなくせる。
ついでに、validateも外出した方が見通しがよくなる。
また、yupと連携できるが、デフォルトのバリデーションの挙動が非同期になるのでテストでは注意。
function validate(values) {
let errors = {}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.password) {
errors.password = 'Required'
}
return errors
}
function MyForm2() {
return (
<div>
<h1>My Form2</h1>
<Formik
initialValues={{ email: '', password: '' }}
validate={validate}
onSubmit={(values, { setSubmitting, setErrors }) => {
LoginToMyApp(values).then(
user => {
setSubmitting(false)
},
errors => {
setSubmitting(false)
setErrors(transformMyApiErrors(errors))
}
)
}}
render={({ values, errors, touched, handleSubmit, isSubmitting }) => (
<form onSubmit={handleSubmit}>
<Field type="email" name="email" placeholder="Email" />
{touched.email && errors.email && <div>{errors.email}</div>}
<Field type="password" name="password" placeholder="Password" />
{touched.password &&
errors.password && <div>{errors.password}</div>}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
/>
</div>
)
}
また、エラーメッセージの部分が冗長なので、readmeにあるように自分でErrorMessage
コンポーネントを定義するとよい。
import { Field, getIn } from 'formik';
const ErrorMessage = ({ name }) => (
<Field
name={name}
render={({ form }) => {
const error = getIn(form.errors, name);
const touch = getIn(form.touched, name);
return touch && error ? error : null;
}}
/>
);
これを使うと非常にスッキリする。
function MyForm3() {
return (
<div>
<h1>My Form3</h1>
<Formik
initialValues={{ email: '', password: '' }}
validate={validate}
onSubmit={submit}
render={({ handleSubmit, isSubmitting }) => (
<form onSubmit={handleSubmit}>
<Field type="email" name="email" placeholder="Email" />
<ErrorMessage name="email" />
<Field type="password" name="password" placeholder="Password" />
<ErrorMessage name="password" />
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
/>
</div>
)
}
テストは別記事に書く予定。
バリデーション
デフォルトでyupと連携しているが、このバリデーションライブラリにはテストがないのでどこまで信用していいのか判断が微妙なところ。
またデフォルトで英語のロケーションが入るのでバリデーションについては検討の余地がある。