Building a Simple Form

Let's build a very basic form with validations and error messages, using redux-form. There's a few basic requirements -

  • Attempting to submit an invalid form should display validation error messages
  • Fields can be populated with initial values
  • When valid values are submitted, the form values are returned in a callback

Typically, developing a Redux application involves writing loads of action creators and reducers, but redux-form has abstracted all of that away and it hardly feels like writing Redux anymore. In this project we won't really be writing anything to do with the Redux store or action creators; there's two very simple exceptions but they're insignificant compared to everything redux-form is doing to behind the scenes to make things work.

A Visual Demo

Here's what the form will eventually become. This post will only cover the basics of architecting such a form, the actual demo has a bit more functionality like formatting and parsing.

Designs by @sbelous and built using inuitcss.

Architecture

It's important to organize the structure of our components and clearly define responsibilities.

Container

Components which have been connected to the redux store act as a bridge to the forms.

  • defines onSubmit callback
  • provides initialValues
  • includes a Form component

Form

An aggregate of form Sections along with custom behavior definitions.

  • decorated by redux-form
  • implements the actual <form> element
  • defines validations
  • knows when the form is invalid
  • composites generic Section components under desired namespaces

Section

A group of inputs coupled together.

  • these are related groups of fields which can be reused inside of <FormSection>
  • they use <Field>, <Fields> and other Sections

Control

The basic building blocks of a form and they are connected to redux-form.

  • receives input value to display
  • knows about validation errors
  • calls onChange when value needs to change

Basic Setup

If you're developing on Codepen, be sure to set your pen's settings to use Babel for JavaScript preprocessing.

The initial tasks are to include external libraries and get things to the point where we can render a component to the page. This will involve creating a Redux store that knows about our reducers, accepts action objects, and passes it's state down to Containers.

Required Libraries

These are the minimum requirements for a React, Redux and ReduxForm app.

  • react - the foundation of our components
  • react-dom - connects the React render tree to the browser DOM
  • redux - manages application state
  • react-redux - allows us to create Containers to bridge the app's Redux store and React render tree
  • redux-form - handles all of the form magic
  • classnames - very handy utility for generating dynamic class names
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-form/6.4.3/redux-form.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/dedupe.js"></script>

The HTML

We just need a simple mount point for the app to attach to.

  <div id="js-app"></div>

ReactDOM will be responsible for attaching the application here.

  ReactDOM.render(..., document.getElementById('js-app');

Configure Redux and Start the App

Since AppContainer is where the form will live, it'll need access to the Redux store; this is enabled by <Provider store> which passes store down to every child that makes use of ReactRedux.connect.

  const {Provider} = ReactRedux;
const store = configureStore({});
ReactDOM.render((
  <Provider {...{store}}>
    <AppContainer />
  </Provider>
), document.getElementById('js-app'));

A Redux store is basically just a plain JavaScript object with a first-level structure matching what we define in Redux.combineReducers. These reducers are very simple functions that modify the values of the namespace they're responsible for. Our store has a shipments reducer for tracking the shipment records created by the form, and a form reducer which comes from redux-form to handle all of the form magic.

An initialState can be provided to seed the store with initial values.

  function configureStore (initialState) {
  const reducers = Redux.combineReducers({
    shipments: shipmentsReducer,
    form: ReduxForm.reducer
  });
  return Redux.createStore(reducers, initialState);
}

Actions and Reducers

Once the form is submitted with valid values for a new shipement record, we dispatch an action object. This object will be picked up by all reducers which decide whether to respond based on the type property. In the case of adding a shipment, we return an object with {type: 'SHIPMENT_ADD'} and the values for the shipment. This action will be passed into ReactRedux.connect() where the returned action object will be used to feed the reducers.

  const shipmentActions = {
  add: shipment => ({type: 'SHIPMENT_ADD', shipment})
};

A reducer accepts two arguments, the current state and an action to be processed. These reducers will need to provide their default state. It's also good practice to never mutate the state, only return a new structure that will become the next state. In this case state is initially an empty array and becomes populated with shipment records when it receives actions with {type: 'SHIPMENT_ADD'}.

  function shipmentsReducer (state = [], action) {
  if (action.type === 'SHIPMENT_ADD') {
    return [
      ...state,
      action.shipment
    ];
  }
  return state;
}

Containers

Components which have been connected to the Redux store using ReactRedux.connect() are called Containers. They will have access to the entire Redux store and will be able to send action objects to the reducers.

AppContainer will be responsible for displaying the form and handling the returned values using onSubmit. The form can be pre-populated using initialValues.

  <ShipmentForm
    onSubmit={this.handleSubmit}
    {...{initialValues}}
/>

To pre-populate the form, initialValues reflects the sturucture of the form. In our form, there will be a <FormSection name="senderDetails"> which will have <Field name="name"> and <Field name="address">.

  const initialValues = {
  senderDetails: {
    name: 'ACME Co.',
    address: '123 Fake Ln.'
  }
};

In this case, once the form has been submitted, we'll be calling actions to send the values to the shipments reducer and clear the form's filled in values so the user can add another record.

  // ...
this.props.shipmentAdd(values);
this.props.resetForm('shipments');
// ...

All of this logic is put to use in AppComponent, which is the non-decorated version of AppContainer.

  class AppComponent extends React.Component {
  static propTypes = {
    resetForm: React.PropTypes.func.isRequired,
    shipmentAdd: React.PropTypes.func.isRequired
  };

  constructor (props) {
    super(props);
    this.handleSubmit = this.onSubmit();
  }

  onSubmit () {
    return values => {
      this.props.shipmentAdd(values);
      this.props.resetForm('shipments');
    };
  }

  render () {
    const initialValues = {
      senderDetails: {
        name: 'ACME Co.',
        address: '123 Fake Ln.'
      }
    };
    return (
      <div className="o-wrapper">
        <div className="o-layout">
          <div className="o-layout__item u-1/1">
            <h1 className="c-h1">{'Create a shipment'}</h1>
            <p>{'Use the form below to create shipment records'}</p>
            <ShipmentForm
                onSubmit={this.handleSubmit}
                {...{initialValues}}
            />
          </div>
        </div>
      </div>
    );
  }
}

To turn AppComponent into an actual container, it needs to be connected to the Redux store. It will accept two arguments, mapStateToProps and mapDispatchToProps. The former is a function that extracts bits of the store that the container will need, and the latter is an object accepting plain action creators to connect to the reducers.

  const AppContainer = ReactRedux.connect(state => ({
}), {
  resetForm: ReduxForm.reset,
  shipmentAdd: shipmentActions.add
})(AppComponent);

Forms

The main purpose of a form is to organize generic Sections under new namespaces, using <FormSection>. Similarly to Containers, Forms are Components that have been decorated, this time by ReduxForm.reduxForm(). The previously defined onSubmit in AppComponent has been turned into this.props.handleSubmit, which is then provided to the <form> element. Forms also know if they are in a valid state, which is useful for disabling the submit button for instance.

  class ShipmentComponent extends React.Component {
  render () {
    const {handleSubmit} = this.props;
    return (
      <form onSubmit={handleSubmit}>
        <h4 className="c-h4 u-margin-bottom-small">{'Sender Details'}</h4>
        <FormSection name="senderDetails">
          <PersonDetailsSection />
        </FormSection>
        <button
            className="c-form-button c-form-button--primary c-form-button--block"
            type="submit"
        >{'Submit'}</button>
        <p className="c-text-small c-text-small--muted u-center-text">{'All fields are required'}</p>
      </form>
    );
  }
}

Decorating ShipmentComponent to become ShipmentForm involves passing in a few options. The first option form: 'shipments' is a unique name for the values to be housed in, and will be used for actions mostly; they will not play a role in initialValues' structure. Validation provides error feedback to the form and prevents submitting (can't call this.props.handleSubmit) until everything passes, and the rules are defined using the validate argument.

  const ShipmentForm = ReduxForm.reduxForm({
  form: 'shipments',
  validate: validateFields({
    senderDetails: validators.personDetails
  })
})(ShipmentComponent);

The validation for this form defines senderDetails which matches the <FormSection name="senderDetails" /> defined in the actual form structure. A validation function validators.personDetails is provided, and it will be fed the values from this section of the form and must return an object containing any error messages. If the return value is an empty object, the validation passes.

  const validators = {
  personDetails: values => {
    const errors = {};
    if (!values || !values.name) {
      errors.name = 'Required';
    }
    if (!values || !values.address) {
      errors.address = 'Required';
    }
    return errors;
  }
};

The validation used for personDetails is very basic, it just checks to see if the property is defined (such as personDetails.address) and returns an object with the strings 'Required' which will be displayed to the user.

Each of these validators only focuses on a small section of the form, which makes them very reusable. The validateFields function is responsible for calling the validators with the approriate sections of the form values and combining those errors into the proper structure.

  function validateFields (validators) {
  return values => {
    return Object.keys(validators).map(name => ({
      name,
      error: validators[name](values[name])
    })).reduce((p, {name, error}) => (
      Object.keys(name).length ? {...p, [name]: error} : p
    ), {});
  };
}

Sections

Sections are customized but reusable groupings of Control inputs. For instance, the PersonDetailsSection implements InputControl with custom placeholder, type and name. Once a <Field name="b"> is nested under a <FormSection name="a">, it will be accessed on submit as values => values.a.b. Sections typically implement <Field component name>, <Fields component names> and/or other Sections.

  const {Field} = ReduxForm;
class PersonDetailsSection extends React.Component {
  render () {
    return (
      <div className="o-layout u-margin-bottom-small">
        <div className="o-layout__item u-1/1 u-1/2@tablet">
          <Field
              component={InputControl}
              name="name"
              placeholder="Name"
              type="text"
          />
        </div>
        <div className="o-layout__item u-1/1 u-1/2@tablet">
          <Field
              component={InputControl}
              name="address"
              placeholder="Address"
              type="text"
          />
        </div>
      </div>
    );
  }
}

Controls

The form decoration done earlier using ReduxForm.reduxForm has allowed a connection between the Redux store and usages of <Field>, where Controls are provided. This means that our Control components will have access to the input value and metadata like whether the field has been touched or has errors.

  const {
  input: {
  },
  meta: {
    error,
    touched
  }
} = this.props;

Here is the final Control component, it will display an input field and error messages.

  class InputControl extends React.Component {
  static propTypes = {
    placeholder: React.PropTypes.string,
    type: React.PropTypes.string.isRequired
  };

  render () {
    const {
      input,
      type,
      placeholder,
      meta: {
        error,
        touched
      }
    } = this.props;
    const className = classNames({
      'c-input-control': true,
      'c-input-control--error': touched && error
    });
    return (
      <div {...{className}}>
        <input
            className="c-input-control__input"
            {...input}
            {...{type, placeholder}}
        />
        <div className="c-input-control__hint c-text-small">{touched && error}</div>
      </div>
    );
  }
}

Conclusion

This architecture of Containters, Forms, Sections and Controls has worked very well for me so far. The main thing to learn is that using redux-form will remove a lot of reason to define action creators and reducers; if you find yourself building either to make a form work, there's a chance it may not be necessary.

There are many more powerful features to redux-form, such as <FieldArray>, normalizing, and handling of server error messages. Below is another example of a form built using this architecture, it is a bit more complicated.


6,931 0 56