1. 程式人生 > >使用Formik輕松開發更高質量的React表單(二)使用指南

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

from def direct cti timeout lba ica pro nis

基礎

Imagine you want to build a form that lets you edit user data. However, your user API has nested objects like so.

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

When we are done we want our dialog to accept just a user, updateUser, and onClose props.

// 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>
  );
};

To make writing forms less verbose. Formik comes with a few helpers to save you key strokes.

  • <Field>

  • <Form />

    This is the exact same form as before, but written with <Form /> and <Field />:

// 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 is 100% compatible with React Native and React Native Web. However, because of differences between ReactDOM‘s and React Native‘s handling of forms and text input, there are two differences to be aware of. This section will walk you through them and what I consider to be best practices.

Before going any further, here‘s a super minimal gist of how to use Formik with React Native that demonstrates the key differences:

// Formik x React Native example
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);

As you can see above, the notable differences between using Formik with React DOM and React Native are:

Formik‘s props.handleSubmit is passed to a <Button onPress={...} /> instead of HTML <form onSubmit={...} /> component (since there is no <form /> element in React Native).
<TextInput /> uses Formik‘s props.handleChange(fieldName) and handleBlur(fieldName) instead of directly assigning the callbacks to props, because we have to get the fieldName from somewhere and with ReactNative we can‘t get it automatically like for web (using input name attribute). You can also use setFieldValue(fieldName, value) and setTouched(fieldName, bool) as an alternative.
Avoiding new functions in render
If for any reason you wish to avoid creating new functions on each render, I suggest treating React Native‘s <TextInput /> as if it were another 3rd party custom input element:

Write your own class wrapper around the custom input element
Pass the custom component props.setFieldValue instead of props.handleChange
Use a custom change handler callback that calls whatever you passed-in setFieldValue as (in this case we‘ll match the React Native TextInput API and call it this.props.onChangeText for parity).

// 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
      />
    );
  }
}

Then you could just use this custom input as follows:

// 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;

Using Formik with TypeScript

TypeScript Types

The Formik source code is written in TypeScript, so you can rest assured that types will always be up to date. As a mental model, Formik‘s types are very similar to React Router 4‘s <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;
}

// You may see / user InjectedFormikProps<OtherProps, FormValues> instead of what comes below. They are the same--InjectedFormikProps was artifact of when Formik only exported an HOC. It is also less flexible as it MUST wrap all props (it passes them through).

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>
  );
};

// The type of props MyForm receives
interface MyFormProps {
  initialEmail?: string;
  message: string; // if this passed all the way through you might do this or make a union type
}

// Wrap our form with the using withFormik HoC
const MyForm = withFormik<MyFormProps, FormValues>({
  // Transform outer props into form values
  mapPropsToValues: props => {
    return {
      email: props.initialEmail || ‘‘,
      password: ‘‘,
    };
  },

  // Add a custom validation function (this can be async too!)
  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);

// Use <MyForm /> anywhere
const Basic = () => (
  <div>
    <h1>My App</h1>
    <p>This can be anywhere in your application</p>
    <MyForm message="Sign up" />
  </div>
);

export default Basic;

How Form Submission Works

To submit a form in Formik, you need to somehow fire off the provided handleSubmit(e) or submitForm prop. When you call either of these methods, Formik will execute the following (pseudo code) each time:

"Pre-submit"
Touch all fields
Set isSubmitting to true
Increment submitCount + 1
"Validation"
Set isValidating to true
Run all field-level validations, validate, and validationSchema asynchronously and deeply merge results
Are there any errors?
Yes: Abort submission. Set isValidating to false, set errors, set isSubmitting to false
No: Set isValidating to false, proceed to "Submission"
"Submission"
Proceed with running your submission handler (i.e.onSubmit or handleSubmit)
you call setSubmitting(false) in your handler to finish the cycle

Frequently Asked Questions

How do I determine if my submission handler is executing?
Why does Formik touch all fields before submit?
How do I protect against double submits?
How do I know when my form is validating before submit?
If isValidating is true and isSubmitting is true.

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