Create an intelligent Form with React Redux Form

This recipe describes how to make intelligent forms using React Redux Form. I started looking for a solution when I realized that I am probably not the only one having form related requirements, such as:

  • Onchange field validation
  • Show an error message below the current field
  • Simple mapping between state (members) and form fields
  • Handlers to handle submit, change, update etc.
  • Initial field values
  • Clear all form fields at once

A few seconds after the moment I realized that there is most likely already a solution existing out there, Google confirmed this when it pointed me to React Redux Form. And the good news (well, there is a lot) is that the documentation is first class and there is a zero-boilerplate react component waiting that takes a minimal set of properties and meet all of the above listed requirements.

Installation

# Dependencies (you probably already have these)
npm install react react-dom redux react-redux --save

# version 1.x.x
npm install react-redux-form@latest --save

Zero-Boilerplate versus setting up your own store

What follows below is the Zero-Boilerplate example from the React Redux Form website.

import styles from './myapp.module.scss';import React from 'react';
import { LocalForm, Control } from 'react-redux-form';

export default class MyApp extends React.Component {
    handleChange(values) { ... }
    handleUpdate(form) { ... }
    handleSubmit(values) { ... }
    render() {
        return (
            <LocalForm
                onUpdate={(form) => this.handleUpdate(form)}
                onChange={(values) => this.handleChange(values)}
                onSubmit={(values) => this.handleSubmit(values)}
            >
                <Control.text model=".username" />
                <Control.text model=".password" />
        </LocalForm>
        );
    }
}

I believe, however, that the LocalForm is really missing a handle to dispatch actions to invoke reducers etc. So you could update the LocalForm to get a dispatcher, as shown below:

import styles from './myapp.module.scss';import React from 'react';
import { LocalForm, Control } from 'react-redux-form';

export default class MyApp extends React.Component {
    handleChange(values) { ... }
    handleUpdate(form) { ... }
    handleSubmit(values) { ... }

    dispatcher: Function;

    attachDispatch(dispatch) {
        this.dispatcher = dispatch,
    }

    render() {
        return (
            <LocalForm
                onUpdate={(form) => this.handleUpdate(form)}
                onChange={(values) => this.handleChange(values)}
                onSubmit={(values) => this.handleSubmit(values)}
                getDispatch={(dispatch) => this.attachDispatch(dispatch)}
            >
                <Control.text model=".username" />
                <Control.text model=".password" />
        </LocalForm>
        );
    }
}

But I still found it rather difficult to get the reducers working correctly e.g. to update state after dispatching actions that modified the store and then to see my changes reflected. Then I noticed that to create your own store for a React Redux Form is super easy. What I show below is a more complex example of React Redux Form as part of a SPFx Clientside Webpart. However, the main changes to the previous sample are:

  • Use Form instead of LocalForm
  • Enclose the Form component with a Provider component
  • Create a custom store and use it to set the Provider’s store property
import * as React from 'react';
import styles from './myapp.module.scss';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { Form, Control, Errors, actions, combineForms } from 'react-redux-form';
import { ActionButton, DefaultButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { WebPartContext } from "@microsoft/sp-webpart-base";

// The customer store, configured with an initial value
const store = createStore(combineForms({
    user: { name: "Initial User" },
}));

// Custom validators
const required: any = (val: string) => val && val.length > 0;
const notSetOrNumber: any = (val: string) => val && val.length === 0 || (Number(val) >= 1);

// Interface describing the (only) model in the store
export interface user {
    name: string;
}

export interface IFormProps {
    context: WebPartContext
    user: user
}

export interface IFormState {
    user: user
}

export default class myapp extends React.Component<IFormProps, IFormState> {

    constructor(props: IFormProps) {
        super(props)
        // Without a store handle it would not be possible to dispatch
        // actions this early in the life cycle. In this case the
        // initial values are "overwritten" and the state is updated and
        // changes are reflected immediately.
        store.dispatch(actions.change("user", this.props.user))
    }

    private handleChange(values) {
        console.log("handling change...");
    }

    private handleUpdate(form) {
        console.log("handling change...");
    }

    private handleSubmit(values) {
        console.log("handling change...");
    }

    private handleReset() {
        console.log("handling reset...")
        // Will reset the store to the initial values i.e. { name: "Initial User" }
        store.dispatch(actions.reset("user.name"))
    }

    public render(): React.ReactElement<any> {
        return (
            <Provider store={store}>
                <Form
                    onUpdate={(form) => this.handleUpdate(form)}
                    onChange={(values) => this.handleChange(values)}
                    onSubmit={(values) => this.handleSubmit(values)}
                    model="user" // Name of the model for the form, see custom store definition
                >
                    <div className={styles.row}>
                        <div className={styles.columnLeft}>
                            <label>{"Example Input Control"}:</label>
                        </div>
                        <div className={styles.columnRight}>
                            <Control.text model=".name" component={TextField} required={true} mapProps={{
                                onKeyUp: (props) => props.onChange
                            }}
                                validators={{
                                    isRequired: required,
                                    isNumber: notSetOrNumber
                                }}
                            />
                            <Errors model=".name"
                                className={styles.errors}
                                show="touched"
                                messages={{
                                    isRequired: "is required message",
                                    isNumber: "is number message"
                                }}
                            />
                        </div>
                    </div>
                    <div className={styles.footer}>
                        <div className={styles.row}>
                            <div className={styles.columnLeft}>
                                <ActionButton
                                    iconProps={{ iconName: "Clear" }}
                                    text={"Clear"}
                                    onClick={() => this.handleReset()}
                                />
                            </div>
                            <div className={styles.buttonRight}>
                                <DefaultButton id="DefaultSubmit"
                                    primary={true}
                                    text={"Click me"}
                                    type="submit" />
                            </div>
                        </div>
                    </div>

                </Form>
            </Provider>
        );
    }
}

Final thoughts

With the example above it is really easy to:

  • Set initial values
  • Overwrite initial values with values passed by parameter
  • Reset the form
  • Dispatch actions in general

Having more fine grained control over dispatching actions (and not needing to wait until the component mounted and then using the getDispatch LocalForm property to get a reference for the dispatcher) should be convincing enough to use your own store.

You May Also Like

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: