Repository: jaredpalmer/formik
Pull request #53 (2017-07-13 18:08:24)

0.8
- [x] Adds deprecation warnings to `setError`
- [x] Adds deprecation warnings to `handleChangeValue`
- [x] Adds deprecation warning to `mapValuesToPayload`
- [x] Adds `setFieldValue`, `setFieldError`, `setFieldTouched` methods
- [x] Adds `status` and `setStatus`
- [x] Adds warning about using `handleChange` with React Native
- [x] Adds new `validate` method. It will run sync or async depending on what is returned (object or Promise)
- [x] Makes `validationSchema` optional, Yup is now optional too
- [x] Adds `validateOnChange?: boolean` option to config
- [x] Adds `validateOnBlur?: boolean` option to config
- [x] Improved tests
- [x] Update documentation


Patch:
diff --git a/README.md b/README.md
index 6f5f61e..9faa7f2 100644
--- a/README.md
+++ b/README.md
@@ -7,19 +7,20 @@
 
 Let's face it, forms are really verbose in React. To make matters worse, most form helpers do wayyyy too much magic and often have a significant performance cost associated with them. Formik is a minimal Higher Order Component that helps you with the 3 most annoying parts: 
 
- 1. Transforming props to a flat React state
+ 1. Transforming props to form state
  2. Validation and error messages
- 3. Transforming a flat React state back into a consumable payload for your API
+ 3. Handling form submission
 
-Lastly, Formik helps you stay organized by colocating all of the above plus your submission handler in one place. This makes testing, refactoring, and reasoning about your forms a breeze.
+By colocating all of the above in one place, Formik will keep things organized--making testing, refactoring, and reasoning about your forms a breeze.
 
 ## Installation
 
-Add Formik and Yup to your project. Formik uses [Yup](https://github.com/jquense/yup) (which is like [Joi](https://github.com/hapijs/joi), but for the browser) for object schema validation. 
+Add Formik and Yup to your project. Formik supports [Yup](https://github.com/jquense/yup) (which is like [Joi](https://github.com/hapijs/joi), but for the browser) for object schema validation.
 
 ```bash
 npm i formik yup --save
 ```
+Note: Yup is 100% optional. You are free to [write your own validators][`validate`].
 
 You can also try before you buy with this **[demo on CodeSandbox.io](https://codesandbox.io/s/zKrK5YLDZ)**
 
@@ -42,33 +43,39 @@ You can also try before you buy with this **[demo on CodeSandbox.io](https://cod
   - [`Formik(options)`](#formikoptions)
     - [`options`](#options)
       - [`displayName?: string`](#displayname-string)
-      - [`handleSubmit: (payload, FormikBag) => void`](#handlesubmit-payload-formikbag--void)
-      - [`mapPropsToValues?: (props) => props`](#mappropstovalues-props--props)
-      - [`mapValuesToPayload?: (values) => payload`](#mapvaluestopayload-values--payload)
-      - [`validationSchema: Schema`](#validationschema-schema)
+      - [`handleSubmit: (values: Values, formikBag: FormikBag) => void`](#handlesubmit-values-values-formikbag-formikbag--void)
+        - [The "FormikBag":](#the-formikbag)
+      - [`mapPropsToValues?: (props: Props) => Values`](#mappropstovalues-props-props--values)
+      - [`validate?: (values: Values, props: Props) => FormikError<Values> | Promise<any>`](#validate-values-values-props-props--formikerrorvalues--promiseany)
+      - [`validateOnBlur?: boolean`](#validateonblur-boolean)
+      - [`validateOnChange?: boolean`](#validateonchange-boolean)
+      - [`validationSchema?: Schema`](#validationschema-schema)
     - [Injected props and methods](#injected-props-and-methods)
-      - [`error?: any`](#error-any)
+      - [`dirty: boolean`](#dirty-boolean)
       - [`errors: { [field: string]: string }`](#errors--field-string-string-)
       - [`handleBlur: (e: any) => void`](#handleblur-e-any--void)
       - [`handleChange: (e: React.ChangeEvent<any>) => void`](#handlechange-e-reactchangeeventany--void)
-      - [`handleChangeValue: (name: string, value: any) => void`](#handlechangevalue-name-string-value-any--void)
       - [`handleReset: () => void`](#handlereset---void)
       - [`handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
       - [`isSubmitting: boolean`](#issubmitting-boolean)
       - [`resetForm: (nextProps?: Props) => void`](#resetform-nextprops-props--void)
-      - [`setError(err: any) => void`](#seterrorerr-any--void)
-      - [`setErrors(fields: { [field: string]: string }) => void`](#seterrorsfields--field-string-string---void)
-      - [`setSubmitting(boolean) => void`](#setsubmittingboolean--void)
-      - [`setTouched(fields: { [field: string]: boolean }) => void`](#settouchedfields--field-string-boolean---void)
-      - [`setValues(fields: { [field: string]: any }) => void`](#setvaluesfields--field-string-any---void)
-      - [`touched: { [field: string]: boolean}`](#touched--field-string-boolean)
+      - [`setErrors: (fields: { [field: string]: string }) => void`](#seterrors-fields--field-string-string---void)
+      - [`setFieldError: (field: string, errorMsg: string) => void`](#setfielderror-field-string-errormsg-string--void)
+      - [`setFieldTouched: (field: string, isTouched: boolean) => void`](#setfieldtouched-field-string-istouched-boolean--void)
+      - [`setFieldValue: (field: string, value: any) => void`](#setfieldvalue-field-string-value-any--void)
+      - [`setStatus: (status?: any) => void`](#setstatus-status-any--void)
+      - [`setSubmitting: (boolean) => void`](#setsubmitting-boolean--void)
+      - [`setTouched: (fields: { [field: string]: boolean }) => void`](#settouched-fields--field-string-boolean---void)
+      - [`setValues: (fields: { [field: string]: any }) => void`](#setvalues-fields--field-string-any---void)
+      - [`status?: any`](#status-any)
+      - [`touched: { [field: string]: boolean }`](#touched--field-string-boolean-)
       - [`values: { [field: string]: any }`](#values--field-string-any-)
 - [Recipes](#recipes)
   - [Ways to call `Formik`](#ways-to-call-formik)
   - [Accessing React Component Lifecycle Functions](#accessing-react-component-lifecycle-functions)
     - [Example: Resetting a form when props change](#example-resetting-a-form-when-props-change)
   - [React Native](#react-native)
-    - [Why `handleChangeValue` instead of `handleChange`?](#why-handlechangevalue-instead-of-handlechange)
+    - [Why use `setFieldValue` instead of `handleChange`?](#why-use-setfieldvalue-instead-of-handlechange)
     - [Avoiding a Render Callback](#avoiding-a-render-callback)
 - [Authors](#authors)
 
@@ -115,7 +122,7 @@ import { Formik } from 'formik';
 import Yup from 'yup';
 
 // Formik is a Higher Order Component that wraps a React Form. Mutable form values 
-// are injected into a prop called `values`. Additionally, Formik injects
+// are injected into a prop called [`values`]. Additionally, Formik injects
 // an onChange handler that you can use on every input. You also get
 // handleSubmit, errors, and isSubmitting for free. This makes building custom
 // inputs easy.
@@ -174,7 +181,7 @@ export default Formik({
     facebook: Yup.string(),
   }),
 
-  // We now map React props to form values. These will be injected as `values` into
+  // We now map React props to form values. These will be injected as [`values`] into
   // our form. (Note: in the real world, you would destructure props, but for clarity this is
   // not shown)
   mapPropsToValues: props => ({
@@ -184,7 +191,7 @@ export default Formik({
   }),
 
   // Sometimes your API needs a different object shape than your form. Formik lets 
-  // you map `values` back into a `payload` before they are
+  // you map [`values`] back into a `payload` before they are
   // passed to handleSubmit.
   mapValuesToPayload: values => ({
     email: values.email,
@@ -231,69 +238,159 @@ Create a higher-order React component class that passes props and form handlers
 ##### `displayName?: string` 
 When your inner form component is a stateless functional component, you can use the `displayName` option to give the component a proper name so you can more easily find it in [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en). If specified, your wrapped form will show up as `Formik(displayName)`. If omitted, it will show up as `Formik(Component)`. This option is not required for class components (e.g. `class XXXXX extends React.Component {..}`).
 
-##### `handleSubmit: (payload, FormikBag) => void`
-Your form submission handler. It is passed the result of `mapValuesToPayload` (if specified), or the result of `mapPropsToValues`, or all props that are not functions (in that order of precedence) and the "`FormikBag`".
+##### `handleSubmit: (values: Values, formikBag: FormikBag) => void`
+Your form submission handler. It is passed your forms [`values`] and the "FormikBag", which includes an object containing a subset of the [injected props and methods](/#injected-props-and-methods) (i.e. all the methods with names that start with `set<Thing>` + `resetForm`) and any props that were passed to the the wrapped component.
 
-##### `mapPropsToValues?: (props) => props`
-If this option is specified, then Formik will transfer its results into updatable form state and make these values available to the new component as `props.values`. If `mapPropsToValues` is not specified, then Formik will map all props that are not functions to the new component's `props.values`. That is, if you omit it, Formik will only pass `props` where `typeof props[k] !== 'function'`, where `k` is some key.
+###### The "FormikBag":
 
-##### `mapValuesToPayload?: (values) => payload`
-If this option is specified, then Formik will run this function just before calling `handleSubmit`. Use it to transform your form's `values` back into a shape that's consumable for other parts of your application or API. If `mapValuesToPayload` **is not** specified, then Formik will map all `values` directly to `payload` (which will be passed to `handleSubmit`). While this transformation can be moved into `handleSubmit`, consistently defining it in `mapValuesToPayload` separates concerns and helps you stay organized.
+- `props` (props passed to the wrapped component)
+- [`resetForm`]
+- [`setErrors`]
+- [`setFieldError`]
+- [`setFieldTouched`]
+- [`setFieldValue`]
+- [`setStatus`]
+- [`setSubmitting`]
+- [`setTouched`]
+- [`setValues`]
 
-##### `validationSchema: Schema`
-[A Yup schema](https://github.com/jquense/yup). This is used for validation on each onChange event. Errors are mapped by key to the `WrappedComponent`'s `props.errors`. Its keys should almost always match those of `WrappedComponent's` `props.values`. 
+Note: [`errors`], [`touched`], [`status`] and all event handlers are NOT included in the `FormikBag`.
+
+##### `mapPropsToValues?: (props: Props) => Values`
+
+If this option is specified, then Formik will transfer its results into updatable form state and make these values available to the new component as [`props.values`][`values`]. If `mapPropsToValues` is not specified, then Formik will map all props that are not functions to the inner component's [`props.values`][`values`]. That is, if you omit it, Formik will only pass `props` where `typeof props[k] !== 'function'`, where `k` is some key. 
+
+Even if your form is not receiving any props from its parent, use `mapPropsToValues` to initialize your forms empty state.
+
+##### `validate?: (values: Values, props: Props) => FormikError<Values> | Promise<any>`
+
+_Note: I suggest using [`validateSchema`] and Yup for validation. However, `validate` is a dependency-free, straightforward way to validate your forms._
+
+Validate the form's [`values`] with function. This function can either be:
+
+1. Synchronous and return an [`errors`] object. 
+
+```js
+// Synchronous validation
+const validate = (values, props) => {
+  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
+}
+```
+- Asynchronous and return a Promise that's error is an [`errors`] object
+
+```js
+// Async Validation
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
+
+const validate = (values, props) => {
+  return sleep(2000).then(() => {
+    let errors = {}
+    if (['admin', 'null', 'god']).includes(values.username) {
+      errors.username = 'Nice try'
+    }
+    // ...
+    if (Object.keys(errors).length) {
+      throw errors
+    }
+  })
+}
+```
+
+##### `validateOnBlur?: boolean`
+
+Default is `true`. Use this option to run validations on `blur` events. More specifically, when either [`handleBlur`], [`setFieldTouched`], or [`setTouched`] are called.
+
+##### `validateOnChange?: boolean`
+
+Default is `false`. Use this option to tell Formik to run validations on `change` events and `change`-related methods. More specifically, when either [`handleChange`], [`setFieldValue`], or [`setValues`] are called.
+
+##### `validationSchema?: Schema`
+
+[A Yup schema](https://github.com/jquense/yup). This is used for validation. Errors are mapped by key to the inner component's [`errors`][`errors`]. Its keys should match those of [`values`]. 
 
 #### Injected props and methods
 
-The following props and methods will be injected into the `WrappedComponent` (i.e. your form):
+The following list of properties and methods will be injected into the your inner form component as React props after you "enhance"/wrap it with the Formik HoC. 
 
-##### `error?: any`
-A top-level error object, can be whatever you need.
+- If your inner form component is a stateless function, then each item will be made available as `props.xxxx`. 
+- If your inner form component is an ES6 class, then each item below will be made available as `this.props.xxxx`. 
+
+Any additional props you pass to your wrapped component (including those props you do not map to form state via [`mapPropsToVales`]) are passed directly down untouched.
+
+##### `dirty: boolean`
+
+Returns `true` if any field has been touched by any means, `false` otherwise. `dirty` is a readonly computed property and should not be mutated directly.
 
 ##### `errors: { [field: string]: string }`
-Form validation errors. Keys match the shape of the `validationSchema` defined in Formik options. This should therefore also map to your `values` object as well. Internally, Formik transforms raw [Yup validation errors](https://github.com/jquense/yup#validationerrorerrors-string--arraystring-value-any-path-string) on your behalf. 
+
+Form validation errors. Should match the shape of your form's [`values`] defined in Formik options. If you are using [`validationSchema`] (which you should be), keys and shape will match your schema exactly. Internally, Formik transforms raw [Yup validation errors](https://github.com/jquense/yup#validationerrorerrors-string--arraystring-value-any-path-string) on your behalf. If you are using [`validate`], then that function will determine the `errors` objects shape. 
 
 ##### `handleBlur: (e: any) => void`
-`onBlur` event handler. Useful for when you need to track whether an input has been `touched` or not. This should be passed to `<input onBlur={handleBlur} ... />`
+`onBlur` event handler. Useful for when you need to track whether an input has been [`touched`] or not. This should be passed to `<input onBlur={handleBlur} ... />`
+
+DOM-only. Use [`setFieldTouched`] in React Native.
 
 ##### `handleChange: (e: React.ChangeEvent<any>) => void`
-General input change event handler. This will update the form value according to an input's `name` attribute. If `name` is not present, `handleChange` will look for an input's `id` attribute. Note: "input" here means all HTML inputs.
+General input change event handler. This will update the `values[key]` where `key` is the event-emitting input's `name` attribute. If the `name` attribute is not present, `handleChange` will look for an input's `id` attribute. Note: "input" here means all HTML inputs.
 
-##### `handleChangeValue: (name: string, value: any) => void`
-Custom input change handler. Use this when you have custom inputs. `name` should match the key of form value you wish to update.
+DOM-only. Use [`setFieldValue`] in React Native. 
 
 ##### `handleReset: () => void`
-Reset handler. This should be passed to `<button onClick={handleReset}>...</button>`
+Reset handler. Will reset the form to its initial state. This should be passed to `<button onClick={handleReset}>...</button>`
 
 ##### `handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`
 Submit handler. This should be passed to `<form onSubmit={handleSubmit}>...</form>`
 
 ##### `isSubmitting: boolean`
-Submitting state. Either `true` or `false`. Formik will set this to `true` on your behalf before calling `handleSubmit` to reduce boilerplate.
+Submitting state. Either `true` or `false`. Formik will set this to `true` on your behalf before calling [`handleSubmit`] to reduce boilerplate.
 
 ##### `resetForm: (nextProps?: Props) => void`
-Imperatively reset the form. This will clear `errors` and `touched`, set `isSubmitting` to `false` and rerun `mapPropsToValues` with the current `WrappedComponent`'s `props` or what's passed as an argument. That latter is useful for calling `resetForm` within `componentWillReceiveProps`.
+Imperatively reset the form. This will clear [`errors`] and [`touched`], set [`isSubmitting`] to `false` and rerun `mapPropsToValues` with the current `WrappedComponent`'s `props` or what's passed as an argument. That latter is useful for calling `resetForm` within `componentWillReceiveProps`.
 
-##### `setError(err: any) => void`
-Set a top-level `error` object. This can only be done manually. It is an escape hatch.
+##### `setErrors: (fields: { [field: string]: string }) => void`
+Set `errors` imperatively.
 
-##### `setErrors(fields: { [field: string]: string }) => void`
-Set `errors` manually.
+##### `setFieldError: (field: string, errorMsg: string) => void`
+Set the error message of a field imperatively. `field` should match the key of [`errors`] you wish to update.  Useful for creating custom input error handlers.
 
-##### `setSubmitting(boolean) => void`
-Set `isSubmitting` manually.
+##### `setFieldTouched: (field: string, isTouched: boolean) => void`
+Set the touched state of a field imperatively. `field` should match the key of [`touched`] you wish to update.  Useful for creating custom input blur handlers.
 
-##### `setTouched(fields: { [field: string]: boolean }) => void`
-Set `touched` manually.
+##### `setFieldValue: (field: string, value: any) => void`
+Set the value of a field imperatively. `field` should match the key of [`values`] you wish to update.  Useful for creating custom input change handlers.
 
-##### `setValues(fields: { [field: string]: any }) => void`
-Set `values` manually.
+##### `setStatus: (status?: any) => void`
+Set a top-level [`status`] to anything you want imperatively. Useful for controlling arbitrary top-level state related to your form. For example, you can use it to pass API responses back into your component in [`handleSubmit`].
 
-##### `touched: { [field: string]: boolean}`
-Touched fields. Use this to keep track of which fields have been visited. Use `handleBlur` to toggle on a given input. Keys work like `errors` and `values`.
+##### `setSubmitting: (boolean) => void`
+Set [`isSubmitting`] imperatively.
+
+##### `setTouched: (fields: { [field: string]: boolean }) => void`
+Set [`touched`] imperatively.
+
+##### `setValues: (fields: { [field: string]: any }) => void`
+Set [`values`] imperatively.
+
+##### `status?: any`
+A top-level status object that you can use to represent form state that can't otherwised be expressed/stored with other methods. This is useful for capturing and passing through API responses to your inner component. 
+
+`status` should only be modifed by calling [`setStatus: (status?: any) => void`](#setstatus-status-any--void)
+
+##### `touched: { [field: string]: boolean }`
+Touched fields. Each key corresponds to a field that has been touched/visited.
 
 ##### `values: { [field: string]: any }`
-Your form's values, the result of `mapPropsToValues` (if specified) or all props that are not functions passed to your `WrappedComponent`.
+Your form's values. Will have the shape of the result of [`mapPropsToValues`] (if specified) or all props that are not functions passed to your wrapped component.
 
 ## Recipes
 
@@ -302,6 +399,7 @@ Your form's values, the result of `mapPropsToValues` (if specified) or all props
 Formik is a Higher Order Component factory; you can use it exactly like React Redux's `connect` or Apollo's `graphql`. There are three ways to call Formik on your component:
 
 You can assign the HoC returned by Formik to a variable (i.e. `withFormik`) for later use.
+
 ```js
 import React from 'react';
 import { Formik } from 'formik';
@@ -419,7 +517,7 @@ const withFormik = Formik({...})
 const MyReactNativeForm = (props) => (
   <View>
     <TextInput 
-      onChangeText={text => props.handleChangeValue('email', text)} 
+      onChangeText={text => props.setFieldValue('email', text)} 
       value={props.values.email}
     />
    <Button onPress={props.handleSubmit} title="Submit" /> // 
@@ -432,12 +530,12 @@ export default withFormik(MyReactNativeForm)
 As you can see above, the notable differences between using Formik with React DOM and React Native are:
 
 1. 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).
-2. `<TextInput />` uses Formik's `props.handleChangeValue` instead of `props.handleChange`. To understand why, see the discussion below.
+2. `<TextInput />` uses Formik's `props.setFieldValue` instead of `props.handleChange`. To understand why, see the discussion below.
 
 
-#### Why use `handleChangeValue` instead of `handleChange`?
+#### Why use `setFieldValue` instead of `handleChange`?
 
-'cuz `handleChange` will not work in React Native...
+'cuz [`handleChange`] will not work in React Native...
 
 ```js
 import { Button, TextInput, View } from 'react-native'
@@ -460,7 +558,7 @@ const MyReactNativeForm = (props) => (
 export default withFormik(MyReactNativeForm)
 ```
 
-The reason is that Formik's `props.handleChange` function expects its first argument to be synthetic DOM event where the `event.target` is the DOM input element and `event.target.id` or `event.target.name` matches the field to be updated. Without this, `props.handleChange` will do nothing. 
+The reason is that Formik's [`handleChange`] function expects its first argument to be synthetic DOM event where the `event.target` is the DOM input element and `event.target.id` or `event.target.name` matches the field to be updated. Without this, [`handleChange`] will do nothing. 
 
 In React Native, neither [`<TextInput />`](https://facebook.github.io/react-native/docs/textinput.html)'s [`onChange`](https://facebook.github.io/react-native/docs/textinput.html#onchange) nor [`onChangeText`](https://facebook.github.io/react-native/docs/textinput.html#onchange) callbacks pass such an event or one like it to its callback. Instead, they do the following *(emphasis added)*:
 
@@ -471,7 +569,7 @@ In React Native, neither [`<TextInput />`](https://facebook.github.io/react-nati
 > Callback that is called when the text input's text changes. **Changed text is passed as an argument to the callback handler.**
 
 
-However, Formik works just fine if you use `props.handleChangeValue`! Philisophically, just treat React Native's `<TextInput/>` the same way you would any other 3rd party custom input element.
+However, Formik works just fine if you use `props.setFieldValue`! Philisophically, just treat React Native's `<TextInput/>` the same way you would any other 3rd party custom input element.
 
 In conclusion, the following WILL work in React Native:
 
@@ -481,7 +579,7 @@ In conclusion, the following WILL work in React Native:
 export const MyReactNativeForm = (props) => (
   <View>
     <TextInput 
-      onChangeText={text => props.handleChangeValue('email', text) } 
+      onChangeText={text => props.setFieldValue('email', text) } 
       value={props.values.email} 
     />
     <Button onPress={props.handleSubmit} />
@@ -495,8 +593,8 @@ export const MyReactNativeForm = (props) => (
 If you are like me and do not like render callbacks, 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.handleChangeValue` instead of `props.handleChange`
-  - Use a custom change handler callback that calls whatever you passed-in `handleChangeValue` as (in this case we'll match the React Native TextInput API and call it `this.props.onChangeText` for parity).
+  - Pass the custom component [`props.setFieldValue`][`setFieldValue`] instead of [`props.handleChange`][`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).
 
 ```tsx
 // FormikReactNativeTextInput.tsx
@@ -506,7 +604,7 @@ import { TextInput } from 'react-native'
 interface FormikReactNativeTextInputProps {
   /** Current value of the input */
   value: string;
-  /** Change handler (this will be Formik's handleChangeValue ;) ) */
+  /** Change handler (this will be Formik's setFieldValue ;) ) */
   onChangeText: (value: string) => void;
   /** The name of the Formik field to be updated upon change */
   name: string;
@@ -516,7 +614,7 @@ interface FormikReactNativeTextInputProps {
 
 export default class FormikReactNativeTextInput extends React.Component<FormikReactNativeTextInputProps, {}> {
     handleChange = (value: string) => {
-       // remember that onChangeText will be Formik's handleChangeValue
+       // remember that onChangeText will be Formik's setFieldValue
        this.props.onChangeText(this.props.name, value)
     }
     
@@ -533,12 +631,12 @@ export default class FormikReactNativeTextInput extends React.Component<FormikRe
 }
 ```
 
+
 Then you could just use this custom input as follows: 
 
 ```tsx
 // MyReactNativeForm.tsx
-import { View } from 'react-native'
-import Button from './MyButton' // Assume this just exists
+import { View, Button } from 'react-native'
 import { FormikReactNativeTextInput as TextInput } from './FormikReactNativeTextInput'
 import { Formik, InjectedFormikProps } from 'formik'
 
@@ -550,7 +648,7 @@ export const MyReactNativeForm: React.SFC<InjectedFormikProps<Props, Values>> =
   <View>
     <TextInput 
       name="email"
-      onChangeText={props.handleChangeValue} 
+      onChangeText={props.setFieldValue} 
       value={props.values.email} 
     />
     <Button onPress={props.handleSubmit} />
@@ -568,3 +666,35 @@ export default Formik<Props, Values, Payload>({ ... })(MyReactNativeForm)
 - Ian White [@eonwhite](https://twitter.com/eonwhite)
 
 MIT License. 
+
+
+[`displayName`]: #displayname-string
+[`handleSubmit`]: #handlesubmit-payload-formikbag--void
+[`FormikBag`]: #the-formikbag
+[`mapPropsToValues`]: #mappropstovalues-props--props
+[`validate`]: #validate-values-values-props-props--formikerrorvalues--promiseany
+[`validateOnBlur`]: #validateonblur-boolean
+[`validateOnChange`]: #validateonchange-boolean
+[`validationSchema`]: #validationschema-schema
+  
+[Injected props and methods]: #injected-props-and-methods
+
+[`dirty`]: #dirty-boolean
+[`errors`]: #errors--field-string-string-
+[`handleBlur`]: #handleblur-e-any--void
+[`handleChange`]: #handlechange-e-reactchangeeventany--void
+[`handleReset`]: #handlereset---void
+[`handleSubmit`]: #handlesubmit-e-reactformeventhtmlformevent--void
+[`isSubmitting`]: #issubmitting-boolean
+[`resetForm`]: #resetform-nextprops-props--void
+[`setErrors`]: #seterrors-fields--field-string-string---void
+[`setFieldError`]: #setfielderror-field-string-errormsg-string--void
+[`setFieldTouched`]: #setfieldtouched-field-string-istouched-boolean--void
+[`setFieldValue`]: #setfieldvalue-field-string-value-any--void
+[`setStatus`]: #setstatus-status-any--void
+[`setSubmitting`]: #setsubmitting-boolean--void
+[`setTouched`]: #settouched-fields--field-string-boolean---void
+[`setValues`]: #setvalues-fields--field-string-any---void
+[`status`]: #status-any
+[`touched`]: #touched--field-string-boolean-
+[`values`]: #values--field-string-any-
\ No newline at end of file
diff --git a/example/Form.tsx b/example/Form.tsx
index 76db326..45b7af6 100644
--- a/example/Form.tsx
+++ b/example/Form.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import * as yup from 'yup';
 
-import { Formik, FormikBag, InjectedFormikProps } from 'formik';
+import { Formik, FormikBag, InjectedFormikProps } from '../src/formik';
 
 export interface Props {
   email: string;
diff --git a/package.json b/package.json
index 50b2749..ec2050e 100644
--- a/package.json
+++ b/package.json
@@ -18,9 +18,7 @@
   "main": "dist/formik.js",
   "module": "dist/formik.es6.js",
   "typings": "dist/types/formik.d.ts",
-  "files": [
-    "dist"
-  ],
+  "files": ["dist"],
   "scripts": {
     "test": "jest --env=jsdom",
     "test:watch": "npm run test -- --watch",
@@ -28,7 +26,7 @@
     "prebuild": "rimraf dist",
     "build": "tsc && NODE_ENV=production rollup -c && NODE_ENV=production rollup -c rollup.config.umd.js && NODE_ENV=development rollup -c rollup.config.umd.js && rimraf compiled",
     "prepublish": "npm run build",
-    "format": "prettier --trailing-comma es5 --single-quote --write 'src/*/*.tsx'",
+    "format": "prettier --trailing-comma es5 --single-quote --write 'src/**/*' 'test/**/*'",
     "precommit": "lint-staged"
   },
   "dependencies": {},
@@ -74,9 +72,7 @@
     ]
   },
   "jest": {
-    "collectCoverageFrom": [
-      "src/**/*.{ts,tsx}"
-    ],
+    "collectCoverageFrom": ["src/**/*.{ts,tsx}"],
     "transform": {
       ".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
     },
@@ -84,14 +80,7 @@
       "<rootDir>/test/**/*.ts?(x)",
       "<rootDir>/test/**/?(*.)(spec|test).ts?(x)"
     ],
-    "transformIgnorePatterns": [
-      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"
-    ],
-    "moduleFileExtensions": [
-      "ts",
-      "tsx",
-      "js",
-      "json"
-    ]
+    "transformIgnorePatterns": ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"],
+    "moduleFileExtensions": ["ts", "tsx", "js", "json"]
   }
 }
diff --git a/src/formik.tsx b/src/formik.tsx
index 53032ca..5ca2e51 100644
--- a/src/formik.tsx
+++ b/src/formik.tsx
@@ -1,121 +1,141 @@
 import * as React from 'react';
 
+import { isPromise, isReactNative } from './utils';
+
 import { hoistNonReactStatics } from './hoistStatics';
 
 /**
- * Transform Yup ValidationError to a more usable object
+ * Values of fields in the form
  */
-export function yupToFormErrors(yupError: any): FormikErrors {
-  let errors: FormikErrors = {};
-  for (let err of yupError.inner) {
-    if (!errors[err.path]) {
-      errors[err.path] = err.message;
-    }
-  }
-  return errors;
+export interface FormikValues {
+  [field: string]: any;
 }
 
 /**
- * Given an object, map keys to boolean
+ * An object containing error messages whose keys correspond to FormikValues.
  */
-export function touchAllFields<T>(fields: T): { [field: string]: boolean } {
-  let touched: { [field: string]: boolean } = {};
-  for (let k of Object.keys(fields)) {
-    touched[k] = true;
-  }
-  return touched;
-}
+export type FormikErrors<Values extends FormikValues> = {
+  [Key in keyof Values]?: string
+};
+
 /**
- * Validate a yup schema.
+ * An object containing touched state of the form whose keys correspond to FormikValues.
  */
-export function validateFormData<T>(data: T, schema: any): Promise<void> {
-  let validateData: any = {};
-  for (let k in data) {
-    if (data.hasOwnProperty(k)) {
-      const key = String(k);
-      validateData[key] = (data as any)[key] !== '' ? (data as any)[key] : undefined;
-    }
-  }
-  return schema.validate(validateData, { abortEarly: false });
-}
-
-export interface FormikValues {
-  [field: string]: any;
-}
-
-// @todo make this limited to keys of values
-export interface FormikErrors {
-  [field: string]: string;
-}
-
-// @todo make this limited to keys of values
-// interfact FormikTouched<Values, Field extends keyof Values> ??
-export interface FormikTouched {
-  [field: string]: boolean;
-}
+export type FormikTouched<Values extends FormikValues> = {
+  [Key in keyof Values]?: boolean
+};
 
 /**
  * Formik configuration options
  */
-export interface FormikConfig<Props, Values, Payload> {
+export interface FormikConfig<
+  Props,
+  Values extends FormikValues,
+  DeprecatedPayload = Values
+  // tslint:disable-next-line:one-line
+> {
   displayName?: string;
-  /* Map props to the form values */
+  /** Map props to the form values */
   mapPropsToValues?: (props: Props) => Values;
-  /* Map form values to submission payload */
-  mapValuesToPayload?: (values: Values) => Payload;
-  /*  Yup Schema */
-  validationSchema: any;
-  /* Submission handler */
-  handleSubmit: (payload: Payload, formikBag: FormikBag<Props, Values>) => void;
+  /** 
+   * Map form values to submission payload 
+   * @deprecated since 0.8.0
+   */
+  mapValuesToPayload?: (values: Values) => DeprecatedPayload;
+  /** 
+   * Validation function. Must return an error object or promise that 
+   * throws an error object where that object keys map to corresponding value.
+   */
+  validate?: (
+    values: Values,
+    props: Props
+  ) => void | FormikErrors<Values> | Promise<any>;
+  /** A Yup Schema */
+  validationSchema?: any;
+  /** Submission handler */
+  handleSubmit: (
+    values: Values | DeprecatedPayload,
+    formikBag: FormikBag<Props, Values>
+  ) => void;
+  /** Tells Formik to validate the form on each input's onChange event */
+  validateOnChange?: boolean;
+  /** Tells Formik to validate the form on each input's onBlur event */
+  validateOnBlur?: boolean;
 }
 
 /**
  * Formik state tree
  */
-export interface FormikState<V> {
-  /* Form values */
-  values: V;
-  /* Top level error, in case you need it */
+export interface FormikState<Values> {
+  /** Form values */
+  values: Values;
+  /** 
+   * Top level error, in case you need it 
+   * @deprecated since 0.8.0
+   */
   error?: any;
   /** map of field names to specific error for that field */
-  errors: FormikErrors;
+  errors: FormikErrors<Values>;
   /** map of field names to whether the field has been touched */
-  touched: FormikTouched;
+  touched: FormikTouched<Values>;
   /** whether the form is currently submitting */
   isSubmitting: boolean;
+  /** Top level status state, in case you need it */
+  status?: any;
+}
+
+/**
+ * Formik computed properties. These are read-only.
+ */
+export interface FormikComputedProps {
+  /** True if any input has been touched. False otherwise. */
+  readonly dirty: boolean;
 }
 
 /**
  * Formik state helpers
  */
-export interface FormikActions<P, V> {
-  /* Manually set top level error */
+export interface FormikActions<Props, Values> {
+  /** Manually set top level status. */
+  setStatus: (status?: any) => void;
+  /** 
+   * Manually set top level error 
+   * @deprecated since 0.8.0
+   */
   setError: (e: any) => void;
-  /* Manually set Errors */
-  setErrors: (errors: FormikErrors) => void;
-  /* Manually set isSubmitting */
+  /** Manually set errors object */
+  setErrors: (errors: FormikErrors<Values>) => void;
+  /** Manually set isSubmitting */
   setSubmitting: (isSubmitting: boolean) => void;
-  /* Manually set touched fields */
-  setTouched: (touched: FormikTouched) => void;
-  /* Manually set values  */
-  setValues: (values: V) => void;
-  /* Reset form */
-  resetForm: (nextProps?: P) => void;
+  /** Manually set touched object */
+  setTouched: (touched: FormikTouched<Values>) => void;
+  /** Manually set values object  */
+  setValues: (values: Values) => void;
+  /** Set value of form field directly */
+  setFieldValue: (field: keyof Values, value: any) => void;
+  /** Set error message of a form field directly */
+  setFieldError: (field: keyof Values, message: string) => void;
+  /** Set whether field has been touched directly */
+  setFieldTouched: (field: keyof Values, isTouched?: boolean) => void;
+  /** Reset form */
+  resetForm: (nextProps?: Props) => void;
+  /** Submit the form imperatively */
+  submitForm: () => void;
 }
 
 /**
  * Formik form event handlers 
  */
 export interface FormikHandlers {
-  /* Form submit handler */
+  /** Form submit handler */
   handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
-  /* Classic React change handler, keyed by input name */
+  /** Classic React change handler, keyed by input name */
   handleChange: (e: React.ChangeEvent<any>) => void;
-  /* Mark input as touched */
+  /** Mark input as touched */
   handleBlur: (e: any) => void;
-  /* Change value of form field directly */
+  /** Change value of form field directly */
   handleChangeValue: (name: string, value: any) => void;
-  /* Reset form event handler  */
+  /** Reset form event handler  */
   handleReset: () => void;
 }
 
@@ -125,43 +145,55 @@ export interface FormikHandlers {
 export type InjectedFormikProps<Props, Values> = Props &
   FormikState<Values> &
   FormikActions<Props, Values> &
-  FormikHandlers;
+  FormikHandlers &
+  FormikComputedProps;
 
 /**
  * Formik actions + { props }
  */
 export type FormikBag<P, V> = { props: P } & FormikActions<P, V>;
 
-export type CompositeComponent<P> = React.ComponentClass<P> | React.StatelessComponent<P>;
+export type CompositeComponent<P> =
+  | React.ComponentClass<P>
+  | React.StatelessComponent<P>;
 
 export interface ComponentDecorator<TOwnProps, TMergedProps> {
-  (component: CompositeComponent<TMergedProps>): React.ComponentClass<TOwnProps>;
+  (component: CompositeComponent<TMergedProps>): React.ComponentClass<
+    TOwnProps
+  >;
 }
 
 export interface InferableComponentDecorator<TOwnProps> {
   <T extends CompositeComponent<TOwnProps>>(component: T): T;
 }
 
-export function Formik<Props, Values extends FormikValues, Payload>({
+export function Formik<Props, Values extends FormikValues, Payload = Values>({
   displayName,
   mapPropsToValues = vanillaProps => {
     let values: Values = {} as Values;
     for (let k in vanillaProps) {
-      if (vanillaProps.hasOwnProperty(k) && typeof vanillaProps[k] !== 'function') {
+      if (
+        vanillaProps.hasOwnProperty(k) &&
+        typeof vanillaProps[k] !== 'function'
+      ) {
         values[k] = vanillaProps[k];
       }
     }
     return values;
   },
-  mapValuesToPayload = values => {
-    // in this case Values and Payload are the same.
-    const payload = (values as any) as Payload;
-    return payload;
-  },
+  mapValuesToPayload,
+  validate,
   validationSchema,
   handleSubmit,
-}: FormikConfig<Props, Values, Payload>): ComponentDecorator<Props, InjectedFormikProps<Props, Values>> {
-  return function wrapWithFormik(WrappedComponent: CompositeComponent<InjectedFormikProps<Props, Values>>): any {
+  validateOnChange = false,
+  validateOnBlur = true,
+}: FormikConfig<Props, Values, Payload>): ComponentDecorator<
+  Props,
+  InjectedFormikProps<Props, Values>
+> {
+  return function wrapWithFormik(
+    WrappedComponent: CompositeComponent<InjectedFormikProps<Props, Values>>
+  ) {
     class Formik extends React.Component<Props, FormikState<Values>> {
       public static displayName = `Formik(${displayName ||
         WrappedComponent.displayName ||
@@ -178,21 +210,42 @@ export function Formik<Props, Values extends FormikValues, Payload>({
           touched: {},
           isSubmitting: false,
         };
+
+        if (mapValuesToPayload) {
+          console.warn(
+            `Warning: Formik\'s mapValuesToPayload is deprecated and may be removed in future releases. Move that function to the first line of \`handleSubmit\` instead.`
+          );
+        }
       }
 
-      setErrors = (errors: FormikErrors) => {
+      setErrors = (errors: FormikErrors<Values>) => {
         this.setState({ errors });
       };
 
-      setTouched = (touched: FormikTouched) => {
+      setTouched = (touched: FormikTouched<Values>) => {
         this.setState({ touched });
+        if (validateOnBlur) {
+          this.runValidations(this.state.values);
+        }
       };
 
       setValues = (values: Values) => {
         this.setState({ values });
+        if (validateOnChange) {
+          this.runValidations(values);
+        }
+      };
+
+      setStatus = (status?: any) => {
+        this.setState({ status });
       };
 
       setError = (error: any) => {
+        if (process.env.NODE_ENV !== 'production') {
+          console.warn(
+            `Warning: Formik\'s setError(error) is deprecated and may be removed in future releases. Please use Formik\'s setStatus(status) instead. It works identically. For more info see https://github.com/jaredpalmer/formik#setstatus-status-any--void`
+          );
+        }
         this.setState({ error });
       };
 
@@ -200,11 +253,58 @@ export function Formik<Props, Values extends FormikValues, Payload>({
         this.setState({ isSubmitting });
       };
 
+      /**
+       * Run validation against a Yup schema and optionally run a function if successful
+       */
+      runValidationSchema = (values: Values, onSuccess?: Function) => {
+        validateYupSchema<Values>(values, validationSchema).then(
+          () => {
+            this.setState({ errors: {} });
+            if (onSuccess) {
+              onSuccess();
+            }
+          },
+          (err: any) =>
+            this.setState({ errors: yupToFormErrors(err), isSubmitting: false })
+        );
+      };
+
+      /**
+       * Run validations and update state accordingly
+       */
+      runValidations = (values: Values) => {
+        if (validate) {
+          const maybePromisedErrors = validate(values, this.props);
+          if (isPromise(maybePromisedErrors)) {
+            (validate(values, this.props) as Promise<any>).then(
+              () => {
+                this.setState({ errors: {} });
+              },
+              errors => this.setState({ errors, isSubmitting: false })
+            );
+          } else {
+            this.setErrors(maybePromisedErrors as FormikErrors<Values>);
+          }
+        }
+
+        if (validationSchema) {
+          this.runValidationSchema(values);
+        }
+      };
+
       handleChange = (e: React.ChangeEvent<any>) => {
+        if (isReactNative) {
+          console.error(
+            `Warning: Formik's handleChange does not work with React Native. Use setFieldValue and within a callback instead. For more info see https://github.com/jaredpalmer/formikhttps://github.com/jaredpalmer/formik#react-native`
+          );
+          return;
+        }
         e.persist();
         const { type, name, id, value, checked, outerHTML } = e.target;
         const field = name ? name : id;
-        const val = /number|range/.test(type) ? parseFloat(value) : /checkbox/.test(type) ? checked : value;
+        const val = /number|range/.test(type)
+          ? parseFloat(value)
+          : /checkbox/.test(type) ? checked : value;
 
         if (!field && process.env.NODE_ENV !== 'production') {
           console.error(
@@ -212,80 +312,181 @@ export function Formik<Props, Values extends FormikValues, Payload>({
 
   ${outerHTML}
 
-Formik cannot determine which value to update. See docs for more information: https://github.com/jaredpalmer/formik#handlechange-e-reactchangeeventany--void
+Formik cannot determine which value to update. For more info see https://github.com/jaredpalmer/formik#handlechange-e-reactchangeeventany--void
 `
           );
         }
 
-        const { values } = this.state;
         // Set form fields by name
-        this.setState(state => ({
-          ...state,
+        this.setState(prevState => ({
+          ...prevState,
           values: {
-            ...values as object,
+            ...prevState.values as object,
             [field]: val,
           },
         }));
-        // Validate against schema
-        validateFormData<Values>({ ...values as any, [field]: val }, validationSchema).then(
-          () => this.setState({ errors: {} }),
-          (err: any) => this.setState({ errors: yupToFormErrors(err) })
+
+        if (validateOnChange) {
+          this.runValidations(
+            {
+              ...this.state.values as object,
+              [field]: value,
+            } as Values
+          );
+        }
+      };
+
+      handleChangeValue = (field: string, value: any) => {
+        if (process.env.NODE_ENV !== 'production') {
+          console.warn(
+            `Warning: Formik\'s handleChangeValue is deprecated and may be removed in future releases. Use Formik's setFieldValue(field, value) and setFieldTouched(field, isTouched) instead. React will merge the updates under the hood and avoid a double render. For more info see https://github.com/jaredpalmer/formik#setfieldvalue-field-string-value-any--void`
+          );
+        }
+        // Set touched and form fields by name
+        this.setState(prevState => ({
+          ...prevState,
+          values: {
+            ...prevState.values as object,
+            [field]: value,
+          },
+          touched: {
+            ...prevState.touched as object,
+            [field]: true,
+          },
+        }));
+
+        this.runValidationSchema(
+          {
+            ...this.state.values as object,
+            [field]: value,
+          } as Values
         );
       };
 
+      setFieldValue = (field: keyof Values, value: any) => {
+        // Set form field by name
+        this.setState(prevState => ({
+          ...prevState,
+          values: {
+            ...prevState.values as object,
+            [field]: value,
+          },
+        }));
+
+        if (validateOnChange) {
+          this.runValidations(
+            {
+              ...this.state.values as object,
+              [field]: value,
+            } as Values
+          );
+        }
+      };
+
       handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
         e.preventDefault();
-        // setTouched();
-        // setSubmitting(true);
+        this.submitForm();
+      };
+
+      submitForm = () => {
         this.setState({
-          touched: touchAllFields(this.state.values),
+          touched: touchAllFields<FormikTouched<Values>>(this.state.values),
           isSubmitting: true,
         });
-        const { values } = this.state;
-        // Validate against schema
-        validateFormData<Values>(values, validationSchema).then(
-          () => {
-            this.setState({ errors: {} });
-            handleSubmit(mapValuesToPayload(this.state.values), {
-              setTouched: this.setTouched,
-              setErrors: this.setErrors,
-              setError: this.setError,
-              setSubmitting: this.setSubmitting,
-              setValues: this.setValues,
-              resetForm: this.resetForm,
-              props: this.props,
+
+        if (validate) {
+          const maybePromisedErrors =
+            validate(this.state.values, this.props) || {};
+          if (isPromise(maybePromisedErrors)) {
+            (validate(this.state.values, this.props) as Promise<any>).then(
+              () => {
+                this.setState({ errors: {} });
+                this.executeSubmit();
+              },
+              errors => this.setState({ errors, isSubmitting: false })
+            );
+            return;
+          } else {
+            this.setState({
+              errors: maybePromisedErrors as FormikErrors<Values>,
+              isSubmitting: Object.keys(maybePromisedErrors).length > 0,
             });
-          },
-          (err: any) => this.setState({ isSubmitting: false, errors: yupToFormErrors(err) })
-        );
+
+            // only submit if there are no errors
+            if (Object.keys(maybePromisedErrors).length === 0) {
+              this.executeSubmit();
+            }
+          }
+        } else if (validationSchema) {
+          this.runValidationSchema(this.state.values, this.executeSubmit);
+        } else {
+          this.executeSubmit();
+        }
+      };
+
+      executeSubmit = () => {
+        const values = mapValuesToPayload
+          ? mapValuesToPayload(this.state.values)
+          : this.state.values;
+        handleSubmit(values, {
+          setStatus: this.setStatus,
+          setTouched: this.setTouched,
+          setErrors: this.setErrors,
+          setError: this.setError,
+          setValues: this.setValues,
+          setFieldError: this.setFieldError,
+          setFieldValue: this.setFieldValue,
+          setFieldTouched: this.setFieldTouched,
+          setSubmitting: this.setSubmitting,
+          resetForm: this.resetForm,
+          submitForm: this.submitForm,
+          props: this.props,
+        });
       };
 
       handleBlur = (e: any) => {
+        if (isReactNative) {
+          console.error(
+            `Warning: Formik's handleBlur does not work with React Native. Use setFieldTouched(field, isTouched) within a callback instead. For more info see https://github.com/jaredpalmer/formik#setfieldtouched-field-string-istouched-boolean--void`
+          );
+          return;
+        }
         e.persist();
         const { name, id } = e.target;
         const field = name ? name : id;
-        this.setState(state => ({ touched: { ...state.touched, [field]: true } }));
+        this.setState(prevState => ({
+          touched: { ...prevState.touched as object, [field]: true },
+        }));
+
+        if (validateOnBlur) {
+          this.runValidations(this.state.values);
+        }
       };
 
-      handleChangeValue = (field: string, value: any) => {
-        const { touched, values } = this.state;
-        // Set touched and form fields by name
-        this.setState(state => ({
-          ...state,
-          values: {
-            ...values as object,
-            [field]: value,
-          },
+      setFieldTouched = (field: keyof Values, touched: boolean = true) => {
+        // Set touched field by name
+        this.setState(prevState => ({
+          ...prevState,
           touched: {
-            ...touched as object,
-            [field]: true,
+            ...prevState.touched as object,
+            [field]: touched,
+          },
+        }));
+
+        if (validateOnBlur) {
+          this.runValidations(this.state.values);
+        }
+      };
+
+      setFieldError = (field: keyof Values, message: string) => {
+        // Set form field by name
+        this.setState(prevState => ({
+          ...prevState,
+          errors: {
+            ...prevState.errors as object,
+            [field]: message,
           },
         }));
-        // Validate against schema
-        validateFormData<Values>({ ...values as any, [field]: value }, validationSchema).then(
-          () => this.setState({ errors: {} }),
-          (err: any) => this.setState({ errors: yupToFormErrors(err) })
-        );
       };
 
       resetForm = (nextProps?: Props) => {
@@ -294,7 +495,9 @@ Formik cannot determine which value to update. See docs for more information: ht
           errors: {},
           touched: {},
           error: undefined,
-          values: nextProps ? mapPropsToValues(nextProps) : mapPropsToValues(this.props),
+          values: nextProps
+            ? mapPropsToValues(nextProps)
+            : mapPropsToValues(this.props),
         });
       };
 
@@ -311,14 +514,20 @@ Formik cannot determine which value to update. See docs for more information: ht
       render() {
         return (
           <WrappedComponent
-            {...this.props as any}
-            {...this.state as any}
+            {...this.props}
+            {...this.state}
+            dirty={Object.values(this.state.touched).filter(Boolean).length > 0}
+            setStatus={this.setStatus}
             setError={this.setError}
+            setFieldError={this.setFieldError}
             setErrors={this.setErrors}
             setSubmitting={this.setSubmitting}
             setTouched={this.setTouched}
+            setFieldTouched={this.setFieldTouched}
             setValues={this.setValues}
+            setFieldValue={this.setFieldValue}
             resetForm={this.resetForm}
+            submitForm={this.submitForm}
             handleReset={this.handleReset}
             handleSubmit={this.handleSubmit}
             handleChange={this.handleChange}
@@ -328,9 +537,47 @@ Formik cannot determine which value to update. See docs for more information: ht
         );
       }
     }
-    // Make sure we preserve any custom statics on the original component.
-    // @see https://github.com/apollographql/react-apollo/blob/master/src/graphql.tsx
-    const FinalComponent = hoistNonReactStatics(Formik, WrappedComponent as React.ComponentClass<any>);
-    return FinalComponent as React.ComponentClass<InjectedFormikProps<Props, Values>>;
+    return hoistNonReactStatics<Props>(
+      Formik,
+      WrappedComponent as React.ComponentClass<
+        InjectedFormikProps<Props, Values>
+      > // cast type to ComponentClass (even if SFC)
+    ) as React.ComponentClass<Props>;
   };
 }
+
+/**
+ * Transform Yup ValidationError to a more usable object
+ */
+export function yupToFormErrors<Values>(yupError: any): FormikErrors<Values> {
+  let errors: FormikErrors<Values> = {};
+  for (let err of yupError.inner) {
+    if (!errors[err.path]) {
+      errors[err.path] = err.message;
+    }
+  }
+  return errors;
+}
+
+/**
+ * Validate a yup schema.
+ */
+export function validateYupSchema<T>(data: T, schema: any): Promise<void> {
+  let validateData: any = {};
+  for (let k in data) {
+    if (data.hasOwnProperty(k)) {
+      const key = String(k);
+      validateData[key] =
+        (data as any)[key] !== '' ? (data as any)[key] : undefined;
+    }
+  }
+  return schema.validate(validateData, { abortEarly: false });
+}
+
+export function touchAllFields<T>(fields: T): FormikTouched<T> {
+  let touched = {} as FormikTouched<T>;
+  for (let k of Object.keys(fields)) {
+    touched[k] = true;
+  }
+  return touched;
+}
diff --git a/src/hoistStatics.tsx b/src/hoistStatics.tsx
index 4af1fc3..f7483cf 100644
--- a/src/hoistStatics.tsx
+++ b/src/hoistStatics.tsx
@@ -17,11 +17,11 @@ const getPrototypeOf = Object.getPrototypeOf;
 const objectPrototype = getPrototypeOf && getPrototypeOf(Object);
 const getOwnPropertyNames = Object.getOwnPropertyNames;
 
-export function hoistNonReactStatics(
-  targetComponent: ComponentClass<any>,
+export function hoistNonReactStatics<P>(
+  targetComponent: ComponentClass<P>,
   sourceComponent: ComponentClass<any>,
   blacklist?: { [name: string]: boolean }
-) {
+): ComponentClass<P> {
   if (typeof sourceComponent !== 'string') {
     // don't hoist over string (html) components
 
@@ -42,7 +42,10 @@ export function hoistNonReactStatics(
       let key: string = keys[i];
       if (!REACT_STATICS[key] && (!blacklist || !blacklist[key])) {
         // Only hoist enumerables and non-enumerable functions
-        if (propIsEnumerable.call(sourceComponent, key) || typeof (targetComponent as any)[key] === 'function') {
+        if (
+          propIsEnumerable.call(sourceComponent, key) ||
+          typeof (targetComponent as any)[key] === 'function'
+        ) {
           try {
             // Avoid failures from read-only properties
             (targetComponent as any)[key] = (targetComponent as any)[key];
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..5740840
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,22 @@
+/**
+ * Returns whether something is a Promise or not.
+ * @param value anything
+ * 
+ * @see https://github.com/pburtchaell/redux-promise-middleware/blob/master/src/isPromise.js
+ */
+export function isPromise(value: any): boolean {
+  if (value !== null && typeof value === 'object') {
+    return value && typeof value.then === 'function';
+  }
+
+  return false;
+}
+
+/**
+ * True if running in React Native. 
+ */
+export const isReactNative =
+  typeof window !== 'undefined' &&
+  window.navigator &&
+  window.navigator.product &&
+  window.navigator.product === 'ReactNative';
diff --git a/tsconfig.json b/tsconfig.json
index 2e47183..c463794 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,7 @@
     "experimentalDecorators": false,
     "importHelpers": true,
     "jsx": "react",
-    "lib": ["es2015", "dom"],
+    "lib": ["es2015", "es2017.object", "dom"],
     "module": "es2015",
     "moduleResolution": "node",
     "noFallthroughCasesInSwitch": true,


Test patch:
diff --git a/test/formik.test.tsx b/test/formik.test.tsx
index 1da7839..43484c9 100644
--- a/test/formik.test.tsx
+++ b/test/formik.test.tsx
@@ -9,209 +9,488 @@ const Yup = require('yup');
 // tslint:disable-next-line:no-empty
 const noop = () => {};
 
-describe('Formik', () => {
-  interface Props {
-    user: {
-      name: string;
-    };
-    someFunction?: () => void;
-  }
-
-  interface Values {
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+interface Props {
+  user: {
     name: string;
-  }
-
-  const Form: React.SFC<InjectedFormikProps<Props, Values>> = ({
-    values,
-    handleSubmit,
-    handleChange,
-    handleBlur,
-    errors,
-    isSubmitting,
-  }) => {
-    return (
-      <form onSubmit={handleSubmit}>
-        <input
-          type="text"
-          onChange={handleChange}
-          onBlur={handleBlur}
-          value={values.name}
-          name="name"
-        />
-        {errors.name &&
-          <div id="feedback">
-            {errors.name}
-          </div>}
-        {isSubmitting && <div id="submitting">Submitting</div>}
-        <button type="submit">Submit</button>
-      </form>
-    );
   };
+  someFunction?: () => void;
+}
+
+interface Values {
+  name: string;
+}
+
+const Form: React.SFC<InjectedFormikProps<Props, Values>> = ({
+  values,
+  handleSubmit,
+  handleChange,
+  handleBlur,
+  setStatus,
+  status,
+  errors,
+  isSubmitting,
+}) => {
+  return (
+    <form onSubmit={handleSubmit}>
+      <input
+        type="text"
+        onChange={handleChange}
+        onBlur={handleBlur}
+        value={values.name}
+        name="name"
+      />
+      {errors.name &&
+        <div id="feedback">
+          {errors.name}
+        </div>}
+      {isSubmitting && <div id="submitting">Submitting</div>}
+      <button
+        id="statusButton"
+        onClick={() => setStatus({ myStatusMessage: 'True' })}
+      >
+        Call setStatus
+      </button>
+      {status &&
+        !!status.myStatusMessage &&
+        <div id="statusMessage">
+          {status.myStatusMessage}
+        </div>}
+      <button type="submit">Submit</button>
+    </form>
+  );
+};
+
+const FormFactory = (options = {}) =>
+  Formik<Props, Values, Values>({
+    mapPropsToValues: ({ user }) => ({ ...user }),
+    handleSubmit: noop,
+    ...options,
+  })(Form);
+
+const BasicForm = FormFactory();
 
-  it('initializes Formik state', () => {
-    const EnhancedForm = Formik<Props, Values, Values>({
-      validationSchema: Yup.object().shape({
-        name: Yup.string(),
-      }),
-      mapPropsToValues: ({ user }) => ({ ...user }),
-      handleSubmit: noop,
-    })(Form);
-    const hoc = mount(<EnhancedForm user={{ name: 'jared' }} />);
-    expect(hoc.state().isSubmitting).toBe(false);
-    expect(hoc.state().touched).toEqual({});
-    expect(hoc.state().values).toEqual({ name: 'jared' });
-    expect(hoc.state().errors).toEqual({});
+describe('Formik', () => {
+  it('should initialize Formik state and pass down props', () => {
+    const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().isSubmitting).toBe(false);
+    expect(tree.find(Form).props().touched).toEqual({});
+    expect(tree.find(Form).props().values).toEqual({ name: 'jared' });
+    expect(tree.find(Form).props().errors).toEqual({});
+    expect(tree.find(Form).props().dirty).toBe(false);
   });
 
-  describe('handleSubmit', () => {
-    it('calls preventDefault()', () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: noop,
-      })(Form);
-      const hoc = mount(<EnhancedForm user={{ name: 'jared' }} />);
-      const preventDefault = jest.fn();
-      hoc.find('form').simulate('submit', { preventDefault });
-      expect(preventDefault).toHaveBeenCalled();
-    });
-
-    it('touches all fields', () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: noop,
-      })(Form);
-      const hoc = mount(<EnhancedForm user={{ name: 'jared' }} />);
-      hoc.find('form').simulate('submit');
-      expect(hoc.state().isSubmitting).toBe(true);
-      expect(hoc.state().touched).toEqual({ name: true });
-    });
-
-    it('pushes submission state chagnes to child component', async () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string().required(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: noop,
-      })(Form);
-      const hoc = shallow(<EnhancedForm user={{ name: 'jared' }} />);
-
-      expect(hoc.find(Form).dive().find('#submitting')).toHaveLength(0);
-
-      hoc.find(Form).dive().find('form').simulate('submit', {
-        // tslint:disable-next-line:no-empty
-        preventDefault() {},
-      });
-      expect(hoc.find(Form).dive().find('#submitting')).toHaveLength(1);
-    });
-
-    it('can map values to payload', async () => {
-      interface Payload {
-        user: { name: string };
-      }
-      const EnhancedForm = Formik<Props, Values, Payload>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string().required(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        mapValuesToPayload: ({ name }) => ({ user: { name } }),
-        handleSubmit: payload => {
-          expect(payload).toEqual({ user: { name: 'jared' } });
-          expect(payload).not.toEqual({ name: 'jared' });
-        },
-      })(Form);
-      const hoc = shallow(<EnhancedForm user={{ name: 'jared' }} />);
-      hoc.find(Form).dive().find('form').simulate('submit', {
-        // tslint:disable-next-line:no-empty
-        preventDefault() {},
+  describe('FormikHandlers', () => {
+    describe('handleChange', () => {
+      it('sets values state', async () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+
+        // Simulate a change event in the inner Form component's input
+        tree.find(Form).dive().find('input').simulate('change', {
+          persist: noop,
+          target: {
+            id: 'name',
+            value: 'ian',
+          },
+        });
+
+        expect(tree.update().state().values).toEqual({ name: 'ian' });
+        expect(
+          tree.update().find(Form).dive().find('input').props().value
+        ).toEqual('ian');
+      });
+
+      it('updates values state via `name` instead of `id` attribute when both are present', async () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+
+        // Simulate a change event in the inner Form component's input
+        tree.find(Form).dive().find('input').simulate('change', {
+          persist: noop,
+          target: {
+            id: 'person-1-thinger',
+            name: 'name',
+            value: 'ian',
+          },
+        });
+
+        expect(tree.update().state().values).toEqual({ name: 'ian' });
+        expect(
+          tree.update().find(Form).dive().find('input').props().value
+        ).toEqual('ian');
+      });
+
+      it('runs validations if validateOnChange is set to true', async () => {
+        const validate = jest.fn(noop);
+        const ValidationForm = FormFactory({
+          validate,
+          validateOnChange: true,
+        });
+        const tree = shallow(<ValidationForm user={{ name: 'jared' }} />);
+        tree.find(Form).dive().find('input').simulate('change', {
+          persist: noop,
+          target: {
+            name: 'name',
+            value: 'ian',
+          },
+        });
+        expect(validate).toHaveBeenCalled();
+      });
+
+      it('does NOT run validations by default or if validateOnChange is set to false', async () => {
+        const validate = jest.fn(noop);
+        const ValidationForm = FormFactory({ validate });
+        const tree = shallow(<ValidationForm user={{ name: 'jared' }} />);
+        tree.find(Form).dive().find('input').simulate('change', {
+          persist: noop,
+          target: {
+            name: 'name',
+            value: 'ian',
+          },
+        });
+        expect(validate).not.toHaveBeenCalled();
       });
     });
-  });
 
-  describe('formikBag', () => {
-    it('handleBlur sets touched', async () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string().required(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: payload => {
-          expect(payload).toEqual({ name: 'jared' });
-        },
-      })(Form);
-
-      const hoc = shallow(<EnhancedForm user={{ name: 'jared' }} />);
-
-      // Simulate a blur event in the inner Form component's input
-      hoc.find(Form).dive().find('input').simulate('blur', {
-        persist: noop,
-        target: {
-          name: 'name',
-        },
-      });
-      expect(hoc.update().state().touched).toEqual({ name: true });
-    });
-
-    it('handleChange sets values, and updates inputs', async () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string().required(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: payload => {
-          expect(payload).toEqual({ name: 'jared' });
-        },
-      })(Form);
-
-      const hoc = shallow(<EnhancedForm user={{ name: 'jared' }} />);
-
-      // Simulate a change event in the inner Form component's input
-      hoc.find(Form).dive().find('input').simulate('change', {
-        persist: noop,
-        target: {
-          name: 'name',
-          value: 'ian',
-        },
-      });
-
-      expect(hoc.update().state().values).toEqual({ name: 'ian' });
-      expect(
-        hoc.update().find(Form).dive().find('input').props().value
-      ).toEqual('ian');
+    describe('handleBlur', () => {
+      it('sets touched state', () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+
+        // Simulate a blur event in the inner Form component's input
+        tree.find(Form).dive().find('input').simulate('blur', {
+          persist: noop,
+          target: {
+            id: 'name',
+          },
+        });
+        expect(tree.update().state().touched).toEqual({ name: true });
+      });
+
+      it('updates touched state via `name` instead of `id` attribute when both are present', () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+
+        // Simulate a blur event in the inner Form component's input
+        tree.find(Form).dive().find('input').simulate('blur', {
+          persist: noop,
+          target: {
+            id: 'person-1-name-blah',
+            name: 'name',
+          },
+        });
+        expect(tree.update().state().touched).toEqual({ name: true });
+      });
+
+      it('runs validations by default or if validateOnBlur is set to true ', async () => {
+        const validate = jest.fn(noop);
+        const ValidationForm = FormFactory({ validate });
+        const ValidationForm2 = FormFactory({ validate, validateOnBlur: true });
+        const tree = shallow(<ValidationForm user={{ name: 'jared' }} />);
+        const tree2 = shallow(<ValidationForm2 user={{ name: 'jared' }} />);
+        tree.find(Form).dive().find('input').simulate('blur', {
+          persist: noop,
+          target: {
+            name: 'name',
+          },
+        });
+        tree2.find(Form).dive().find('input').simulate('blur', {
+          persist: noop,
+          target: {
+            name: 'name',
+          },
+        });
+        expect(validate).toHaveBeenCalledTimes(2);
+      });
     });
 
+    describe('handleSubmit', () => {
+      it('should call preventDefault()', () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+        const preventDefault = jest.fn();
+        tree.find(Form).dive().find('form').simulate('submit', {
+          preventDefault,
+        });
+        expect(preventDefault).toHaveBeenCalled();
+      });
+
+      it('should touch all fields', () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+        tree.find(Form).dive().find('form').simulate('submit', {
+          preventDefault: noop,
+        });
+        expect(tree.update().state().touched).toEqual({ name: true });
+      });
+
+      it('should push submission state changes to child component', () => {
+        const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+
+        expect(tree.find(Form).dive().find('#submitting')).toHaveLength(0);
+
+        tree.find(Form).dive().find('form').simulate('submit', {
+          preventDefault: noop,
+        });
+
+        expect(tree.find(Form).dive().find('#submitting')).toHaveLength(1);
+      });
+
+      it('should correctly map form values to payload', () => {
+        interface Payload {
+          user: { name: string };
+        }
+        const CustomPayloadForm = Formik<Props, Values, Payload>({
+          mapPropsToValues: ({ user }) => ({ ...user }),
+          mapValuesToPayload: ({ name }) => ({ user: { name } }),
+          handleSubmit: payload => {
+            expect(payload).toEqual({ user: { name: 'jared' } });
+            expect(payload).not.toEqual({ name: 'jared' });
+          },
+        })(Form);
+        const tree = shallow(<CustomPayloadForm user={{ name: 'jared' }} />);
+        tree.find(Form).dive().find('form').simulate('submit', {
+          preventDefault: noop,
+        });
+      });
+
+      describe('with validate (SYNC)', () => {
+        it('should call validate if present', () => {
+          const validate = jest.fn().mockReturnValue({});
+          const ValidateForm = Formik<Props, Values, Values>({
+            validate,
+            mapPropsToValues: ({ user }) => ({ ...user }),
+            handleSubmit: noop,
+          })(Form);
+          const tree = shallow(<ValidateForm user={{ name: 'jared' }} />);
+          tree.find(Form).dive().find('form').simulate('submit', {
+            preventDefault: noop,
+          });
+          expect(validate).toHaveBeenCalled();
+        });
+
+        it('should submit the form if valid', () => {
+          const handleSubmit = jest.fn();
+          const ValidateForm = Formik<Props, Values, Values>({
+            validate: noop,
+            mapPropsToValues: ({ user }) => ({ ...user }),
+            handleSubmit,
+          })(Form);
+          const tree = shallow(<ValidateForm user={{ name: 'jared' }} />);
+          tree.find(Form).dive().find('form').simulate('submit', {
+            preventDefault: noop,
+          });
+          expect(handleSubmit).toHaveBeenCalled();
+        });
+
+        it('should not submit the form if invalid', () => {
+          const validate = jest.fn().mockReturnValue({ name: 'Error!' });
+          const handleSubmit = jest.fn();
+
+          const ValidateForm = Formik<Props, Values, Values>({
+            validate,
+            mapPropsToValues: ({ user }) => ({ ...user }),
+            handleSubmit,
+          })(Form);
+
+          const tree = shallow(<ValidateForm user={{ name: '' }} />);
+          tree.find(Form).dive().find('form').simulate('submit', {
+            preventDefault: noop,
+          });
+          expect(validate).toHaveBeenCalled();
+          expect(handleSubmit).not.toHaveBeenCalled();
+        });
+      });
+
+      describe('with validate (ASYNC)', () => {
+        it('should call validate if present', () => {
+          const validate = jest.fn(() => Promise.resolve({}));
+          const ValidateForm = Formik<Props, Values, Values>({
+            validate,
+            mapPropsToValues: ({ user }) => ({ ...user }),
+            handleSubmit: noop,
+          })(Form);
+          const tree = shallow(<ValidateForm user={{ name: 'jared' }} />);
+          tree.find(Form).dive().find('form').simulate('submit', {
+            preventDefault: noop,
+          });
+          expect(validate).toHaveBeenCalled();
+        });
+
+        it('should submit the form if valid', async () => {
+          const handleSubmit = jest.fn();
+
+          const ValidateForm = Formik<Props, Values, Values>({
+            validate: () => Promise.resolve({}),
+            mapPropsToValues: ({ user }) => ({ ...user }),
+            handleSubmit,
+          })(Form);
+
+          const tree = shallow(<ValidateForm user={{ name: '' }} />);
+          await tree.find(Form).props().submitForm();
+
+          expect(handleSubmit).toHaveBeenCalled();
+        });
+
+        it('should not submit the form if invalid', async () => {
+          const handleSubmit = jest.fn();
+
+          const ValidateForm = Formik<Props, Values, Values>({
+            validate: () =>
+              sleep(25).then(() => {
+                throw { name: 'error!' };
+              }),
+            mapPropsToValues: ({ user }) => ({ ...user }),
+            handleSubmit,
+          })(Form);
+
+          const tree = shallow(<ValidateForm user={{ name: '' }} />);
+          await tree.find(Form).props().submitForm();
+
+          expect(handleSubmit).not.toHaveBeenCalled();
+        });
+      });
+    });
+  });
+
+  describe('FormikActions', () => {
     it('setValues sets values', async () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string().required(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: noop,
-      })(Form);
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setValues({ name: 'ian' });
+      expect(tree.find(Form).dive().find('input').props().value).toEqual('ian');
+    });
+
+    it('setValues should run validations when validateOnChange is true', async () => {
+      const validate = jest.fn().mockReturnValue({});
+      const ValidateOnBlurForm = FormFactory({
+        validate,
+        validateOnChange: true,
+      });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setValues({ name: 'ian' });
+      expect(validate).toHaveBeenCalled();
+    });
 
-      const hoc = mount(<EnhancedForm user={{ name: 'jared' }} />);
-      hoc.find(Form).props().setValues({ name: 'ian' });
-      expect(hoc.find('input').props().value).toEqual('ian');
+    it('setValues should NOT run validations when validateOnChange is false or undefined', () => {
+      const validate = jest.fn();
+      const ValidateOnBlurForm = FormFactory({ validate });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setValues({ name: 'ian' });
+      expect(validate).not.toHaveBeenCalled();
+    });
+
+    it('setFieldValue sets value by key', () => {
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldValue('name', 'ian');
+      expect(tree.find(Form).dive().find('input').props().value).toEqual('ian');
+    });
+
+    it('setFieldValue should run validations when validateOnChange is true', () => {
+      const validate = jest.fn().mockReturnValue({});
+      const ValidateOnBlurForm = FormFactory({
+        validate,
+        validateOnChange: true,
+      });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldValue('name', 'ian');
+      expect(validate).toHaveBeenCalled();
+    });
+
+    it('setFieldValue should NOT run validations when validateOnChange is false or undefined', () => {
+      const validate = jest.fn();
+      const ValidateOnBlurForm = FormFactory({ validate });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldValue('name', 'ian');
+      expect(validate).not.toHaveBeenCalled();
+    });
+
+    it('setTouched sets touched', async () => {
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setTouched({ name: true });
+      expect(tree.find(Form).props().touched).toEqual({ name: true });
+    });
+
+    it('setTouched should run validations by default, or when validateOnBlur is true', async () => {
+      const validate = jest.fn().mockReturnValue({});
+      const ValidateOnBlurForm = FormFactory({
+        validate,
+      });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setTouched({ name: true });
+      expect(validate).toHaveBeenCalled();
+    });
+
+    it('setTouched should NOT run validations when validateOnBlur is false', () => {
+      const validate = jest.fn();
+      const ValidateOnBlurForm = FormFactory({
+        validate,
+        validateOnBlur: false,
+      });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setTouched({ name: true });
+      expect(validate).not.toHaveBeenCalled();
+    });
+
+    it('setFieldTouched sets touched by key', async () => {
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldTouched('name', true);
+      expect(tree.find(Form).props().touched).toEqual({ name: true });
+      expect(tree.find(Form).props().dirty).toBe(true);
+      tree.find(Form).props().setFieldTouched('name', false);
+      expect(tree.find(Form).props().touched).toEqual({ name: false });
+      expect(tree.find(Form).props().dirty).toBe(false);
+    });
+
+    it('setFieldTouched should run validations when validateOnBlur is true', async () => {
+      const validate = jest.fn().mockReturnValue({});
+      const ValidateOnBlurForm = FormFactory({
+        validate,
+        validateOnBlur: true,
+      });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldTouched('name', true);
+      expect(validate).toHaveBeenCalled();
+    });
+
+    it('setFieldTouched should NOT run validations when validateOnBlur is true', async () => {
+      const validate = jest.fn().mockReturnValue({});
+      const ValidateOnBlurForm = FormFactory({
+        validate,
+        validateOnBlur: false,
+      });
+      const tree = shallow(<ValidateOnBlurForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldTouched('name', true);
+      expect(validate).not.toHaveBeenCalled();
     });
 
     it('setErrors sets error object', async () => {
-      const EnhancedForm = Formik<Props, Values, Values>({
-        validationSchema: Yup.object().shape({
-          name: Yup.string().required(),
-        }),
-        mapPropsToValues: ({ user }) => ({ ...user }),
-        handleSubmit: noop,
-      })(Form);
-
-      const hoc = mount(<EnhancedForm user={{ name: 'jared' }} />);
-      hoc.find(Form).props().setErrors({ name: 'Required' });
-      expect(hoc.find('#feedback').text()).toEqual('Required');
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setErrors({ name: 'Required' });
+      expect(tree.find(Form).dive().find('#feedback').text()).toEqual(
+        'Required'
+      );
+    });
+
+    it('setFieldError sets error by key', async () => {
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).props().setFieldError('name', 'Required');
+      expect(tree.find(Form).dive().find('#feedback').text()).toEqual(
+        'Required'
+      );
+    });
+
+    it('setStatus sets status object', async () => {
+      const tree = shallow(<BasicForm user={{ name: 'jared' }} />);
+      tree.find(Form).dive().find('#statusButton').simulate('click');
+      expect(tree.find(Form).dive().find('#statusMessage')).toHaveLength(1);
+    });
+  });
+
+  describe('FormikComputedProps', () => {
+    it('dirty, should update as soon as any input is touched', () => {
+      const tree = mount(<BasicForm user={{ name: 'jared' }} />);
+
+      expect(tree.find(Form).props().dirty).toBe(false);
+
+      tree.setState({ touched: { name: true } });
+
+      expect(tree.update().find(Form).props().dirty).toBe(true);
     });
   });
 });
diff --git a/test/helpers.test.ts b/test/helpers.test.ts
index 0a0957a..4045206 100644
--- a/test/helpers.test.ts
+++ b/test/helpers.test.ts
@@ -1,4 +1,4 @@
-import { validateFormData, yupToFormErrors } from '../src/formik';
+import { validateYupSchema, yupToFormErrors } from '../src/formik';
 
 const Yup = require('yup');
 const schema = Yup.object().shape({
@@ -18,10 +18,10 @@ describe('helpers', () => {
     });
   });
 
-  describe('validateFormData()', () => {
+  describe('validateYupSchema()', () => {
     it('should validate', async () => {
       try {
-        await validateFormData({}, schema);
+        await validateYupSchema({}, schema);
       } catch (e) {
         expect(e.name).toEqual('ValidationError');
         expect(e.errors).toEqual(['required']);
@@ -30,7 +30,7 @@ describe('helpers', () => {
 
     it('should stringify all values', async () => {
       try {
-        const result = await validateFormData({ name: 1 }, schema);
+        const result = await validateYupSchema({ name: 1 }, schema);
         expect(result).not.toEqual({ name: 1 });
         expect(result).toEqual({ name: '1' });
       } catch (e) {