Repository: final-form/react-final-form (js)

Issue:
Expose method of subscribing to form data
Feature request: I would like to be able to subscribe to form updates for the purpose of autosave without submission. I am currently using `FormSpy` to do this but its render function is also triggered by parents rerendering:

```javascript
<FormSpy subscription={{ values: true }}>
   {({ values }) => {
     onChange(values);
     return null;
   }}
</FormSpy>
```
It would nice if there was easy way to listen to updates without it also trigging on renders and gives the granularity subscriptions offer (`debug` is too noisy). I could get hold of `reactFinalForm` from `context` but this in not part of the public API.

One of the following would work for me:

* An `onChange` prop on `FormSpy` 
* A simular component that accepts `subcription` and `onChange` as `props`
* A component that fetches `reactFinalForm` from the `context` 
* `reactFinalForm` being exposed as a render prop on Form

_Am I missing something, is there already a way to do this?_

Pull request:
Revert "Revert "Added onChange prop to FormSpy""
Reverts #43. Changed my mind again. 🙈

Closes #41.

Patch:
diff --git a/README.md b/README.md
index 32198d1..e0ce6f6 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
 
 ✅ Opt-in subscriptions - only update on the state you need!
 
-✅ 💥 **2.2k gzipped** 💥
+✅ 💥 **2.5k gzipped** 💥
 
 ---
 
@@ -177,8 +177,9 @@ const MyForm = () => (
   * [`FormSpyProps`](#formspyprops)
     * [`children?: ((props: FormSpyRenderProps) => React.Node) | React.Node`](#children-props-formspyrenderprops--reactnode--reactnode)
     * [`component?: React.ComponentType<FormSpyRenderProps>`](#component-reactcomponenttypeformspyrenderprops)
+    * [`onChange?: (formState: FormState) => void`](#onchange-formstate-formstate--void)
     * [`render?: (props: FormSpyRenderProps) => React.Node`](#render-props-formspyrenderprops--reactnode)
-    * [`formSubscription?: FormSubscription`](#formsubscription-formsubscription)
+    * [`subscription?: FormSubscription`](#subscription-formsubscription-1)
   * [`FormSpyRenderProps`](#formspyrenderprops)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -538,19 +539,28 @@ of the ways to render: `component`, `render`, or `children`.
 #### `children?: ((props: FormSpyRenderProps) => React.Node) | React.Node`
 
 A render function that is given [`FormSpyRenderProps`](#formspyrenderprops), as
-well as any non-API props passed into the `<FormSpy/>` component.
+well as any non-API props passed into the `<FormSpy/>` component. Will not be
+called if an `onChange` prop is provided.
 
 #### `component?: React.ComponentType<FormSpyRenderProps>`
 
 A component that is given [`FormSpyRenderProps`](#formspyrenderprops) as props,
-as well as any non-API props passed into the `<FormSpy/>` component.
+as well as any non-API props passed into the `<FormSpy/>` component. Will not be
+called if an `onChange` prop is provided.
+
+#### `onChange?: (formState: FormState) => void`
+
+A change listener that will be called with form state whenever the form state,
+as subscribed to by the `subscription` prop, has changed. When an `onChange`
+prop is provided, the `<FormSpy/>` will not render anything.
 
 #### `render?: (props: FormSpyRenderProps) => React.Node`
 
 A render function that is given [`FormSpyRenderProps`](#formspyrenderprops), as
-well as any non-API props passed into the `<FormSpy/>` component.
+well as any non-API props passed into the `<FormSpy/>` component. Will not be
+called if an `onChange` prop is provided.
 
-#### `formSubscription?: FormSubscription`
+#### `subscription?: FormSubscription`
 
 A
 [`FormSubscription`](https://github.com/final-form/final-form#formsubscription--string-boolean-)
diff --git a/docs/comparison.md b/docs/comparison.md
index 9b7cf15..9c531aa 100644
--- a/docs/comparison.md
+++ b/docs/comparison.md
@@ -6,7 +6,7 @@ and you'd like it to be), please correct the mistake.
 
 | Feature                               |              [🏁 React-Final-Form](https://github.com/final-form/react-final-form#-react-final-form)              | [Formik](https://github.com/jaredpalmer/formik)  |  [Redux-Form](https://github.com/erikras/redux-form)  |
 | ------------------------------------- | :---------------------------------------------------------------------------------------------------------------: | :----------------------------------------------: | :---------------------------------------------------: |
-| Bundle Size                           | [3.5k](https://bundlephobia.com/result?p=final-form) + [2.2k](https://bundlephobia.com/result?p=react-final-form) | [7.5k](https://bundlephobia.com/result?p=formik) | [26.8k](https://bundlephobia.com/result?p=redux-form) |
+| Bundle Size                           | [3.5k](https://bundlephobia.com/result?p=final-form) + [2.5k](https://bundlephobia.com/result?p=react-final-form) | [7.5k](https://bundlephobia.com/result?p=formik) | [26.8k](https://bundlephobia.com/result?p=redux-form) |
 | Works without Redux                   |                                                        ✅                                                         |                        ✅                        |                          ❌                           |
 | Record-Level Sync Validation          |                                                        ✅                                                         |                        ✅                        |                          ✅                           |
 | Record-Level Async Validation         |                                                        ✅                                                         |                        ✅                        |                          ✅                           |
diff --git a/src/FormSpy.js b/src/FormSpy.js
index 93d6583..d8703eb 100644
--- a/src/FormSpy.js
+++ b/src/FormSpy.js
@@ -31,6 +31,9 @@ export default class FormSpy extends React.PureComponent<Props, State> {
           this.notify(state)
         } else {
           initialState = state
+          if (props.onChange) {
+            props.onChange(state)
+          }
         }
       })
     }
@@ -47,7 +50,12 @@ export default class FormSpy extends React.PureComponent<Props, State> {
     )
   }
 
-  notify = (state: FormState) => this.setState({ state })
+  notify = (state: FormState) => {
+    this.setState({ state })
+    if (this.props.onChange) {
+      this.props.onChange(state)
+    }
+  }
 
   componentWillReceiveProps(nextProps: Props) {
     const { subscription } = nextProps
@@ -71,8 +79,10 @@ export default class FormSpy extends React.PureComponent<Props, State> {
   }
 
   render() {
-    const { subscription, ...rest } = this.props
-    return renderComponent({ ...rest, ...this.state.state }, 'FormSpy')
+    const { onChange, subscription, ...rest } = this.props
+    return onChange
+      ? null
+      : renderComponent({ ...rest, ...this.state.state }, 'FormSpy')
   }
 }
 
diff --git a/src/types.js.flow b/src/types.js.flow
index d8febc0..49b3ad9 100644
--- a/src/types.js.flow
+++ b/src/types.js.flow
@@ -46,7 +46,7 @@ export type FormRenderProps = {
   reset: () => void
 } & FormState
 
-export type FormSpyRenderProps = {} & FormState
+export type FormSpyRenderProps = FormState
 
 export type RenderableProps<T> = $Shape<{
   children: ((props: T) => React.Node) | React.Node,
@@ -68,5 +68,6 @@ export type FieldProps = {
 } & RenderableProps<FieldRenderProps>
 
 export type FormSpyProps = {
+  onChange?: (formState: FormState) => void,
   subscription?: FormSubscription
 } & RenderableProps<FormSpyRenderProps>


Test patch:
diff --git a/src/FormSpy.test.js b/src/FormSpy.test.js
index 31f2c94..93fc577 100644
--- a/src/FormSpy.test.js
+++ b/src/FormSpy.test.js
@@ -202,4 +202,91 @@ describe('FormSpy', () => {
     const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button')
     TestUtils.Simulate.click(button)
   })
+
+  it('should call onChange', () => {
+    const input = jest.fn(({ input }) => <input {...input} />)
+    const onChange = jest.fn()
+    TestUtils.renderIntoDocument(
+      <Form onSubmit={onSubmitMock}>
+        {() => (
+          <form>
+            <FormSpy subscription={{ dirty: true }} onChange={onChange} />
+            <Field name="foo" render={input} />
+          </form>
+        )}
+      </Form>
+    )
+    expect(input).toHaveBeenCalled()
+    expect(input).toHaveBeenCalledTimes(1)
+    expect(onChange).toHaveBeenCalled()
+    expect(onChange).toHaveBeenCalledTimes(1)
+    expect(onChange).toHaveBeenCalledWith({ dirty: false })
+
+    input.mock.calls[0][0].input.onChange('bar')
+
+    expect(input).toHaveBeenCalledTimes(2)
+    expect(onChange).toHaveBeenCalledTimes(2)
+    expect(onChange).toHaveBeenCalledWith({ dirty: true })
+  })
+
+  it('should not render with render prop when given onChange', () => {
+    const onChange = jest.fn()
+    const render = jest.fn()
+    TestUtils.renderIntoDocument(
+      <Form onSubmit={onSubmitMock}>
+        {() => (
+          <form>
+            <FormSpy
+              subscription={{ dirty: true }}
+              onChange={onChange}
+              render={render}
+            />
+          </form>
+        )}
+      </Form>
+    )
+    expect(onChange).toHaveBeenCalled()
+    expect(onChange).toHaveBeenCalledTimes(1)
+    expect(render).not.toHaveBeenCalled()
+  })
+
+  it('should not render with child render prop when given onChange', () => {
+    const onChange = jest.fn()
+    const render = jest.fn()
+    TestUtils.renderIntoDocument(
+      <Form onSubmit={onSubmitMock}>
+        {() => (
+          <form>
+            <FormSpy subscription={{ dirty: true }} onChange={onChange}>
+              {render}
+            </FormSpy>
+          </form>
+        )}
+      </Form>
+    )
+    expect(onChange).toHaveBeenCalled()
+    expect(onChange).toHaveBeenCalledTimes(1)
+    expect(render).not.toHaveBeenCalled()
+  })
+
+  it('should not render with component prop when given onChange', () => {
+    const onChange = jest.fn()
+    const render = jest.fn()
+    TestUtils.renderIntoDocument(
+      <Form onSubmit={onSubmitMock}>
+        {() => (
+          <form>
+            <FormSpy
+              subscription={{ dirty: true }}
+              onChange={onChange}
+              component={render}
+            />
+          </form>
+        )}
+      </Form>
+    )
+    expect(onChange).toHaveBeenCalled()
+    expect(onChange).toHaveBeenCalledTimes(1)
+    expect(render).not.toHaveBeenCalled()
+  })
 })