为了账号安全,请及时绑定邮箱和手机立即绑定

使用Formik轻松开发更高质量的React表单

标签:
JavaScript

一个基本的例子

设想你要开发一个可以编辑用户数据的表单。不过,你的用户API端使用了具有类似下面的嵌套对象表达:

{   id: string,   email: string,   social: {     facebook: string,     twitter: string,     // ...   }}

最后,我们想使开发的对话框表单能够接收下面几个属性(props):user,updateUser和onClose(显然,user是一个对象,updateUser和onClose却都是两个方法)。

// User.jsimport React from 'react';import Dialog from 'MySuperDialog';import { Formik } from 'formik';const EditUserDialog = ({ user, updateUser, onClose }) => {  return (    <Dialog onClose={onClose}>      <h1>Edit User</h1>      <Formik        initialValues={user /** { email, social } */}        onSubmit={(values, actions) => {          CallMyApi(user.id, values).then(            updatedUser => {              actions.setSubmitting(false);              updateUser(updatedUser), onClose();            },            error => {              actions.setSubmitting(false);              actions.setErrors(transformMyAPIErrorToAnObject(error));            }          );        }}        render={({          values,          errors,          touched,          handleBlur,          handleChange,          handleSubmit,          isSubmitting,        }) => (          <form onSubmit={handleSubmit}>            <input              type="email"              name="email"              onChange={handleChange}              onBlur={handleBlur}              value={values.email}            />            {errors.email && touched.email && <div>{errors.email}</div>}            <input              type="text"              name="social.facebook"              onChange={handleChange}              onBlur={handleBlur}              value={values.social.facebook}            />            {errors.social &&              errors.social.facebook &&              touched.facebook && <div>{errors.social.facebook}</div>}            <input              type="text"              name="social.twitter"              onChange={handleChange}              onBlur={handleBlur}              value={values.social.twitter}            />            {errors.social &&              errors.social.twitter &&              touched.twitter && <div>{errors.social.twitter}</div>}            <button type="submit" disabled={isSubmitting}>              Submit            </button>          </form>        )}      />    </Dialog>  );};

简化编码

为了简化表单组件的编码,Formik还提供了两个帮助API:


  • <Field>

  • <Form />


于是,下面的代码与前面一致,只是使用<Form />和<Field />这两个API进行了改写:

// EditUserDialog.jsimport React from 'react';import Dialog from 'MySuperDialog';import { Formik, Field, Form } from 'formik';const EditUserDialog = ({ user, updateUser, onClose }) => {  return (    <Dialog onClose={onClose}>      <h1>Edit User</h1>      <Formik        initialValues={user /** { email, social } */}        onSubmit={(values, actions) => {          CallMyApi(user.id, values).then(            updatedUser => {              actions.setSubmitting(false);              updateUser(updatedUser), onClose();            },            error => {              actions.setSubmitting(false);              actions.setErrors(transformMyAPIErrorToAnObject(error));            }          );        }}        render={({ errors, touched, isSubmitting }) => (          <Form>            <Field type="email" name="email" />            {errors.email && touched.social.email && <div>{errors.email}</div>}            <Field type="text" name="social.facebook" />            {errors.social.facebook &&              touched.social.facebook && <div>{errors.social.facebook}</div>}            <Field type="text" name="social.twitter" />            {errors.social.twitter &&              touched.social.twitter && <div>{errors.social.twitter}</div>}            <button type="submit" disabled={isSubmitting}>              Submit            </button>          </Form>        )}      />    </Dialog>  );};

React Native开发问题


Formik与React Native 和React Native Web开发完全兼容。然而,由于ReactDOM和React Native表单处理与文本输入方式的不同,有两个区别值得注意。本文将介绍这个问题并推荐更佳使用方式。

在进一步讨论前,先来最简要地概括一下如何在React Native中使用Formik。下面的轮廓代码展示了两者的关键区别:

// Formik +React Native示例import React from 'react';import { Button, TextInput, View } from 'react-native';import { withFormik } from 'formik';const enhancer = withFormik({  /*...*/});const MyReactNativeForm = props => (  <View>    <TextInput      onChangeText={props.handleChange('email')}      onBlur={props.handleBlur('email')}      value={props.values.email}    />    <Button onPress={props.handleSubmit} title="Submit" />  </View>);export default enhancer(MyReactNativeForm);

从上面代码中,你会明显注意到在React Native 和React DOM开发中使用Formik存在如下不同:


(1)Formik的props.handleSubmit被传递给一个<Button onPress={...} />,而不是HTML <form onSubmit={...} /> 组件(因为在React Native中没有<form />元素)。

(2)<TextInput />使用Formik的props.handleChange(fieldName)和handleBlur(fieldName),而不是直接把回调函数赋值给props,因为我们必须从某处得到fieldName,而在ReactNative中我们无法你在Web中一样自动获取它(使用input的name属性)。作为可选方案,你还可以使用 setFieldValue(fieldName, value) 和setTouched(fieldName, bool) 这两个函数。


避免在render中创建新函数

如果因某种原因你想在每一个render中避免创建新函数,那么我建议你把React Native的 <TextInput /> 当作它是一个第三方提供的定制输入元素:


  • 编写你自己的针对定制输入元素的类包装器;

  • 传递定制组件的props.setFieldValue,而不是传递props.handleChange;

  • 使用一个定制的change函数回调,它将调用你传递给setFieldValue的任何内容。


请参考下面的代码:

// FormikReactNativeTextInput.jsimport * as React from 'react';import { TextInput } from 'react-native';export default class FormikReactNativeTextInput extends React.Component {  handleChange = (value: string) => {    // remember that onChangeText will be Formik's setFieldValue    this.props.onChangeText(this.props.name, value);  };  render() {    // we want to pass through all the props except for onChangeText    const { onChangeText, ...otherProps } = this.props;    return (      <TextInput        onChangeText={this.handleChange}        {...otherProps} // IRL, you should be more explicit when using TS      />    );  }}

然后,你可以像下面这样使用这个定制输入组件:

// MyReactNativeForm.jsimport { View, Button } from 'react-native';import TextInput from './FormikReactNativeTextInput';import { Formik } from 'formik';const MyReactNativeForm = props => (  <View>    <Formik      onSubmit={(values, actions) => {        setTimeout(() => {          console.log(JSON.stringify(values, null, 2));          actions.setSubmitting(false);        }, 1000);      }}      render={props => (        <View>          <TextInput            name="email"            onChangeText={props.setFieldValue}            value={props.values.email}          />          <Button title="submit" onPress={props.handleSubmit} />        </View>      )}    />  </View>);export default MyReactNativeForm;

使用TypeScript开发Formik表单

(一)TypeScript类型

Formik是使用TypeScript写的,Formik中的类型十分类似于React Router 4中的<Route>。

Render props (<Formik /> and <Field />)import * as React from 'react';import { Formik, FormikProps, Form, Field, FieldProps } from 'formik';interface MyFormValues {  firstName: string;}export const MyApp: React.SFC<{} /* whatever */> = () => {  return (    <div>      <h1>My Example</h1>      <Formik        initialValues={{ firstName: '' }}        onSubmit={(values: MyFormValues) => alert(JSON.stringify(values))}        render={(formikBag: FormikProps<MyFormValues>) => (          <Form>            <Field              name="firstName"              render={({ field, form }: FieldProps<MyFormValues>) => (                <div>                  <input type="text" {...field} placeholder="First Name" />                  {form.touched.firstName &&                    form.errors.firstName &&                    form.errors.firstName}                </div>              )}            />          </Form>        )}      />    </div>  );};

(二)使用withFormik()

import React from 'react';import * as Yup from 'yup';import { withFormik, FormikProps, FormikErrors, Form, Field } from 'formik';// Shape of form valuesinterface FormValues {  email: string;  password: string;}interface OtherProps {  message: string;}

顺便提醒一下,你可以使用InjectedFormikProps<OtherProps, FormValues>来代替下面的实现方式。本质上,它们是相同的,只不过InjectedFormikProps是当Formik仅输出一个HOC(高阶组件)时的代替而已。而且,这个方法灵活性差一些,因为它需要对所有属性(props)进行包装。

const InnerForm = (props: OtherProps & FormikProps<FormValues>) => {  const { touched, errors, isSubmitting, message } = props;  return (    <Form>      <h1>{message}</h1>      <Field type="email" name="email" />      {touched.email && errors.email && <div>{errors.email}</div>}      <Field type="password" name="password" />      {touched.password && errors.password && <div>{errors.password}</div>}      <button type="submit" disabled={isSubmitting}>        Submit      </button>    </Form>  );};//MyForm接收的props的类型interface MyFormProps {  initialEmail?: string;  message: string; // if this passed all the way through you might do this or make a union type}//使用withFormik高阶组件包装你的表单const MyForm = withFormik<MyFormProps, FormValues>({  // Transform outer props into form values  mapPropsToValues: props => {    return {      email: props.initialEmail || '',      password: '',    };  },  //添加定制的校验函数(也有可能是异步的)  validate: (values: FormValues) => {    let errors: FormikErrors = {};    if (!values.email) {      errors.email = 'Required';    } else if (!isValidEmail(values.email)) {      errors.email = 'Invalid email address';    }    return errors;  },  handleSubmit: values => {    // do submitting things  },})(InnerForm);// 你可以在任何地方使用<MyForm />const Basic = () => (  <div>    <h1>My App</h1>    <p>This can be anywhere in your application</p>    <MyForm message="Sign up" />  </div>);export default Basic;

Formik表单提交原理


要在Formik中提交表单,你需要以某种方式触发 handleSubmit(e) 或者submitForm属性调用(在Formik中这两个方法都是以属性的方式提供的)。 当调用其中一个方法时,Formik每次都会执行下面的伪代码:

(一)预提交
(1)修改所有字段
(2)把isSubmitting 设置为true
(3)submitCount + 1
(二)校验
(1)把isValidating设置为true
(2)异步运行所有字段级的校验和validationSchema,并深度合并执行结果
(3)判断是否存在错误:
如果存在错误:取消提交,把isValidating设置为false,设置错误信息,并把isSubmitting设置为false
如果不存在错误:Set isValidating to false, proceed to "Submission"
(三)提交
最后继续运行你的提交函数吧(例如是onSubmit或者handleSubmit)。你可以通过在你的处理器函数中调用setSubmitting(false) 来结束生命周期。

FAQ



(1)Q:怎么判定提交处理器(submission handler)正在执行中?
A:当isValidating为false且isSubmitting为true时。

(2)Q:为什么在提交前Formik要“润色一下(touch)”表单中所有字段?
A:通常,当UI表单中输入字段被操作过后(Formik中称为“touched”)只显示与之相关的错误信息。于是,在提交一个表单前,Formik会touch一下所有字段,这样所有可能隐藏的错误都会变得可见。

(3)Q:如何避免两次重复提交?
A:办法是当isSubmitting为true时,禁止所有能够触发提交的调用。

(4)Q:如何得知表单在提交前正在校验中?
A:如果isValidating为true而且isSubmitting也为true的话,......

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消