29
votes

redux-form is a very compelling library for providing redux bindings for forms in a react application, which should be super-convenient. Unfortunately, using the library's own examples, I'm failing to actually bind anything, which is super in-convenient.

I'm attempting to make use of the sample code on the project site, and finding multiple obstacles, despite attempting to reproduce it faithfully. Where am I misinterpreting this API? Has the API shifted since the demo code was written? Am I missing some critical and obvious piece of redux knowledge?

Problem 1: the signature for the handleSubmit method should be handleSubmit(data). But handleSubmit is currently receiving only the React syntheticEvent from the submit action, and no data. (In fact, using the example as-written was sending two separate events, seemingly because of the stacked onSubmit action on the form and the onClick on the button.) Where is that data supposed to be coming from, and why am I failing to pass it to the handler?

Problem 2: there's a critical fields object that must be defined on the form parent and supplied as prop to your form. Unfortunately, the shape of that fields object is not explained in the docs, nor its purpose, really. Is it essentially the initial 'state' object? A simple object container for redux-form to use at runtime for errors, etc? I've gotten it to stop erroring by matching the props on fields to the field names in connectReduxForm, but because the data isn't binding, I'm assuming it's not the right shape.

Problem 3: The fields are supposed to be auto-bound to handlers for onBlur and onChange, so that they update the store appropriately. That's never happening. (Which we can see thanks to the Redux dev-tools. However, handleSubmit is successfully dispatching the initialize action, which suggests the store, reducer, and other basic plumbing are all working.)

Problem 4: validateContact is firing once on init, but never again.

This is unfortunately too complex for a simple Fiddle, but the entire repo (it's just the basic ReduxStarterApp, plus this form POC) is available here.

And, here is the outer component:

import React       from 'react';
import { connect } from 'react-redux';
import {initialize} from 'redux-form';

import ContactForm from '../components/simple-form/SimpleForm.js';

const mapStateToProps = (state) => ({
  counter : state.counter
});
export class HomeView extends React.Component {
  static propTypes = {
    dispatch : React.PropTypes.func.isRequired,
    counter  : React.PropTypes.number
  }

  constructor () {
    super();
  }
  handleSubmit(event, data) {
    event.preventDefault();
    console.log(event); // this should be the data, but is an event
    console.log(data); // no data here, either...
    console.log('Submission received!', data);
    this.props.dispatch(initialize('contact', {})); // clear form: THIS works
    return false;
  }

  _increment () {
    this.props.dispatch({ type : 'COUNTER_INCREMENT' });
  }


  render () {
    const fields = {
      name: '',
      address: '',
      phone: ''
    };

    return (
      <div className='container text-center'>
        <h1>Welcome to the React Redux Starter Kit</h1>
        <h2>Sample Counter: {this.props.counter}</h2>
        <button className='btn btn-default'
                onClick={::this._increment}>
          Increment
        </button>
        <ContactForm handleSubmit={this.handleSubmit.bind(this)} fields={fields} />
      </div>
    );
  }
}

export default connect(mapStateToProps)(HomeView);

And the inner form component:

import React, {Component, PropTypes} from 'react';
import {connectReduxForm} from 'redux-form';

function validateContact(data) {
  console.log("validating");
  console.log(data);
  const errors = {};
  if (!data.name) {
    errors.name = 'Required';
  }
  if (data.address && data.address.length > 50) {
    errors.address = 'Must be fewer than 50 characters';
  }
  if (!data.phone) {
    errors.phone = 'Required';
  } else if (!/\d{3}-\d{3}-\d{4}/.test(data.phone)) {
    errors.phone = 'Phone must match the form "999-999-9999"';
  }
  return errors;
}

class ContactForm extends Component {
  static propTypes = {
    fields: PropTypes.object.isRequired,
    handleSubmit: PropTypes.func.isRequired
  }

  render() {
    const { fields: {name, address, phone}, handleSubmit } = this.props;
    return (
      <form onSubmit={handleSubmit}>
        <label>Name</label>
        <input type="text" {...name}/>     {/* will pass value, onBlur and onChange */}
        {name.error && name.touched && <div>{name.error}</div>}

        <label>Address</label>
        <input type="text" {...address}/>  {/* will pass value, onBlur and onChange*/}
        {address.error && address.touched && <div>{address.error}</div>}

        <label>Phone</label>
        <input type="text" {...phone}/>    {/* will pass value, onBlur and onChange */}
        {phone.error && phone.touched && <div>{phone.error}</div>}

        <button type='submit'>Submit</button>
      </form>
    );
  }
}

// apply connectReduxForm() and include synchronous validation
ContactForm = connectReduxForm({
  form: 'contact',                      // the name of your form and the key to
                                        // where your form's state will be mounted
  fields: ['name', 'address', 'phone'], // a list of all your fields in your form
  validate: validateContact             // a synchronous validation function
})(ContactForm);

// export the wrapped component
export default ContactForm;
2
Thank you for this elaborate question! I had the same difficulties and I find the redux-form documentation and examples intricate and incomplete. With your and @Jonny Buchanan's help I was able to proceed (Although I have a complicated data-structure and I'm not finished yet). Adding the redux devtools extension also helped a lot with debugging!Christian Benke

2 Answers

23
votes

connectReduxForm wraps your component with another component which handles passing in the fields and handleSubmit props, but you're blowing those away by passing them in yourself.

Try this instead (renamed the prop to onSubmit):

<ContactForm onSubmit={this.handleSubmit.bind(this)}/>

And in ContactForm, pass your own submit handler to the handleSubmit function provided by redux-form:

<form onSubmit={handleSubmit(this.props.onSubmit)}>

I recommend using the React developer tools to get a better picture of what's going on - you'll see how redux-form wraps your component and passes it a whole bunch of props, as documented in its README.

redux-form composition in React developer tools

8
votes

Thanks to Jonny Buchanan, who covered the most important point: don't do as I did and automatically assume that if props are required in your component, you must need to provide them yourself. The whole point of the higher-order function that is connectReduxForm is to provide them in the wrapper component. Fixing that immediately gave me event-handlers, for everything except Submit.

The other critical oversight was here:

NOTE – If you are not doing the connect()ing yourself (and it is recommended that you do not, unless you have an advanced use case that requires it), you must mount the reducer at form.

I didn't catch the point of that. But, the implementation is here:

import { createStore, combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
const reducers = {
  // ... your other reducers here ...
  form: formReducer           // <---- Mounted at 'form'
}
const reducer = combineReducers(reducers);
const store = createStore(reducer);

The formReducer can't be referenced at formReducer, but requires the syntax form: formReducer. This was the correction that properly enabled handleSubmit.