Introduction

Recently, I’ve been working on a React/Redux app which uses the redux-form library for web forms. It’s a great library which seamlessly integrates all aspects of web forms, including validation, into the React/Redux data flow.

Below is a very simple example of a redux-form based form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react';
import { Form, Field, reduxForm } from 'redux-form';
import { TextField } from 'redux-form-material-ui';
import RaisedButton from 'material-ui/RaisedButton';

class SimpleForm extends Component {
  render() {
    const { handleSubmit, valid, submitting, onSubmit } = this.props;

    return (
      <Form onSubmit={handleSubmit(onSubmit)}>
        <Field name="firstName" hintText="First Name" component={TextField} fullWidth />
        <Field name="lastName" hintText="Last Name" component={TextField} fullWidth />
        <RaisedButton label="Submit" type="submit" disabled={valid || submitting} />
      </Form>
    );
  }
}

function validate(form) {
  const errors = {};
  if (!form.firstName) {
    errors.firstName = "required";
  }

  return errors;
}

export default reduxForm({ form: 'simple-form', validate })(SimpleForm);

This example uses the material-ui library and its redux-form bindings. We defined 2 text fields (firstName and lastName) and a submission button, which is only enabled if the form is valid and is not being submitted. Finally, we register the form with Redux and bind it to a validation function using the reduxForm function.

Behind the scenes, redux-form invokes the validate function on every field change and keeps track of whether the user has touched any field. This information is then available via the component properties. You can look at the official documentation of redux-form to see all relevant properties and quite a few examples.

While this is a very simple and intuitive way to build forms, it has one major problem. It assumes that the form and the submission button are in the same component. However, in some cases we need to submit a form from outside of its component. For example, a form can be embedded as a step/screen within a wizard. Thus, the “NEXT” button of the parent wizard component must submit the embedded forms.

This setup poses 2 problems:

  • The button in the parent component can not access the form component properties. Therefore, it can not check if the form is ready to be submitted (e.g. all fields are valid);
  • The button in the parent component can not programmatically submit the form;

Solution 1

A form component can observe when it becomes ready to be submitted and signal the parent component via a specially provided callback

The following form demonstrates this idea. It does not have a submission button and accepts a callback function enabledCallback via its properties. We use the componentDidUpdate life cycle method to detect when a component becomes (un)available for submission:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { Component } from 'react';
import { Form, Field, reduxForm } from 'redux-form';
import { TextField } from 'redux-form-material-ui';

class ChildComponentForm extends Component {
  componentDidUpdate(prevProps) {
    const enabled = (this.props.valid && !this.props.submitting) || !this.props.anyTouched;
    const wasEnabled = (prevProps.valid && !prevProps.submitting) || !prevProps.anyTouched;

    if (enabled !== wasEnabled) {
      // Signal to the parent component that the form can be submitted
      this.props.enabledCallback(enabled);
    }
  }

  render() {
    const { handleSubmit, onSubmit } = this.props;

    // Standard form but no submit button - it's in the parent
    return (
      <Form onSubmit={handleSubmit(onSubmit)}>
        <Field name="firstName" hintText="First Name" component={TextField} fullWidth />
        <Field name="lastName" hintText="Last Name" component={TextField} fullWidth />
      </Form>
    );
  }
}

function validate(form) {
  const errors = {};
  if (!form.firstName) {
    errors.firstName = "required";
  }

  return errors;
}

export default reduxForm({ form: 'child-form', validate })(ChildComponentForm);

From the parent component, we can pass a callback function which saves in the state whether the form can be submitted. Based on the state, we can conditionally disable the submission button.

While rendering the form in its parent, we preserve a reference to its respective component as a member variable. We can then use this reference to programmatically submit the form when the submission button is clicked.

The following demonstrates this approach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, { Component } from 'react';
import RaisedButton from 'material-ui/RaisedButton';
import ChildComponentForm from './ChildComponentForm';

export default class App extends Component {
  constructor(props) {
    super(props);

    // We'll keep in state whether the submission button is enabled
    this.state = { submitButtonEnabled: true };
  }

  render() {
    // Render the form - pass the callback and obtain a reference
    return (
      <div>
        <ChildComponentForm
          onSubmit={this.onSubmit}
          // While rendering - save a reference to the form
          ref={form => this.formReference = form}
          // Pass a callback for when enabled
          enabledCallback={this.enabledCallback}
        />
        <RaisedButton
          label="Submit"
          disabled={!this.state.submitButtonEnabled}
          onClick={this.onSubmitClick}
        />
      </div>
    );
  }

  // Callback for when the form is submitted
  onSubmit = (form) => alert(JSON.stringify(form, null, 2))
  
  // Callback for the button - use the saved form reference to submit it 
  onSubmitClick = () => this.formReference.submit()

  // Callback for when form can be submitted
  enabledCallback = (enabled) => this.setState(prevState => ({
    ...prevState,
    submitButtonEnabled: enabled
  }))
}

Solution 2

Another approach is to use the global functions and selectors provided by redux-form. They vary from version to version - in the example below we’ll be using version 7.2.0.

Every redux-form has a unique name, which is used as an identifier in redux. In the example below, we define a simple form and associate it with a name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react';
import { Form, Field, reduxForm } from 'redux-form';
import { TextField } from 'redux-form-material-ui';

class ChildComponentForm extends Component {
  render() {
    const { handleSubmit, onSubmit } = this.props;

    // Standard form but no submit button - it's in the parent
    return (
      <Form onSubmit={handleSubmit(onSubmit)}>
        <Field name="firstName" hintText="First Name" component={TextField} fullWidth />
        <Field name="lastName" hintText="Last Name" component={TextField} fullWidth />
      </Form>
    );
  }
}

function validate(form) {
  const errors = {};
  if (!form.firstName) {
    errors.firstName = "required";
  }

  return errors;
}

// The global name for this form will be 'child-form'
export default reduxForm({ form: 'child-form', validate })(ChildComponentForm);

This form is agnostic of its parent which will perform its submission and inspect its validity, as in this example usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { Component } from 'react';
import { connect } from 'react-redux';
import RaisedButton from 'material-ui/RaisedButton';
import ChildComponentForm from './ChildComponentForm';
import { bindActionCreators } from 'redux';
import { isValid, isSubmitting, submit } from 'redux-form';

// A simple component which uses the form
export class App extends Component {
  render() {
    return (
      <div>
        <ChildComponentForm onSubmit={this.onSubmit} />
        <RaisedButton
          label="Submit"
          disabled={!this.props.formEnabled}
          onClick={this.props.submitForm}
        />
      </div>
    );
  }

  // Callback for when the form is submitted
  onSubmit = (form) => alert(JSON.stringify(form, null, 2))
}

// Use redux-form selectors to check the form's state - e.g. valid, submitting
function mapStateToProps(state) {
  return {
    formEnabled: isValid('child-form')(state) && !isSubmitting('child-form')(state) 
  };
}

function mapDispatchToProps(dispatch) {
  // Bind an action, which submit the form by its name
  return bindActionCreators({
    submitForm: () => submit('child-form')
  }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(App);