使用Formik輕鬆開發更高質量的React表單(二)使用指南

一個基本的例子

設想你要開發一個可以編輯用戶數據的表單。不過,你的用戶API端使用了具有類似下面的嵌套對象表達:

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

最後,我們想使開發的對話框表單能夠接收下面幾個屬性(props):user,updateUser和onClose(顯然,user是一個對象,updateUser和onClose卻都是兩個方法)。

// User.js
import 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.js
import 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.js
import * 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.js
import { 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 values
interface 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的話,......

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章